Merge remote-tracking branch 'origin/develop' into enhancement/OP-2040_Maya_Render_creator-should-have-configurable-options

This commit is contained in:
Allan Ihsan 2022-05-12 00:29:49 +03:00
commit e65798b612
72 changed files with 2507 additions and 1823 deletions

View file

@ -266,7 +266,7 @@ class AssetLoader(LoaderPlugin):
# Only containerise if it's not already a collection from a .blend file.
# representation = context["representation"]["name"]
# if representation != "blend":
# from avalon.blender.pipeline import containerise
# from openpype.hosts.blender.api.pipeline import containerise
# return containerise(
# name=name,
# namespace=namespace,

View file

@ -45,7 +45,8 @@ def install():
This is where you install menus and register families, data
and loaders into fusion.
It is called automatically when installing via `api.install(avalon.fusion)`
It is called automatically when installing via
`openpype.pipeline.install_host(openpype.hosts.fusion.api)`
See the Maya equivalent for inspiration on how to implement this.

View file

@ -6,7 +6,7 @@ from openpype.pipeline import load
class FusionSetFrameRangeLoader(load.LoaderPlugin):
"""Specific loader of Alembic for the avalon.animation family"""
"""Set frame range excluding pre- and post-handles"""
families = ["animation",
"camera",
@ -40,7 +40,7 @@ class FusionSetFrameRangeLoader(load.LoaderPlugin):
class FusionSetFrameRangeWithHandlesLoader(load.LoaderPlugin):
"""Specific loader of Alembic for the avalon.animation family"""
"""Set frame range including pre- and post-handles"""
families = ["animation",
"camera",

View file

@ -463,7 +463,7 @@ def imprint(node_id, data, remove=False):
remove (bool): Removes the data from the scene.
Example:
>>> from avalon.harmony import lib
>>> from openpype.hosts.harmony.api import lib
>>> node = "Top/Display"
>>> data = {"str": "someting", "int": 1, "float": 0.32, "bool": True}
>>> lib.imprint(layer, data)

View file

@ -144,6 +144,7 @@ class CollectFarmRender(openpype.lib.abstract_collect_render.
label=node.split("/")[1],
subset=subset_name,
asset=legacy_io.Session["AVALON_ASSET"],
task=task_name,
attachTo=False,
setMembers=[node],
publish=info[4],

View file

@ -553,10 +553,10 @@ class PublishAction(QtWidgets.QAction):
#
# '''
# import hiero.core
# from avalon.nuke import imprint
# from pype.hosts.nuke import (
# lib as nklib
# )
# from openpype.hosts.nuke.api.lib import (
# BuildWorkfile,
# imprint
# )
#
# # check if the file exists if does then Raise "File exists!"
# if os.path.exists(filepath):
@ -583,8 +583,7 @@ class PublishAction(QtWidgets.QAction):
#
# nuke_script.addNode(root_node)
#
# # here to call pype.hosts.nuke.lib.BuildWorkfile
# script_builder = nklib.BuildWorkfile(
# script_builder = BuildWorkfile(
# root_node=root_node,
# root_path=root_path,
# nodes=nuke_script.getNodes(),

View file

@ -6,7 +6,7 @@ from openpype.pipeline import load
class SetFrameRangeLoader(load.LoaderPlugin):
"""Set Houdini frame range"""
"""Set frame range excluding pre- and post-handles"""
families = [
"animation",
@ -44,7 +44,7 @@ class SetFrameRangeLoader(load.LoaderPlugin):
class SetFrameRangeWithHandlesLoader(load.LoaderPlugin):
"""Set Maya frame range including pre- and post-handles"""
"""Set frame range including pre- and post-handles"""
families = [
"animation",

View file

@ -7,7 +7,7 @@ from openpype.hosts.houdini.api import pipeline
class AbcLoader(load.LoaderPlugin):
"""Specific loader of Alembic for the avalon.animation family"""
"""Load Alembic"""
families = ["model", "animation", "pointcache", "gpuCache"]
label = "Load Alembic"

View file

@ -0,0 +1,75 @@
import os
from openpype.pipeline import (
load,
get_representation_path,
)
from openpype.hosts.houdini.api import pipeline
class AbcArchiveLoader(load.LoaderPlugin):
"""Load Alembic as full geometry network hierarchy """
families = ["model", "animation", "pointcache", "gpuCache"]
label = "Load Alembic as Archive"
representations = ["abc"]
order = -5
icon = "code-fork"
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
import hou
# Format file name, Houdini only wants forward slashes
file_path = os.path.normpath(self.fname)
file_path = file_path.replace("\\", "/")
# Get the root node
obj = hou.node("/obj")
# Define node name
namespace = namespace if namespace else context["asset"]["name"]
node_name = "{}_{}".format(namespace, name) if namespace else name
# Create an Alembic archive node
node = obj.createNode("alembicarchive", node_name=node_name)
node.moveToGoodPosition()
# TODO: add FPS of project / asset
node.setParms({"fileName": file_path,
"channelRef": True})
# Apply some magic
node.parm("buildHierarchy").pressButton()
node.moveToGoodPosition()
nodes = [node]
self[:] = nodes
return pipeline.containerise(node_name,
namespace,
nodes,
context,
self.__class__.__name__,
suffix="")
def update(self, container, representation):
node = container["node"]
# Update the file path
file_path = get_representation_path(representation)
file_path = file_path.replace("\\", "/")
# Update attributes
node.setParms({"fileName": file_path,
"representation": str(representation["_id"])})
# Rebuild
node.parm("buildHierarchy").pressButton()
def remove(self, container):
node = container["node"]
node.destroy()

View file

@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
import os
import re
from openpype.pipeline import (
load,
get_representation_path,
)
from openpype.hosts.houdini.api import pipeline
class BgeoLoader(load.LoaderPlugin):
"""Load bgeo files to Houdini."""
label = "Load bgeo"
families = ["model", "pointcache", "bgeo"]
representations = [
"bgeo", "bgeosc", "bgeogz",
"bgeo.sc", "bgeo.gz", "bgeo.lzma", "bgeo.bz2"]
order = -10
icon = "code-fork"
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
import hou
# Get the root node
obj = hou.node("/obj")
# Define node name
namespace = namespace if namespace else context["asset"]["name"]
node_name = "{}_{}".format(namespace, name) if namespace else name
# Create a new geo node
container = obj.createNode("geo", node_name=node_name)
is_sequence = bool(context["representation"]["context"].get("frame"))
# Remove the file node, it only loads static meshes
# Houdini 17 has removed the file node from the geo node
file_node = container.node("file1")
if file_node:
file_node.destroy()
# Explicitly create a file node
file_node = container.createNode("file", node_name=node_name)
file_node.setParms({"file": self.format_path(self.fname, is_sequence)})
# Set display on last node
file_node.setDisplayFlag(True)
nodes = [container, file_node]
self[:] = nodes
return pipeline.containerise(
node_name,
namespace,
nodes,
context,
self.__class__.__name__,
suffix="",
)
@staticmethod
def format_path(path, is_sequence):
"""Format file path correctly for single bgeo or bgeo sequence."""
if not os.path.exists(path):
raise RuntimeError("Path does not exist: %s" % path)
# The path is either a single file or sequence in a folder.
if not is_sequence:
filename = path
print("single")
else:
filename = re.sub(r"(.*)\.(\d+)\.(bgeo.*)", "\\1.$F4.\\3", path)
filename = os.path.join(path, filename)
filename = os.path.normpath(filename)
filename = filename.replace("\\", "/")
return filename
def update(self, container, representation):
node = container["node"]
try:
file_node = next(
n for n in node.children() if n.type().name() == "file"
)
except StopIteration:
self.log.error("Could not find node of type `alembic`")
return
# Update the file path
file_path = get_representation_path(representation)
file_path = self.format_path(file_path)
file_node.setParms({"fileName": file_path})
# Update attribute
node.setParms({"representation": str(representation["_id"])})
def remove(self, container):
node = container["node"]
node.destroy()

View file

@ -78,7 +78,7 @@ def transfer_non_default_values(src, dest, ignore=None):
class CameraLoader(load.LoaderPlugin):
"""Specific loader of Alembic for the avalon.animation family"""
"""Load camera from an Alembic file"""
families = ["camera"]
label = "Load Camera (abc)"

View file

@ -42,9 +42,9 @@ def get_image_avalon_container():
class ImageLoader(load.LoaderPlugin):
"""Specific loader of Alembic for the avalon.animation family"""
"""Load images into COP2"""
families = ["colorbleed.imagesequence"]
families = ["imagesequence"]
label = "Load Image (COP2)"
representations = ["*"]
order = -10

View file

@ -9,7 +9,7 @@ from openpype.hosts.houdini.api import pipeline
class VdbLoader(load.LoaderPlugin):
"""Specific loader of Alembic for the avalon.animation family"""
"""Load VDB"""
families = ["vdbcache"]
label = "Load VDB"

View file

@ -1,3 +1,7 @@
import os
import subprocess
from openpype.lib.vendor_bin_utils import find_executable
from openpype.pipeline import load
@ -14,12 +18,7 @@ class ShowInUsdview(load.LoaderPlugin):
def load(self, context, name=None, namespace=None, data=None):
import os
import subprocess
import avalon.lib as lib
usdview = lib.which("usdview")
usdview = find_executable("usdview")
filepath = os.path.normpath(self.fname)
filepath = filepath.replace("\\", "/")

View file

@ -77,6 +77,7 @@ IMAGE_PREFIXES = {
"arnold": "defaultRenderGlobals.imageFilePrefix",
"renderman": "rmanGlobals.imageFileFormat",
"redshift": "defaultRenderGlobals.imageFilePrefix",
"mayahardware2": "defaultRenderGlobals.imageFilePrefix"
}
RENDERMAN_IMAGE_DIR = "maya/<scene>/<layer>"
@ -169,7 +170,8 @@ def get(layer, render_instance=None):
"arnold": RenderProductsArnold,
"vray": RenderProductsVray,
"redshift": RenderProductsRedshift,
"renderman": RenderProductsRenderman
"renderman": RenderProductsRenderman,
"mayahardware2": RenderProductsMayaHardware
}.get(renderer_name.lower(), None)
if renderer is None:
raise UnsupportedRendererException(
@ -1173,6 +1175,67 @@ class RenderProductsRenderman(ARenderProducts):
return new_files
class RenderProductsMayaHardware(ARenderProducts):
"""Expected files for MayaHardware renderer."""
renderer = "mayahardware2"
extensions = [
{"label": "JPEG", "index": 8, "extension": "jpg"},
{"label": "PNG", "index": 32, "extension": "png"},
{"label": "EXR(exr)", "index": 40, "extension": "exr"}
]
def _get_extension(self, value):
result = None
if isinstance(value, int):
extensions = {
extension["index"]: extension["extension"]
for extension in self.extensions
}
try:
result = extensions[value]
except KeyError:
raise NotImplementedError(
"Could not find extension for {}".format(value)
)
if isinstance(value, six.string_types):
extensions = {
extension["label"]: extension["extension"]
for extension in self.extensions
}
try:
result = extensions[value]
except KeyError:
raise NotImplementedError(
"Could not find extension for {}".format(value)
)
if not result:
raise NotImplementedError(
"Could not find extension for {}".format(value)
)
return result
def get_render_products(self):
"""Get all AOVs.
See Also:
:func:`ARenderProducts.get_render_products()`
"""
ext = self._get_extension(
self._get_attr("defaultRenderGlobals.imageFormat")
)
products = []
for cam in self.get_renderable_cameras():
product = RenderProduct(productName="beauty", ext=ext, camera=cam)
products.append(product)
return products
class AOVError(Exception):
"""Custom exception for determining AOVs."""

View file

@ -2,7 +2,7 @@ import openpype.hosts.maya.api.plugin
class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
"""Specific loader of Alembic for the avalon.animation family"""
"""Loader to reference an Alembic file"""
families = ["animation",
"camera",

View file

@ -10,7 +10,7 @@ from openpype.hosts.maya.api.lib import (
class SetFrameRangeLoader(load.LoaderPlugin):
"""Specific loader of Alembic for the avalon.animation family"""
"""Set frame range excluding pre- and post-handles"""
families = ["animation",
"camera",
@ -44,7 +44,7 @@ class SetFrameRangeLoader(load.LoaderPlugin):
class SetFrameRangeWithHandlesLoader(load.LoaderPlugin):
"""Specific loader of Alembic for the avalon.animation family"""
"""Set frame range including pre- and post-handles"""
families = ["animation",
"camera",

View file

@ -16,7 +16,7 @@ from openpype.hosts.maya.api.pipeline import containerise
class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
"""Load the Proxy"""
"""Load Arnold Proxy as reference"""
families = ["ass"]
representations = ["ass"]

View file

@ -8,7 +8,7 @@ from openpype.api import get_project_settings
class GpuCacheLoader(load.LoaderPlugin):
"""Load model Alembic as gpuCache"""
"""Load Alembic as gpuCache"""
families = ["model"]
representations = ["abc"]

View file

@ -12,7 +12,7 @@ from openpype.hosts.maya.api.lib import maintained_selection
class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
"""Load the model"""
"""Reference file"""
families = ["model",
"pointcache",

View file

@ -74,6 +74,7 @@ def _fix_duplicate_vvg_callbacks():
class LoadVDBtoVRay(load.LoaderPlugin):
"""Load OpenVDB in a V-Ray Volume Grid"""
families = ["vdbcache"]
representations = ["vdb"]

View file

@ -287,7 +287,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
"byFrameStep": int(
self.get_render_attribute("byFrameStep",
layer=layer_name)),
"renderer": renderer,
"renderer": self.get_render_attribute(
"currentRenderer", layer=layer_name).lower(),
# instance subset
"family": "renderlayer",
"families": ["renderlayer"],

View file

@ -50,15 +50,17 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
'vray': 'vraySettings.fileNamePrefix',
'arnold': 'defaultRenderGlobals.imageFilePrefix',
'renderman': 'rmanGlobals.imageFileFormat',
'redshift': 'defaultRenderGlobals.imageFilePrefix'
'redshift': 'defaultRenderGlobals.imageFilePrefix',
'mayahardware2': 'defaultRenderGlobals.imageFilePrefix',
}
ImagePrefixTokens = {
'arnold': 'maya/<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>', # noqa
'mentalray': 'maya/<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>', # noqa: E501
'arnold': 'maya/<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>', # noqa: E501
'redshift': 'maya/<Scene>/<RenderLayer>/<RenderLayer>',
'vray': 'maya/<Scene>/<Layer>/<Layer>',
'renderman': '<layer>{aov_separator}<aov>.<f4>.<ext>' # noqa
'renderman': '<layer>{aov_separator}<aov>.<f4>.<ext>',
'mayahardware2': 'maya/<Scene>/<RenderLayer>/<RenderLayer>',
}
_aov_chars = {
@ -234,7 +236,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
# load validation definitions from settings
validation_settings = (
instance.context.data["project_settings"]["maya"]["publish"]["ValidateRenderSettings"].get( # noqa: E501
"{}_render_attributes".format(renderer))
"{}_render_attributes".format(renderer)) or []
)
# go through definitions and test if such node.attribute exists.

View file

@ -1,4 +1,5 @@
import os
from pprint import pformat
import re
import six
import platform
@ -193,7 +194,7 @@ def imprint(node, data, tab=None):
Examples:
```
import nuke
from avalon.nuke import lib
from openpype.hosts.nuke.api import lib
node = nuke.createNode("NoOp")
data = {
@ -364,17 +365,15 @@ def fix_data_for_node_create(data):
return data
def add_write_node(name, **kwarg):
def add_write_node_legacy(name, **kwarg):
"""Adding nuke write node
Arguments:
name (str): nuke node name
kwarg (attrs): data for nuke knobs
Returns:
node (obj): nuke write node
"""
frame_range = kwarg.get("frame_range", None)
frame_range = kwarg.get("use_range_limit", None)
w = nuke.createNode(
"Write",
@ -400,6 +399,35 @@ def add_write_node(name, **kwarg):
return w
def add_write_node(name, file_path, knobs, **kwarg):
"""Adding nuke write node
Arguments:
name (str): nuke node name
kwarg (attrs): data for nuke knobs
Returns:
node (obj): nuke write node
"""
frame_range = kwarg.get("use_range_limit", None)
w = nuke.createNode(
"Write",
"name {}".format(name))
w["file"].setValue(file_path)
# finally add knob overrides
set_node_knobs_from_settings(w, knobs, **kwarg)
if frame_range:
w["use_limit"].setValue(True)
w["first"].setValue(frame_range[0])
w["last"].setValue(frame_range[1])
return w
def read_avalon_data(node):
"""Return user-defined knobs from given `node`
@ -500,13 +528,9 @@ def get_nuke_imageio_settings():
return get_anatomy_settings(Context.project_name)["imageio"]["nuke"]
def get_created_node_imageio_setting(**kwarg):
def get_created_node_imageio_setting_legacy(nodeclass, creator, subset):
''' Get preset data for dataflow (fileType, compression, bitDepth)
'''
log.debug(kwarg)
nodeclass = kwarg.get("nodeclass", None)
creator = kwarg.get("creator", None)
subset = kwarg.get("subset", None)
assert any([creator, nodeclass]), nuke.message(
"`{}`: Missing mandatory kwargs `host`, `cls`".format(__file__))
@ -578,6 +602,97 @@ def get_created_node_imageio_setting(**kwarg):
return imageio_node
def get_imageio_node_setting(node_class, plugin_name, subset):
''' Get preset data for dataflow (fileType, compression, bitDepth)
'''
imageio_nodes = get_nuke_imageio_settings()["nodes"]
required_nodes = imageio_nodes["requiredNodes"]
imageio_node = None
for node in required_nodes:
log.info(node)
if (
node_class in node["nukeNodeClass"]
and plugin_name in node["plugins"]
):
imageio_node = node
break
log.debug("__ imageio_node: {}".format(imageio_node))
if not imageio_node:
return
# find overrides and update knobs with them
get_imageio_node_override_setting(
node_class,
plugin_name,
subset,
imageio_node["knobs"]
)
log.info("ImageIO node: {}".format(imageio_node))
return imageio_node
def get_imageio_node_override_setting(
node_class, plugin_name, subset, knobs_settings
):
''' Get imageio node overrides from settings
'''
imageio_nodes = get_nuke_imageio_settings()["nodes"]
override_nodes = imageio_nodes["overrideNodes"]
# find matching override node
override_imageio_node = None
for onode in override_nodes:
log.info(onode)
if node_class not in onode["nukeNodeClass"]:
continue
if plugin_name not in onode["plugins"]:
continue
if (
onode["subsets"]
and not any(re.search(s, subset) for s in onode["subsets"])
):
continue
override_imageio_node = onode
break
log.debug("__ override_imageio_node: {}".format(override_imageio_node))
# add overrides to imageio_node
if override_imageio_node:
# get all knob names in imageio_node
knob_names = [k["name"] for k in knobs_settings]
for oknob in override_imageio_node["knobs"]:
for knob in knobs_settings:
# override matching knob name
if oknob["name"] == knob["name"]:
log.debug(
"_ overriding knob: `{}` > `{}`".format(
knob, oknob
))
if not oknob["value"]:
# remove original knob if no value found in oknob
knobs_settings.remove(knob)
else:
# override knob value with oknob's
knob["value"] = oknob["value"]
# add missing knobs into imageio_node
if oknob["name"] not in knob_names:
log.debug(
"_ adding knob: `{}`".format(oknob))
knobs_settings.append(oknob)
knob_names.append(oknob["name"])
return knobs_settings
def get_imageio_input_colorspace(filename):
''' Get input file colorspace based on regex in settings.
'''
@ -725,15 +840,14 @@ def check_subsetname_exists(nodes, subset_name):
def get_render_path(node):
''' Generate Render path from presets regarding avalon knob data
'''
data = {'avalon': read_avalon_data(node)}
data_preset = {
"nodeclass": data["avalon"]["family"],
"families": [data["avalon"]["families"]],
"creator": data["avalon"]["creator"],
"subset": data["avalon"]["subset"]
}
avalon_knob_data = read_avalon_data(node)
data = {'avalon': avalon_knob_data}
nuke_imageio_writes = get_created_node_imageio_setting(**data_preset)
nuke_imageio_writes = get_imageio_node_setting(
node_class=avalon_knob_data["family"],
plugin_name=avalon_knob_data["creator"],
subset=avalon_knob_data["subset"]
)
host_name = os.environ.get("AVALON_APP")
data.update({
@ -825,8 +939,282 @@ def add_button_clear_rendered(node, path):
node.addKnob(knob)
def create_write_node(name, data, input=None, prenodes=None,
review=True, linked_knobs=None, farm=True):
def create_prenodes(
prev_node,
nodes_setting,
plugin_name=None,
subset=None,
**kwargs
):
last_node = None
for_dependency = {}
for name, node in nodes_setting.items():
# get attributes
nodeclass = node["nodeclass"]
knobs = node["knobs"]
# create node
now_node = nuke.createNode(
nodeclass, "name {}".format(name))
now_node.hideControlPanel()
# add for dependency linking
for_dependency[name] = {
"node": now_node,
"dependent": node["dependent"]
}
if all([plugin_name, subset]):
# find imageio overrides
get_imageio_node_override_setting(
now_node.Class(),
plugin_name,
subset,
knobs
)
# add data to knob
set_node_knobs_from_settings(now_node, knobs, **kwargs)
# switch actual node to previous
last_node = now_node
for _node_name, node_prop in for_dependency.items():
if not node_prop["dependent"]:
node_prop["node"].setInput(
0, prev_node)
elif node_prop["dependent"] in for_dependency:
_prev_node = for_dependency[node_prop["dependent"]]["node"]
node_prop["node"].setInput(
0, _prev_node)
else:
log.warning("Dependency has wrong name of node: {}".format(
node_prop
))
return last_node
def create_write_node(
name,
data,
input=None,
prenodes=None,
review=True,
farm=True,
linked_knobs=None,
**kwargs
):
''' Creating write node which is group node
Arguments:
name (str): name of node
data (dict): creator write instance data
input (node)[optional]: selected node to connect to
prenodes (dict)[optional]:
nodes to be created before write with dependency
review (bool)[optional]: adding review knob
farm (bool)[optional]: rendering workflow target
kwargs (dict)[optional]: additional key arguments for formating
Example:
prenodes = {
"nodeName": {
"nodeclass": "Reformat",
"dependent": [
following_node_01,
...
],
"knobs": [
{
"type": "text",
"name": "knobname",
"value": "knob value"
},
...
]
},
...
}
Return:
node (obj): group node with avalon data as Knobs
'''
prenodes = prenodes or {}
# group node knob overrides
knob_overrides = data.pop("knobs", [])
# filtering variables
plugin_name = data["creator"]
subset = data["subset"]
# get knob settings for write node
imageio_writes = get_imageio_node_setting(
node_class=data["nodeclass"],
plugin_name=plugin_name,
subset=subset
)
for knob in imageio_writes["knobs"]:
if knob["name"] == "file_type":
representation = knob["value"]
host_name = os.environ.get("AVALON_APP")
try:
data.update({
"app": host_name,
"imageio_writes": imageio_writes,
"representation": representation,
})
anatomy_filled = format_anatomy(data)
except Exception as e:
msg = "problem with resolving anatomy template: {}".format(e)
log.error(msg)
nuke.message(msg)
# build file path to workfiles
fdir = str(anatomy_filled["work"]["folder"]).replace("\\", "/")
fpath = data["fpath_template"].format(
work=fdir,
version=data["version"],
subset=data["subset"],
frame=data["frame"],
ext=representation
)
# create directory
if not os.path.isdir(os.path.dirname(fpath)):
log.warning("Path does not exist! I am creating it.")
os.makedirs(os.path.dirname(fpath))
GN = nuke.createNode("Group", "name {}".format(name))
prev_node = None
with GN:
if input:
input_name = str(input.name()).replace(" ", "")
# if connected input node was defined
prev_node = nuke.createNode(
"Input", "name {}".format(input_name))
else:
# generic input node connected to nothing
prev_node = nuke.createNode(
"Input", "name {}".format("rgba"))
prev_node.hideControlPanel()
# creating pre-write nodes `prenodes`
last_prenode = create_prenodes(
prev_node,
prenodes,
plugin_name,
subset,
**kwargs
)
if last_prenode:
prev_node = last_prenode
# creating write node
write_node = now_node = add_write_node(
"inside_{}".format(name),
fpath,
imageio_writes["knobs"],
**data
)
write_node.hideControlPanel()
# connect to previous node
now_node.setInput(0, prev_node)
# switch actual node to previous
prev_node = now_node
now_node = nuke.createNode("Output", "name Output1")
now_node.hideControlPanel()
# connect to previous node
now_node.setInput(0, prev_node)
# imprinting group node
set_avalon_knob_data(GN, data["avalon"])
add_publish_knob(GN)
add_rendering_knobs(GN, farm)
if review:
add_review_knob(GN)
# add divider
GN.addKnob(nuke.Text_Knob('', 'Rendering'))
# Add linked knobs.
linked_knob_names = []
# add input linked knobs and create group only if any input
if linked_knobs:
linked_knob_names.append("_grp-start_")
linked_knob_names.extend(linked_knobs)
linked_knob_names.append("_grp-end_")
linked_knob_names.append("Render")
for _k_name in linked_knob_names:
if "_grp-start_" in _k_name:
knob = nuke.Tab_Knob(
"rnd_attr", "Rendering attributes", nuke.TABBEGINCLOSEDGROUP)
GN.addKnob(knob)
elif "_grp-end_" in _k_name:
knob = nuke.Tab_Knob(
"rnd_attr_end", "Rendering attributes", nuke.TABENDGROUP)
GN.addKnob(knob)
else:
if "___" in _k_name:
# add divider
GN.addKnob(nuke.Text_Knob(""))
else:
# add linked knob by _k_name
link = nuke.Link_Knob("")
link.makeLink(write_node.name(), _k_name)
link.setName(_k_name)
# make render
if "Render" in _k_name:
link.setLabel("Render Local")
link.setFlag(0x1000)
GN.addKnob(link)
# adding write to read button
add_button_write_to_read(GN)
# adding write to read button
add_button_clear_rendered(GN, os.path.dirname(fpath))
# Deadline tab.
add_deadline_tab(GN)
# open the our Tab as default
GN[_NODE_TAB_NAME].setFlag(0)
# set tile color
tile_color = next(
iter(
k["value"] for k in imageio_writes["knobs"]
if "tile_color" in k["name"]
), [255, 0, 0, 255]
)
GN["tile_color"].setValue(
color_gui_to_int(tile_color))
# finally add knob overrides
set_node_knobs_from_settings(GN, knob_overrides, **kwargs)
return GN
def create_write_node_legacy(
name, data, input=None, prenodes=None,
review=True, linked_knobs=None, farm=True
):
''' Creating write node which is group node
Arguments:
@ -858,8 +1246,14 @@ def create_write_node(name, data, input=None, prenodes=None,
Return:
node (obj): group node with avalon data as Knobs
'''
knob_overrides = data.get("knobs", [])
nodeclass = data["nodeclass"]
creator = data["creator"]
subset = data["subset"]
imageio_writes = get_created_node_imageio_setting(**data)
imageio_writes = get_created_node_imageio_setting_legacy(
nodeclass, creator, subset
)
for knob in imageio_writes["knobs"]:
if knob["name"] == "file_type":
representation = knob["value"]
@ -981,7 +1375,8 @@ def create_write_node(name, data, input=None, prenodes=None,
prev_node = now_node
# creating write node
write_node = now_node = add_write_node(
write_node = now_node = add_write_node_legacy(
"inside_{}".format(name),
**_data
)
@ -1061,9 +1456,106 @@ def create_write_node(name, data, input=None, prenodes=None,
tile_color = _data.get("tile_color", "0xff0000ff")
GN["tile_color"].setValue(tile_color)
# overrie knob values from settings
for knob in knob_overrides:
knob_type = knob["type"]
knob_name = knob["name"]
knob_value = knob["value"]
if knob_name not in GN.knobs():
continue
if not knob_value:
continue
# set correctly knob types
if knob_type == "string":
knob_value = str(knob_value)
if knob_type == "number":
knob_value = int(knob_value)
if knob_type == "decimal_number":
knob_value = float(knob_value)
if knob_type == "bool":
knob_value = bool(knob_value)
if knob_type in ["2d_vector", "3d_vector"]:
knob_value = list(knob_value)
GN[knob_name].setValue(knob_value)
return GN
def set_node_knobs_from_settings(node, knob_settings, **kwargs):
""" Overriding knob values from settings
Using `schema_nuke_knob_inputs` for knob type definitions.
Args:
node (nuke.Node): nuke node
knob_settings (list): list of dict. Keys are `type`, `name`, `value`
kwargs (dict)[optional]: keys for formatable knob settings
"""
for knob in knob_settings:
log.debug("__ knob: {}".format(pformat(knob)))
knob_type = knob["type"]
knob_name = knob["name"]
if knob_name not in node.knobs():
continue
# first deal with formatable knob settings
if knob_type == "formatable":
template = knob["template"]
to_type = knob["to_type"]
try:
_knob_value = template.format(
**kwargs
)
log.debug("__ knob_value0: {}".format(_knob_value))
except KeyError as msg:
log.warning("__ msg: {}".format(msg))
raise KeyError(msg)
# convert value to correct type
if to_type == "2d_vector":
knob_value = _knob_value.split(";").split(",")
else:
knob_value = _knob_value
knob_type = to_type
else:
knob_value = knob["value"]
if not knob_value:
continue
# first convert string types to string
# just to ditch unicode
if isinstance(knob_value, six.text_type):
knob_value = str(knob_value)
# set correctly knob types
if knob_type == "bool":
knob_value = bool(knob_value)
elif knob_type == "decimal_number":
knob_value = float(knob_value)
elif knob_type == "number":
knob_value = int(knob_value)
elif knob_type == "text":
knob_value = knob_value
elif knob_type == "color_gui":
knob_value = color_gui_to_int(knob_value)
elif knob_type in ["2d_vector", "3d_vector", "color"]:
knob_value = [float(v) for v in knob_value]
node[knob_name].setValue(knob_value)
def color_gui_to_int(color_gui):
hex_value = (
"0x{0:0>2x}{1:0>2x}{2:0>2x}{3:0>2x}").format(*color_gui)
return int(hex_value, 16)
def add_rendering_knobs(node, farm=True):
''' Adds additional rendering knobs to given node
@ -1364,15 +1856,11 @@ class WorkfileSettings(object):
if avalon_knob_data.get("families"):
families.append(avalon_knob_data.get("families"))
data_preset = {
"nodeclass": avalon_knob_data["family"],
"families": families,
"creator": avalon_knob_data["creator"],
"subset": avalon_knob_data["subset"]
}
nuke_imageio_writes = get_created_node_imageio_setting(
**data_preset)
nuke_imageio_writes = get_imageio_node_setting(
node_class=avalon_knob_data["family"],
plugin_name=avalon_knob_data["creator"],
subset=avalon_knob_data["subset"]
)
log.debug("nuke_imageio_writes: `{}`".format(nuke_imageio_writes))
@ -1687,17 +2175,13 @@ def get_write_node_template_attr(node):
'''
# get avalon data from node
data = {"avalon": read_avalon_data(node)}
data_preset = {
"nodeclass": data["avalon"]["family"],
"families": [data["avalon"]["families"]],
"creator": data["avalon"]["creator"],
"subset": data["avalon"]["subset"]
}
avalon_knob_data = read_avalon_data(node)
# get template data
nuke_imageio_writes = get_created_node_imageio_setting(**data_preset)
nuke_imageio_writes = get_imageio_node_setting(
node_class=avalon_knob_data["family"],
plugin_name=avalon_knob_data["creator"],
subset=avalon_knob_data["subset"]
)
# collecting correct data
correct_data = OrderedDict({

View file

@ -17,7 +17,8 @@ from .lib import (
reset_selection,
maintained_selection,
set_avalon_knob_data,
add_publish_knob
add_publish_knob,
get_nuke_imageio_settings
)
@ -27,9 +28,6 @@ class OpenPypeCreator(LegacyCreator):
def __init__(self, *args, **kwargs):
super(OpenPypeCreator, self).__init__(*args, **kwargs)
self.presets = get_current_project_settings()["nuke"]["create"].get(
self.__class__.__name__, {}
)
if check_subsetname_exists(
nuke.allNodes(),
self.data["subset"]):
@ -605,6 +603,8 @@ class AbstractWriteRender(OpenPypeCreator):
family = "render"
icon = "sign-out"
defaults = ["Main", "Mask"]
knobs = []
prenodes = {}
def __init__(self, *args, **kwargs):
super(AbstractWriteRender, self).__init__(*args, **kwargs)
@ -672,7 +672,8 @@ class AbstractWriteRender(OpenPypeCreator):
"nodeclass": self.n_class,
"families": [self.family],
"avalon": self.data,
"subset": self.data["subset"]
"subset": self.data["subset"],
"knobs": self.knobs
}
# add creator data
@ -680,21 +681,12 @@ class AbstractWriteRender(OpenPypeCreator):
self.data.update(creator_data)
write_data.update(creator_data)
if self.presets.get('fpath_template'):
self.log.info("Adding template path from preset")
write_data.update(
{"fpath_template": self.presets["fpath_template"]}
)
else:
self.log.info("Adding template path from plugin")
write_data.update({
"fpath_template":
("{work}/" + self.family + "s/nuke/{subset}"
"/{subset}.{frame}.{ext}")})
write_node = self._create_write_node(selected_node,
inputs, outputs,
write_data)
write_node = self._create_write_node(
selected_node,
inputs,
outputs,
write_data
)
# relinking to collected connections
for i, input in enumerate(inputs):
@ -709,6 +701,28 @@ class AbstractWriteRender(OpenPypeCreator):
return write_node
def is_legacy(self):
"""Check if it needs to run legacy code
In case where `type` key is missing in singe
knob it is legacy project anatomy.
Returns:
bool: True if legacy
"""
imageio_nodes = get_nuke_imageio_settings()["nodes"]
node = imageio_nodes["requiredNodes"][0]
if "type" not in node["knobs"][0]:
# if type is not yet in project anatomy
return True
elif next(iter(
_k for _k in node["knobs"]
if _k.get("type") == "__legacy__"
), None):
# in case someone re-saved anatomy
# with old configuration
return True
@abstractmethod
def _create_write_node(self, selected_node, inputs, outputs, write_data):
"""Family dependent implementation of Write node creation

View file

@ -1,7 +1,8 @@
import nuke
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import create_write_node
from openpype.hosts.nuke.api.lib import (
create_write_node, create_write_node_legacy)
class CreateWritePrerender(plugin.AbstractWriteRender):
@ -12,22 +13,37 @@ class CreateWritePrerender(plugin.AbstractWriteRender):
n_class = "Write"
family = "prerender"
icon = "sign-out"
# settings
fpath_template = "{work}/render/nuke/{subset}/{subset}.{frame}.{ext}"
defaults = ["Key01", "Bg01", "Fg01", "Branch01", "Part01"]
reviewable = False
use_range_limit = True
def __init__(self, *args, **kwargs):
super(CreateWritePrerender, self).__init__(*args, **kwargs)
def _create_write_node(self, selected_node, inputs, outputs, write_data):
reviewable = self.presets.get("reviewable")
write_node = create_write_node(
self.data["subset"],
write_data,
input=selected_node,
prenodes=[],
review=reviewable,
linked_knobs=["channels", "___", "first", "last", "use_limit"])
# add fpath_template
write_data["fpath_template"] = self.fpath_template
write_data["use_range_limit"] = self.use_range_limit
return write_node
if not self.is_legacy():
return create_write_node(
self.data["subset"],
write_data,
input=selected_node,
review=self.reviewable,
linked_knobs=["channels", "___", "first", "last", "use_limit"]
)
else:
return create_write_node_legacy(
self.data["subset"],
write_data,
input=selected_node,
review=self.reviewable,
linked_knobs=["channels", "___", "first", "last", "use_limit"]
)
def _modify_write_node(self, write_node):
# open group node
@ -38,7 +54,7 @@ class CreateWritePrerender(plugin.AbstractWriteRender):
w_node = n
write_node.end()
if self.presets.get("use_range_limit"):
if self.use_range_limit:
w_node["use_limit"].setValue(True)
w_node["first"].setValue(nuke.root()["first_frame"].value())
w_node["last"].setValue(nuke.root()["last_frame"].value())

View file

@ -1,7 +1,8 @@
import nuke
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import create_write_node
from openpype.hosts.nuke.api.lib import (
create_write_node, create_write_node_legacy)
class CreateWriteRender(plugin.AbstractWriteRender):
@ -12,12 +13,36 @@ class CreateWriteRender(plugin.AbstractWriteRender):
n_class = "Write"
family = "render"
icon = "sign-out"
# settings
fpath_template = "{work}/render/nuke/{subset}/{subset}.{frame}.{ext}"
defaults = ["Main", "Mask"]
prenodes = {
"Reformat01": {
"nodeclass": "Reformat",
"dependent": None,
"knobs": [
{
"type": "text",
"name": "resize",
"value": "none"
},
{
"type": "bool",
"name": "black_outside",
"value": True
}
]
}
}
def __init__(self, *args, **kwargs):
super(CreateWriteRender, self).__init__(*args, **kwargs)
def _create_write_node(self, selected_node, inputs, outputs, write_data):
# add fpath_template
write_data["fpath_template"] = self.fpath_template
# add reformat node to cut off all outside of format bounding box
# get width and height
try:
@ -26,25 +51,36 @@ class CreateWriteRender(plugin.AbstractWriteRender):
actual_format = nuke.root().knob('format').value()
width, height = (actual_format.width(), actual_format.height())
_prenodes = [
{
"name": "Reformat01",
"class": "Reformat",
"knobs": [
("resize", 0),
("black_outside", 1),
],
"dependent": None
}
]
if not self.is_legacy():
return create_write_node(
self.data["subset"],
write_data,
input=selected_node,
prenodes=self.prenodes,
**{
"width": width,
"height": height
}
)
else:
_prenodes = [
{
"name": "Reformat01",
"class": "Reformat",
"knobs": [
("resize", 0),
("black_outside", 1),
],
"dependent": None
}
]
write_node = create_write_node(
self.data["subset"],
write_data,
input=selected_node,
prenodes=_prenodes)
return write_node
return create_write_node_legacy(
self.data["subset"],
write_data,
input=selected_node,
prenodes=_prenodes
)
def _modify_write_node(self, write_node):
return write_node

View file

@ -1,7 +1,8 @@
import nuke
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import create_write_node
from openpype.hosts.nuke.api.lib import (
create_write_node, create_write_node_legacy)
class CreateWriteStill(plugin.AbstractWriteRender):
@ -12,42 +13,69 @@ class CreateWriteStill(plugin.AbstractWriteRender):
n_class = "Write"
family = "still"
icon = "image"
# settings
fpath_template = "{work}/render/nuke/{subset}/{subset}.{ext}"
defaults = [
"ImageFrame{:0>4}".format(nuke.frame()),
"MPFrame{:0>4}".format(nuke.frame()),
"LayoutFrame{:0>4}".format(nuke.frame())
"ImageFrame",
"MPFrame",
"LayoutFrame"
]
prenodes = {
"FrameHold01": {
"nodeclass": "FrameHold",
"dependent": None,
"knobs": [
{
"type": "formatable",
"name": "first_frame",
"template": "{frame}",
"to_type": "number"
}
]
}
}
def __init__(self, *args, **kwargs):
super(CreateWriteStill, self).__init__(*args, **kwargs)
def _create_write_node(self, selected_node, inputs, outputs, write_data):
# explicitly reset template to 'renders', not same as other 2 writes
write_data.update({
"fpath_template": (
"{work}/renders/nuke/{subset}/{subset}.{ext}")})
# add fpath_template
write_data["fpath_template"] = self.fpath_template
_prenodes = [
{
"name": "FrameHold01",
"class": "FrameHold",
"knobs": [
("first_frame", nuke.frame())
],
"dependent": None
}
]
write_node = create_write_node(
self.name,
write_data,
input=selected_node,
review=False,
prenodes=_prenodes,
farm=False,
linked_knobs=["channels", "___", "first", "last", "use_limit"])
return write_node
if not self.is_legacy():
return create_write_node(
self.name,
write_data,
input=selected_node,
review=False,
prenodes=self.prenodes,
farm=False,
linked_knobs=["channels", "___", "first", "last", "use_limit"],
**{
"frame": nuke.frame()
}
)
else:
_prenodes = [
{
"name": "FrameHold01",
"class": "FrameHold",
"knobs": [
("first_frame", nuke.frame())
],
"dependent": None
}
]
return create_write_node_legacy(
self.name,
write_data,
input=selected_node,
review=False,
prenodes=_prenodes,
farm=False,
linked_knobs=["channels", "___", "first", "last", "use_limit"]
)
def _modify_write_node(self, write_node):
write_node.begin()

View file

@ -9,7 +9,7 @@ log = Logger().get_logger(__name__)
class SetFrameRangeLoader(load.LoaderPlugin):
"""Specific loader of Alembic for the avalon.animation family"""
"""Set frame range excluding pre- and post-handles"""
families = ["animation",
"camera",
@ -43,7 +43,7 @@ class SetFrameRangeLoader(load.LoaderPlugin):
class SetFrameRangeWithHandlesLoader(load.LoaderPlugin):
"""Specific loader of Alembic for the avalon.animation family"""
"""Set frame range including pre- and post-handles"""
families = ["animation",
"camera",

View file

@ -29,6 +29,16 @@ class PSItem(object):
color_code = attr.ib(default=None) # color code of layer
instance_id = attr.ib(default=None)
@property
def clean_name(self):
"""Returns layer name without publish icon highlight
Returns:
(str)
"""
return (self.name.replace(PhotoshopServerStub.PUBLISH_ICON, '')
.replace(PhotoshopServerStub.LOADED_ICON, ''))
class PhotoshopServerStub:
"""

View file

@ -5,6 +5,7 @@ import pyblish.api
from openpype.lib import prepare_template_data
from openpype.hosts.photoshop import api as photoshop
from openpype.settings import get_project_settings
class CollectColorCodedInstances(pyblish.api.ContextPlugin):
@ -49,6 +50,12 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin):
asset_name = context.data["asset"]
task_name = context.data["task"]
variant = context.data["variant"]
project_name = context.data["projectEntity"]["name"]
naming_conventions = get_project_settings(project_name).get(
"photoshop", {}).get(
"publish", {}).get(
"ValidateNaming", {})
stub = photoshop.stub()
layers = stub.get_layers()
@ -83,6 +90,9 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin):
subset = resolved_subset_template.format(
**prepare_template_data(fill_pairs))
subset = self._clean_subset_name(stub, naming_conventions,
subset, layer)
if subset in existing_subset_names:
self.log.info(
"Subset {} already created, skipping.".format(subset))
@ -141,6 +151,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin):
instance.data["task"] = task_name
instance.data["subset"] = subset
instance.data["layer"] = layer
instance.data["families"] = []
return instance
@ -186,3 +197,21 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin):
self.log.debug("resolved_subset_template {}".format(
resolved_subset_template))
return family, resolved_subset_template
def _clean_subset_name(self, stub, naming_conventions, subset, layer):
"""Cleans invalid characters from subset name and layer name."""
if re.search(naming_conventions["invalid_chars"], subset):
subset = re.sub(
naming_conventions["invalid_chars"],
naming_conventions["replace_char"],
subset
)
layer_name = re.sub(
naming_conventions["invalid_chars"],
naming_conventions["replace_char"],
layer.clean_name
)
layer.name = layer_name
stub.rename_layer(layer.id, layer_name)
return subset

View file

@ -42,7 +42,8 @@ class ValidateNamingRepair(pyblish.api.Action):
layer_name = re.sub(invalid_chars,
replace_char,
current_layer_state.name)
current_layer_state.clean_name)
layer_name = stub.PUBLISH_ICON + layer_name
stub.rename_layer(current_layer_state.id, layer_name)
@ -73,13 +74,17 @@ class ValidateNaming(pyblish.api.InstancePlugin):
def process(self, instance):
help_msg = ' Use Repair action (A) in Pyblish to fix it.'
msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"],
help_msg)
formatting_data = {"msg": msg}
if re.search(self.invalid_chars, instance.data["name"]):
raise PublishXmlValidationError(self, msg,
formatting_data=formatting_data)
layer = instance.data.get("layer")
if layer:
msg = "Name \"{}\" is not allowed.{}".format(layer.clean_name,
help_msg)
formatting_data = {"msg": msg}
if re.search(self.invalid_chars, layer.clean_name):
raise PublishXmlValidationError(self, msg,
formatting_data=formatting_data
)
msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"],
help_msg)

View file

@ -1,70 +0,0 @@
import copy
import pyblish.api
from pprint import pformat
class CollectBatchInstances(pyblish.api.InstancePlugin):
"""Collect all available instances for batch publish."""
label = "Collect Batch Instances"
order = pyblish.api.CollectorOrder + 0.489
hosts = ["standalonepublisher"]
families = ["background_batch"]
# presets
default_subset_task = {
"background_batch": "background"
}
subsets = {
"background_batch": {
"backgroundLayout": {
"task": "background",
"family": "backgroundLayout"
},
"backgroundComp": {
"task": "background",
"family": "backgroundComp"
},
"workfileBackground": {
"task": "background",
"family": "workfile"
}
}
}
unchecked_by_default = []
def process(self, instance):
context = instance.context
asset_name = instance.data["asset"]
family = instance.data["family"]
default_task_name = self.default_subset_task.get(family)
for subset_name, subset_data in self.subsets[family].items():
instance_name = f"{asset_name}_{subset_name}"
task_name = subset_data.get("task") or default_task_name
# create new instance
new_instance = context.create_instance(instance_name)
# add original instance data except name key
for key, value in instance.data.items():
if key not in ["name"]:
# Make sure value is copy since value may be object which
# can be shared across all new created objects
new_instance.data[key] = copy.deepcopy(value)
# add subset data from preset
new_instance.data.update(subset_data)
new_instance.data["label"] = instance_name
new_instance.data["subset"] = subset_name
new_instance.data["task"] = task_name
if subset_name in self.unchecked_by_default:
new_instance.data["publish"] = False
self.log.info(f"Created new instance: {instance_name}")
self.log.debug(f"_ inst_data: {pformat(new_instance.data)}")
# delete original instance
context.remove(instance)

View file

@ -1,243 +0,0 @@
import os
import json
import copy
import openpype.api
from openpype.pipeline import legacy_io
PSDImage = None
class ExtractBGForComp(openpype.api.Extractor):
label = "Extract Background for Compositing"
families = ["backgroundComp"]
hosts = ["standalonepublisher"]
new_instance_family = "background"
# Presetable
allowed_group_names = [
"OL", "BG", "MG", "FG", "SB", "UL", "SKY", "Field Guide", "Field_Guide",
"ANIM"
]
def process(self, instance):
# Check if python module `psd_tools` is installed
try:
global PSDImage
from psd_tools import PSDImage
except Exception:
raise AssertionError(
"BUG: Python module `psd-tools` is not installed!"
)
self.allowed_group_names = [
name.lower()
for name in self.allowed_group_names
]
self.redo_global_plugins(instance)
repres = instance.data.get("representations")
if not repres:
self.log.info("There are no representations on instance.")
return
if not instance.data.get("transfers"):
instance.data["transfers"] = []
# Prepare staging dir
staging_dir = self.staging_dir(instance)
if not os.path.exists(staging_dir):
os.makedirs(staging_dir)
for repre in tuple(repres):
# Skip all files without .psd extension
repre_ext = repre["ext"].lower()
if repre_ext.startswith("."):
repre_ext = repre_ext[1:]
if repre_ext != "psd":
continue
# Prepare publish dir for transfers
publish_dir = instance.data["publishDir"]
# Prepare json filepath where extracted metadata are stored
json_filename = "{}.json".format(instance.name)
json_full_path = os.path.join(staging_dir, json_filename)
self.log.debug(f"`staging_dir` is \"{staging_dir}\"")
# Prepare new repre data
new_repre = {
"name": "json",
"ext": "json",
"files": json_filename,
"stagingDir": staging_dir
}
# TODO add check of list
psd_filename = repre["files"]
psd_folder_path = repre["stagingDir"]
psd_filepath = os.path.join(psd_folder_path, psd_filename)
self.log.debug(f"psd_filepath: \"{psd_filepath}\"")
psd_object = PSDImage.open(psd_filepath)
json_data, transfers = self.export_compositing_images(
psd_object, staging_dir, publish_dir
)
self.log.info("Json file path: {}".format(json_full_path))
with open(json_full_path, "w") as json_filestream:
json.dump(json_data, json_filestream, indent=4)
instance.data["transfers"].extend(transfers)
instance.data["representations"].remove(repre)
instance.data["representations"].append(new_repre)
def export_compositing_images(self, psd_object, output_dir, publish_dir):
json_data = {
"__schema_version__": 1,
"children": []
}
transfers = []
for main_idx, main_layer in enumerate(psd_object):
if (
not main_layer.is_visible()
or main_layer.name.lower() not in self.allowed_group_names
or not main_layer.is_group
):
continue
export_layers = []
layers_idx = 0
for layer in main_layer:
# TODO this way may be added also layers next to "ADJ"
if layer.name.lower() == "adj":
for _layer in layer:
export_layers.append((layers_idx, _layer))
layers_idx += 1
else:
export_layers.append((layers_idx, layer))
layers_idx += 1
if not export_layers:
continue
main_layer_data = {
"index": main_idx,
"name": main_layer.name,
"children": []
}
for layer_idx, layer in export_layers:
has_size = layer.width > 0 and layer.height > 0
if not has_size:
self.log.debug((
"Skipping layer \"{}\" because does "
"not have any content."
).format(layer.name))
continue
main_layer_name = main_layer.name.replace(" ", "_")
layer_name = layer.name.replace(" ", "_")
filename = "{:0>2}_{}_{:0>2}_{}.png".format(
main_idx + 1, main_layer_name, layer_idx + 1, layer_name
)
layer_data = {
"index": layer_idx,
"name": layer.name,
"filename": filename
}
output_filepath = os.path.join(output_dir, filename)
dst_filepath = os.path.join(publish_dir, filename)
transfers.append((output_filepath, dst_filepath))
pil_object = layer.composite(viewport=psd_object.viewbox)
pil_object.save(output_filepath, "PNG")
main_layer_data["children"].append(layer_data)
if main_layer_data["children"]:
json_data["children"].append(main_layer_data)
return json_data, transfers
def redo_global_plugins(self, instance):
# TODO do this in collection phase
# Copy `families` and check if `family` is not in current families
families = instance.data.get("families") or list()
if families:
families = list(set(families))
if self.new_instance_family in families:
families.remove(self.new_instance_family)
self.log.debug(
"Setting new instance families {}".format(str(families))
)
instance.data["families"] = families
# Override instance data with new information
instance.data["family"] = self.new_instance_family
subset_name = instance.data["anatomyData"]["subset"]
asset_doc = instance.data["assetEntity"]
latest_version = self.find_last_version(subset_name, asset_doc)
version_number = 1
if latest_version is not None:
version_number += latest_version
instance.data["latestVersion"] = latest_version
instance.data["version"] = version_number
# Same data apply to anatomy data
instance.data["anatomyData"].update({
"family": self.new_instance_family,
"version": version_number
})
# Redo publish and resources dir
anatomy = instance.context.data["anatomy"]
template_data = copy.deepcopy(instance.data["anatomyData"])
template_data.update({
"frame": "FRAME_TEMP",
"representation": "TEMP"
})
anatomy_filled = anatomy.format(template_data)
if "folder" in anatomy.templates["publish"]:
publish_folder = anatomy_filled["publish"]["folder"]
else:
publish_folder = os.path.dirname(anatomy_filled["publish"]["path"])
publish_folder = os.path.normpath(publish_folder)
resources_folder = os.path.join(publish_folder, "resources")
instance.data["publishDir"] = publish_folder
instance.data["resourcesDir"] = resources_folder
self.log.debug("publishDir: \"{}\"".format(publish_folder))
self.log.debug("resourcesDir: \"{}\"".format(resources_folder))
def find_last_version(self, subset_name, asset_doc):
subset_doc = legacy_io.find_one({
"type": "subset",
"name": subset_name,
"parent": asset_doc["_id"]
})
if subset_doc is None:
self.log.debug("Subset entity does not exist yet.")
else:
version_doc = legacy_io.find_one(
{
"type": "version",
"parent": subset_doc["_id"]
},
sort=[("name", -1)]
)
if version_doc:
return int(version_doc["name"])
return None

View file

@ -1,248 +0,0 @@
import os
import copy
import json
import pyblish.api
import openpype.api
from openpype.pipeline import legacy_io
PSDImage = None
class ExtractBGMainGroups(openpype.api.Extractor):
label = "Extract Background Layout"
order = pyblish.api.ExtractorOrder + 0.02
families = ["backgroundLayout"]
hosts = ["standalonepublisher"]
new_instance_family = "background"
# Presetable
allowed_group_names = [
"OL", "BG", "MG", "FG", "UL", "SB", "SKY", "Field Guide", "Field_Guide",
"ANIM"
]
def process(self, instance):
# Check if python module `psd_tools` is installed
try:
global PSDImage
from psd_tools import PSDImage
except Exception:
raise AssertionError(
"BUG: Python module `psd-tools` is not installed!"
)
self.allowed_group_names = [
name.lower()
for name in self.allowed_group_names
]
repres = instance.data.get("representations")
if not repres:
self.log.info("There are no representations on instance.")
return
self.redo_global_plugins(instance)
repres = instance.data.get("representations")
if not repres:
self.log.info("There are no representations on instance.")
return
if not instance.data.get("transfers"):
instance.data["transfers"] = []
# Prepare staging dir
staging_dir = self.staging_dir(instance)
if not os.path.exists(staging_dir):
os.makedirs(staging_dir)
# Prepare publish dir for transfers
publish_dir = instance.data["publishDir"]
for repre in tuple(repres):
# Skip all files without .psd extension
repre_ext = repre["ext"].lower()
if repre_ext.startswith("."):
repre_ext = repre_ext[1:]
if repre_ext != "psd":
continue
# Prepare json filepath where extracted metadata are stored
json_filename = "{}.json".format(instance.name)
json_full_path = os.path.join(staging_dir, json_filename)
self.log.debug(f"`staging_dir` is \"{staging_dir}\"")
# Prepare new repre data
new_repre = {
"name": "json",
"ext": "json",
"files": json_filename,
"stagingDir": staging_dir
}
# TODO add check of list
psd_filename = repre["files"]
psd_folder_path = repre["stagingDir"]
psd_filepath = os.path.join(psd_folder_path, psd_filename)
self.log.debug(f"psd_filepath: \"{psd_filepath}\"")
psd_object = PSDImage.open(psd_filepath)
json_data, transfers = self.export_compositing_images(
psd_object, staging_dir, publish_dir
)
self.log.info("Json file path: {}".format(json_full_path))
with open(json_full_path, "w") as json_filestream:
json.dump(json_data, json_filestream, indent=4)
instance.data["transfers"].extend(transfers)
instance.data["representations"].remove(repre)
instance.data["representations"].append(new_repre)
def export_compositing_images(self, psd_object, output_dir, publish_dir):
json_data = {
"__schema_version__": 1,
"children": []
}
output_ext = ".png"
to_export = []
for layer_idx, layer in enumerate(psd_object):
layer_name = layer.name.replace(" ", "_")
if (
not layer.is_visible()
or layer_name.lower() not in self.allowed_group_names
):
continue
has_size = layer.width > 0 and layer.height > 0
if not has_size:
self.log.debug((
"Skipping layer \"{}\" because does not have any content."
).format(layer.name))
continue
filebase = "{:0>2}_{}".format(layer_idx, layer_name)
if layer_name.lower() == "anim":
if not layer.is_group:
self.log.warning("ANIM layer is not a group layer.")
continue
children = []
for anim_idx, anim_layer in enumerate(layer):
anim_layer_name = anim_layer.name.replace(" ", "_")
filename = "{}_{:0>2}_{}{}".format(
filebase, anim_idx, anim_layer_name, output_ext
)
children.append({
"index": anim_idx,
"name": anim_layer.name,
"filename": filename
})
to_export.append((anim_layer, filename))
json_data["children"].append({
"index": layer_idx,
"name": layer.name,
"children": children
})
continue
filename = filebase + output_ext
json_data["children"].append({
"index": layer_idx,
"name": layer.name,
"filename": filename
})
to_export.append((layer, filename))
transfers = []
for layer, filename in to_export:
output_filepath = os.path.join(output_dir, filename)
dst_filepath = os.path.join(publish_dir, filename)
transfers.append((output_filepath, dst_filepath))
pil_object = layer.composite(viewport=psd_object.viewbox)
pil_object.save(output_filepath, "PNG")
return json_data, transfers
def redo_global_plugins(self, instance):
# TODO do this in collection phase
# Copy `families` and check if `family` is not in current families
families = instance.data.get("families") or list()
if families:
families = list(set(families))
if self.new_instance_family in families:
families.remove(self.new_instance_family)
self.log.debug(
"Setting new instance families {}".format(str(families))
)
instance.data["families"] = families
# Override instance data with new information
instance.data["family"] = self.new_instance_family
subset_name = instance.data["anatomyData"]["subset"]
asset_doc = instance.data["assetEntity"]
latest_version = self.find_last_version(subset_name, asset_doc)
version_number = 1
if latest_version is not None:
version_number += latest_version
instance.data["latestVersion"] = latest_version
instance.data["version"] = version_number
# Same data apply to anatomy data
instance.data["anatomyData"].update({
"family": self.new_instance_family,
"version": version_number
})
# Redo publish and resources dir
anatomy = instance.context.data["anatomy"]
template_data = copy.deepcopy(instance.data["anatomyData"])
template_data.update({
"frame": "FRAME_TEMP",
"representation": "TEMP"
})
anatomy_filled = anatomy.format(template_data)
if "folder" in anatomy.templates["publish"]:
publish_folder = anatomy_filled["publish"]["folder"]
else:
publish_folder = os.path.dirname(anatomy_filled["publish"]["path"])
publish_folder = os.path.normpath(publish_folder)
resources_folder = os.path.join(publish_folder, "resources")
instance.data["publishDir"] = publish_folder
instance.data["resourcesDir"] = resources_folder
self.log.debug("publishDir: \"{}\"".format(publish_folder))
self.log.debug("resourcesDir: \"{}\"".format(resources_folder))
def find_last_version(self, subset_name, asset_doc):
subset_doc = legacy_io.find_one({
"type": "subset",
"name": subset_name,
"parent": asset_doc["_id"]
})
if subset_doc is None:
self.log.debug("Subset entity does not exist yet.")
else:
version_doc = legacy_io.find_one(
{
"type": "version",
"parent": subset_doc["_id"]
},
sort=[("name", -1)]
)
if version_doc:
return int(version_doc["name"])
return None

View file

@ -1,171 +0,0 @@
import os
import copy
import pyblish.api
import openpype.api
from openpype.pipeline import legacy_io
PSDImage = None
class ExtractImagesFromPSD(openpype.api.Extractor):
# PLUGIN is not currently enabled because was decided to use different
# approach
enabled = False
active = False
label = "Extract Images from PSD"
order = pyblish.api.ExtractorOrder + 0.02
families = ["backgroundLayout"]
hosts = ["standalonepublisher"]
new_instance_family = "image"
ignored_instance_data_keys = ("name", "label", "stagingDir", "version")
# Presetable
allowed_group_names = [
"OL", "BG", "MG", "FG", "UL", "SKY", "Field Guide", "Field_Guide",
"ANIM"
]
def process(self, instance):
# Check if python module `psd_tools` is installed
try:
global PSDImage
from psd_tools import PSDImage
except Exception:
raise AssertionError(
"BUG: Python module `psd-tools` is not installed!"
)
self.allowed_group_names = [
name.lower()
for name in self.allowed_group_names
]
repres = instance.data.get("representations")
if not repres:
self.log.info("There are no representations on instance.")
return
for repre in tuple(repres):
# Skip all files without .psd extension
repre_ext = repre["ext"].lower()
if repre_ext.startswith("."):
repre_ext = repre_ext[1:]
if repre_ext != "psd":
continue
# TODO add check of list of "files" value
psd_filename = repre["files"]
psd_folder_path = repre["stagingDir"]
psd_filepath = os.path.join(psd_folder_path, psd_filename)
self.log.debug(f"psd_filepath: \"{psd_filepath}\"")
psd_object = PSDImage.open(psd_filepath)
self.create_new_instances(instance, psd_object)
# Remove the instance from context
instance.context.remove(instance)
def create_new_instances(self, instance, psd_object):
asset_doc = instance.data["assetEntity"]
for layer in psd_object:
if (
not layer.is_visible()
or layer.name.lower() not in self.allowed_group_names
):
continue
has_size = layer.width > 0 and layer.height > 0
if not has_size:
self.log.debug((
"Skipping layer \"{}\" because does "
"not have any content."
).format(layer.name))
continue
layer_name = layer.name.replace(" ", "_")
instance_name = subset_name = f"image{layer_name}"
self.log.info(
f"Creating new instance with name \"{instance_name}\""
)
new_instance = instance.context.create_instance(instance_name)
for key, value in instance.data.items():
if key not in self.ignored_instance_data_keys:
new_instance.data[key] = copy.deepcopy(value)
new_instance.data["label"] = " ".join(
(new_instance.data["asset"], instance_name)
)
# Find latest version
latest_version = self.find_last_version(subset_name, asset_doc)
version_number = 1
if latest_version is not None:
version_number += latest_version
self.log.info(
"Next version of instance \"{}\" will be {}".format(
instance_name, version_number
)
)
# Set family and subset
new_instance.data["family"] = self.new_instance_family
new_instance.data["subset"] = subset_name
new_instance.data["version"] = version_number
new_instance.data["latestVersion"] = latest_version
new_instance.data["anatomyData"].update({
"subset": subset_name,
"family": self.new_instance_family,
"version": version_number
})
# Copy `families` and check if `family` is not in current families
families = new_instance.data.get("families") or list()
if families:
families = list(set(families))
if self.new_instance_family in families:
families.remove(self.new_instance_family)
new_instance.data["families"] = families
# Prepare staging dir for new instance
staging_dir = self.staging_dir(new_instance)
output_filename = "{}.png".format(layer_name)
output_filepath = os.path.join(staging_dir, output_filename)
pil_object = layer.composite(viewport=psd_object.viewbox)
pil_object.save(output_filepath, "PNG")
new_repre = {
"name": "png",
"ext": "png",
"files": output_filename,
"stagingDir": staging_dir
}
self.log.debug(
"Creating new representation: {}".format(new_repre)
)
new_instance.data["representations"] = [new_repre]
def find_last_version(self, subset_name, asset_doc):
subset_doc = legacy_io.find_one({
"type": "subset",
"name": subset_name,
"parent": asset_doc["_id"]
})
if subset_doc is None:
self.log.debug("Subset entity does not exist yet.")
else:
version_doc = legacy_io.find_one(
{
"type": "version",
"parent": subset_doc["_id"]
},
sort=[("name", -1)]
)
if version_doc:
return int(version_doc["name"])
return None

View file

@ -2,7 +2,10 @@ import os
import tempfile
import pyblish.api
import openpype.api
import openpype.lib
from openpype.lib import (
get_ffmpeg_tool_path,
get_ffprobe_streams,
)
class ExtractThumbnailSP(pyblish.api.InstancePlugin):
@ -71,7 +74,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1]
self.log.info("output {}".format(full_thumbnail_path))
ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg")
ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
ffmpeg_args = self.ffmpeg_args or {}
@ -110,6 +113,13 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
# remove thumbnail key from origin repre
thumbnail_repre.pop("thumbnail")
streams = get_ffprobe_streams(full_thumbnail_path)
width = height = None
for stream in streams:
if "width" in stream and "height" in stream:
width = stream["width"]
height = stream["height"]
break
filename = os.path.basename(full_thumbnail_path)
staging_dir = staging_dir or os.path.dirname(full_thumbnail_path)
@ -122,6 +132,9 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
"stagingDir": staging_dir,
"tags": ["thumbnail"],
}
if width and height:
representation["width"] = width
representation["height"] = height
# # add Delete tag when temp file was rendered
if not is_jpeg:

View file

@ -2,10 +2,7 @@ from openpype.pipeline import (
Creator,
CreatedInstance
)
from openpype.lib import (
FileDef,
BoolDef,
)
from openpype.lib import FileDef
from .pipeline import (
list_instances,
@ -43,7 +40,6 @@ class TrayPublishCreator(Creator):
class SettingsCreator(TrayPublishCreator):
create_allow_context_change = True
enable_review = False
extensions = []
def collect_instances(self):
@ -67,19 +63,15 @@ class SettingsCreator(TrayPublishCreator):
self._add_instance_to_context(new_instance)
def get_instance_attr_defs(self):
output = []
file_def = FileDef(
"filepath",
folders=False,
extensions=self.extensions,
allow_sequences=self.allow_sequences,
label="Filepath",
)
output.append(file_def)
if self.enable_review:
output.append(BoolDef("review", label="Review"))
return output
return [
FileDef(
"filepath",
folders=False,
extensions=self.extensions,
allow_sequences=self.allow_sequences,
label="Filepath",
)
]
@classmethod
def from_settings(cls, item_data):
@ -97,7 +89,6 @@ class SettingsCreator(TrayPublishCreator):
"icon": item_data["icon"],
"description": item_data["description"],
"detailed_description": item_data["detailed_description"],
"enable_review": item_data["enable_review"],
"extensions": item_data["extensions"],
"allow_sequences": item_data["allow_sequences"],
"default_variants": item_data["default_variants"]

View file

@ -0,0 +1,31 @@
import pyblish.api
from openpype.lib import BoolDef
from openpype.pipeline import OpenPypePyblishPluginMixin
class CollectReviewFamily(
pyblish.api.InstancePlugin, OpenPypePyblishPluginMixin
):
"""Add review family."""
label = "Collect Review Family"
order = pyblish.api.CollectorOrder - 0.49
hosts = ["traypublisher"]
families = [
"image",
"render",
"plate",
"review"
]
def process(self, instance):
values = self.get_attr_values_from_data(instance.data)
if values.get("add_review_family"):
instance.data["families"].append("review")
@classmethod
def get_attribute_defs(cls):
return [
BoolDef("add_review_family", label="Review", default=True)
]

View file

@ -22,10 +22,6 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin):
repres = instance.data["representations"]
creator_attributes = instance.data["creator_attributes"]
if creator_attributes.get("review"):
instance.data["families"].append("review")
filepath_item = creator_attributes["filepath"]
self.log.info(filepath_item)
filepaths = [
@ -34,9 +30,11 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin):
]
instance.data["sourceFilepaths"] = filepaths
instance.data["stagingDir"] = filepath_item["directory"]
filenames = filepath_item["filenames"]
ext = os.path.splitext(filenames[0])[-1]
_, ext = os.path.splitext(filenames[0])
ext = ext[1:]
if len(filenames) == 1:
filenames = filenames[0]
@ -46,3 +44,7 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin):
"stagingDir": filepath_item["directory"],
"files": filenames
})
self.log.debug("Created Simple Settings instance {}".format(
instance.data
))

View file

@ -573,7 +573,7 @@ def composite_rendered_layers(
layer_ids_by_position[layer_position] = layer["layer_id"]
# Sort layer positions
sorted_positions = tuple(sorted(layer_ids_by_position.keys()))
sorted_positions = tuple(reversed(sorted(layer_ids_by_position.keys())))
# Prepare variable where filepaths without any rendered content
# - transparent will be created
transparent_filepaths = set()

View file

@ -1012,8 +1012,8 @@ class ApplicationLaunchContext:
self.log.debug("Discovery of launch hooks started.")
paths = self.paths_to_launch_hooks()
self.log.debug("Paths where will look for launch hooks:{}".format(
"\n- ".join(paths)
self.log.debug("Paths searched for launch hooks:\n{}".format(
"\n".join("- {}".format(path) for path in paths)
))
all_classes = {
@ -1023,7 +1023,7 @@ class ApplicationLaunchContext:
for path in paths:
if not os.path.exists(path):
self.log.info(
"Path to launch hooks does not exists: \"{}\"".format(path)
"Path to launch hooks does not exist: \"{}\"".format(path)
)
continue
@ -1044,13 +1044,14 @@ class ApplicationLaunchContext:
hook = klass(self)
if not hook.is_valid:
self.log.debug(
"Hook is not valid for current launch context."
"Skipped hook invalid for current launch context: "
"{}".format(klass.__name__)
)
continue
if inspect.isabstract(hook):
self.log.debug("Skipped abstract hook: {}".format(
str(hook)
klass.__name__
))
continue
@ -1062,7 +1063,8 @@ class ApplicationLaunchContext:
except Exception:
self.log.warning(
"Initialization of hook failed. {}".format(str(klass)),
"Initialization of hook failed: "
"{}".format(klass.__name__),
exc_info=True
)

View file

@ -316,6 +316,7 @@ class FileDefItem(object):
self.is_sequence = False
self.template = None
self.frames = []
self.is_empty = True
self.set_filenames(filenames, frames, template)
@ -323,7 +324,9 @@ class FileDefItem(object):
return json.dumps(self.to_dict())
def __repr__(self):
if self.is_sequence:
if self.is_empty:
filename = "< empty >"
elif self.is_sequence:
filename = self.template
else:
filename = self.filenames[0]
@ -335,6 +338,9 @@ class FileDefItem(object):
@property
def label(self):
if self.is_empty:
return None
if not self.is_sequence:
return self.filenames[0]
@ -386,6 +392,8 @@ class FileDefItem(object):
@property
def ext(self):
if self.is_empty:
return None
_, ext = os.path.splitext(self.filenames[0])
if ext:
return ext
@ -393,6 +401,9 @@ class FileDefItem(object):
@property
def is_dir(self):
if self.is_empty:
return False
# QUESTION a better way how to define folder (in init argument?)
if self.ext:
return False
@ -411,6 +422,7 @@ class FileDefItem(object):
if is_sequence and not template:
raise ValueError("Missing template for sequence")
self.is_empty = len(filenames) == 0
self.filenames = filenames
self.template = template
self.frames = frames
@ -560,11 +572,7 @@ class FileDef(AbtractAttrDef):
# Change horizontal label
is_label_horizontal = kwargs.get("is_label_horizontal")
if is_label_horizontal is None:
if single_item:
is_label_horizontal = True
else:
is_label_horizontal = False
kwargs["is_label_horizontal"] = is_label_horizontal
kwargs["is_label_horizontal"] = False
self.single_item = single_item
self.folders = folders

View file

@ -98,7 +98,7 @@ class Terminal:
r"\*\*\* WRN": _SB + _LY + r"*** WRN" + _RST,
r" \- ": _SB + _LY + r" - " + _RST,
r"\[ ": _SB + _LG + r"[ " + _RST,
r"\]": _SB + _LG + r"]" + _RST,
r" \]": _SB + _LG + r" ]" + _RST,
r"{": _LG + r"{",
r"}": r"}" + _RST,
r"\(": _LY + r"(",

View file

@ -493,8 +493,9 @@ def convert_for_ffmpeg(
erase_reason = "has too long value ({} chars).".format(
len(attr_value)
)
erase_attribute = True
if erase_attribute:
if not erase_attribute:
for char in NOT_ALLOWED_FFMPEG_CHARS:
if char in attr_value:
erase_attribute = True
@ -623,8 +624,9 @@ def convert_input_paths_for_ffmpeg(
erase_reason = "has too long value ({} chars).".format(
len(attr_value)
)
erase_attribute = True
if erase_attribute:
if not erase_attribute:
for char in NOT_ALLOWED_FFMPEG_CHARS:
if char in attr_value:
erase_attribute = True

View file

@ -290,49 +290,16 @@ def _load_modules():
log = PypeLogger.get_logger("ModulesLoader")
current_dir = os.path.abspath(os.path.dirname(__file__))
processed_paths = set()
processed_paths.add(current_dir)
# Import default modules imported from 'openpype.modules'
for filename in os.listdir(current_dir):
# Ignore filenames
if (
filename in IGNORED_FILENAMES
or filename in IGNORED_DEFAULT_FILENAMES
):
continue
fullpath = os.path.join(current_dir, filename)
basename, ext = os.path.splitext(filename)
if os.path.isdir(fullpath):
# Check existence of init fil
init_path = os.path.join(fullpath, "__init__.py")
if not os.path.exists(init_path):
log.debug((
"Module directory does not contan __init__.py file {}"
).format(fullpath))
continue
elif ext not in (".py", ):
continue
try:
import_str = "openpype.modules.{}".format(basename)
new_import_str = "{}.{}".format(modules_key, basename)
default_module = __import__(import_str, fromlist=("", ))
sys.modules[new_import_str] = default_module
setattr(openpype_modules, basename, default_module)
except Exception:
msg = (
"Failed to import default module '{}'."
).format(basename)
log.error(msg, exc_info=True)
# Look for OpenPype modules in paths defined with `get_module_dirs`
# - dynamically imported OpenPype modules and addons
for dirpath in get_module_dirs():
module_dirs = get_module_dirs()
# Add current directory at first place
# - has small differences in import logic
current_dir = os.path.abspath(os.path.dirname(__file__))
module_dirs.insert(0, current_dir)
processed_paths = set()
for dirpath in module_dirs:
# Skip already processed paths
if dirpath in processed_paths:
continue
@ -344,20 +311,29 @@ def _load_modules():
).format(dirpath))
continue
is_in_current_dir = dirpath == current_dir
for filename in os.listdir(dirpath):
# Ignore filenames
if filename in IGNORED_FILENAMES:
continue
if (
is_in_current_dir
and filename in IGNORED_DEFAULT_FILENAMES
):
continue
fullpath = os.path.join(dirpath, filename)
basename, ext = os.path.splitext(filename)
# Validations
if os.path.isdir(fullpath):
# Check existence of init fil
# Check existence of init file
init_path = os.path.join(fullpath, "__init__.py")
if not os.path.exists(init_path):
log.debug((
"Module directory does not contan __init__.py file {}"
"Module directory does not contain __init__.py"
" file {}"
).format(fullpath))
continue
@ -367,27 +343,29 @@ def _load_modules():
# TODO add more logic how to define if folder is module or not
# - check manifest and content of manifest
try:
if os.path.isdir(fullpath):
# Module without init file can't be used as OpenPype module
# because the module class could not be imported
init_file = os.path.join(fullpath, "__init__.py")
if not os.path.exists(init_file):
log.info((
"Skipping module directory because of"
" missing \"__init__.py\" file. \"{}\""
).format(fullpath))
continue
# Don't import dynamically current directory modules
if is_in_current_dir:
import_str = "openpype.modules.{}".format(basename)
new_import_str = "{}.{}".format(modules_key, basename)
default_module = __import__(import_str, fromlist=("", ))
sys.modules[new_import_str] = default_module
setattr(openpype_modules, basename, default_module)
elif os.path.isdir(fullpath):
import_module_from_dirpath(dirpath, filename, modules_key)
elif ext in (".py", ):
else:
module = import_filepath(fullpath)
setattr(openpype_modules, basename, module)
except Exception:
log.error(
"Failed to import '{}'.".format(fullpath),
exc_info=True
)
if is_in_current_dir:
msg = "Failed to import default module '{}'.".format(
basename
)
else:
msg = "Failed to import module '{}'.".format(fullpath)
log.error(msg, exc_info=True)
class _OpenPypeInterfaceMeta(ABCMeta):

View file

@ -440,7 +440,10 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
output_filename_0 = filename_0
dirname = os.path.dirname(output_filename_0)
# this is needed because renderman handles directory and file
# prefixes separately
if self._instance.data["renderer"] == "renderman":
dirname = os.path.dirname(output_filename_0)
# Create render folder ----------------------------------------------
try:

View file

@ -569,7 +569,7 @@ class DeleteOldVersions(BaseAction):
context["frame"] = self.sequence_splitter
sequence_path = os.path.normpath(
StringTemplate.format_strict_template(
context, template
template, context
)
)

View file

@ -3,6 +3,7 @@ import json
import copy
import pyblish.api
from openpype.lib import get_ffprobe_streams
from openpype.lib.profiles_filtering import filter_profiles
@ -142,6 +143,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
# Create thumbnail components
# TODO what if there is multiple thumbnails?
first_thumbnail_component = None
first_thumbnail_component_repre = None
for repre in thumbnail_representations:
published_path = repre.get("published_path")
if not published_path:
@ -169,12 +171,43 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
src_components_to_add.append(copy.deepcopy(thumbnail_item))
# Create copy of first thumbnail
if first_thumbnail_component is None:
first_thumbnail_component = copy.deepcopy(thumbnail_item)
first_thumbnail_component_repre = repre
first_thumbnail_component = thumbnail_item
# Set location
thumbnail_item["component_location"] = ftrack_server_location
# Add item to component list
component_list.append(thumbnail_item)
if first_thumbnail_component is not None:
width = first_thumbnail_component_repre.get("width")
height = first_thumbnail_component_repre.get("height")
if not width or not height:
component_path = first_thumbnail_component["component_path"]
streams = []
try:
streams = get_ffprobe_streams(component_path)
except Exception:
self.log.debug((
"Failed to retrieve information about intput {}"
).format(component_path))
for stream in streams:
if "width" in stream and "height" in stream:
width = stream["width"]
height = stream["height"]
break
if width and height:
component_data = first_thumbnail_component["component_data"]
component_data["name"] = "ftrackreview-image"
component_data["metadata"] = {
"ftr_meta": json.dumps({
"width": width,
"height": height,
"format": "image"
})
}
# Create review components
# Change asset name of each new component for review
is_first_review_repre = True

View file

@ -25,7 +25,7 @@ def install():
session = session_data_from_environment(context_keys=True)
session["schema"] = "openpype:session-2.0"
session["schema"] = "openpype:session-3.0"
try:
schema.validate(session)
except schema.ValidationError as e:

View file

@ -55,36 +55,49 @@
"nukeNodeClass": "Write",
"knobs": [
{
"type": "text",
"name": "file_type",
"value": "exr"
},
{
"type": "text",
"name": "datatype",
"value": "16 bit half"
},
{
"type": "text",
"name": "compression",
"value": "Zip (1 scanline)"
},
{
"type": "bool",
"name": "autocrop",
"value": "True"
"value": true
},
{
"type": "color_gui",
"name": "tile_color",
"value": "0xff0000ff"
"value": [
186,
35,
35,
255
]
},
{
"type": "text",
"name": "channels",
"value": "rgb"
},
{
"type": "text",
"name": "colorspace",
"value": "linear"
},
{
"type": "bool",
"name": "create_directories",
"value": "True"
"value": true
}
]
},
@ -95,36 +108,49 @@
"nukeNodeClass": "Write",
"knobs": [
{
"type": "text",
"name": "file_type",
"value": "exr"
},
{
"type": "text",
"name": "datatype",
"value": "16 bit half"
},
{
"type": "text",
"name": "compression",
"value": "Zip (1 scanline)"
},
{
"type": "bool",
"name": "autocrop",
"value": "False"
"value": true
},
{
"type": "color_gui",
"name": "tile_color",
"value": "0xadab1dff"
"value": [
171,
171,
10,
255
]
},
{
"type": "text",
"name": "channels",
"value": "rgb"
},
{
"type": "text",
"name": "colorspace",
"value": "linear"
},
{
"type": "bool",
"name": "create_directories",
"value": "True"
"value": true
}
]
},
@ -135,32 +161,44 @@
"nukeNodeClass": "Write",
"knobs": [
{
"type": "text",
"name": "file_type",
"value": "tiff"
},
{
"type": "text",
"name": "datatype",
"value": "16 bit"
},
{
"type": "text",
"name": "compression",
"value": "Deflate"
},
{
"type": "color_gui",
"name": "tile_color",
"value": "0x23ff00ff"
"value": [
56,
162,
7,
255
]
},
{
"type": "text",
"name": "channels",
"value": "rgb"
},
{
"type": "text",
"name": "colorspace",
"value": "sRGB"
},
{
"type": "bool",
"name": "create_directories",
"value": "True"
"value": true
}
]
}
@ -170,7 +208,7 @@
"regexInputs": {
"inputs": [
{
"regex": "[^-a-zA-Z0-9]beauty[^-a-zA-Z0-9]",
"regex": "(beauty).*(?=.exr)",
"colorspace": "linear"
}
]

View file

@ -21,10 +21,29 @@
"defaults": [
"Main",
"Mask"
]
],
"knobs": [],
"prenodes": {
"Reformat01": {
"nodeclass": "Reformat",
"dependent": "",
"knobs": [
{
"type": "text",
"name": "resize",
"value": "none"
},
{
"type": "bool",
"name": "black_outside",
"value": true
}
]
}
}
},
"CreateWritePrerender": {
"fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}",
"fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}",
"use_range_limit": true,
"defaults": [
"Key01",
@ -33,7 +52,32 @@
"Branch01",
"Part01"
],
"reviewable": false
"reviewable": false,
"knobs": [],
"prenodes": {}
},
"CreateWriteStill": {
"fpath_template": "{work}/renders/nuke/{subset}/{subset}.{ext}",
"defaults": [
"ImageFrame",
"MPFrame",
"LayoutFrame"
],
"knobs": [],
"prenodes": {
"FrameHold01": {
"nodeclass": "FrameHold",
"dependent": "",
"knobs": [
{
"type": "formatable",
"name": "first_frame",
"template": "{frame}",
"to_type": "number"
}
]
}
}
}
},
"publish": {
@ -129,17 +173,17 @@
"reformat_node_add": false,
"reformat_node_config": [
{
"type": "string",
"type": "text",
"name": "type",
"value": "to format"
},
{
"type": "string",
"type": "text",
"name": "format",
"value": "HD_1080"
},
{
"type": "string",
"type": "text",
"name": "filter",
"value": "Lanczos6"
},
@ -220,11 +264,12 @@
"repre_names": [
"exr",
"dpx",
"mov"
"mov",
"mp4",
"h264"
],
"loaders": [
"LoadSequence",
"LoadMov"
"LoadClip"
]
}
],

View file

@ -8,7 +8,6 @@
"default_variants": [
"Main"
],
"enable_review": false,
"description": "Publish workfile backup",
"detailed_description": "",
"allow_sequences": true,

View file

@ -87,7 +87,7 @@
"children": [
{
"type": "dict",
"collapsible": false,
"collapsible": true,
"key": "CreateWriteRender",
"label": "CreateWriteRender",
"is_group": true,
@ -104,12 +104,53 @@
"object_type": {
"type": "text"
}
},
{
"type": "schema_template",
"name": "template_nuke_knob_inputs",
"template_data": [
{
"label": "Node knobs",
"key": "knobs"
}
]
},
{
"key": "prenodes",
"label": "Pre write nodes",
"type": "dict-modifiable",
"highlight_content": true,
"object_type": {
"type": "dict",
"children": [
{
"key": "nodeclass",
"label": "Node class",
"type": "text"
},
{
"key": "dependent",
"label": "Outside node dependency",
"type": "text"
},
{
"type": "schema_template",
"name": "template_nuke_knob_inputs",
"template_data": [
{
"label": "Node knobs",
"key": "knobs"
}
]
}
]
}
}
]
},
{
"type": "dict",
"collapsible": false,
"collapsible": true,
"key": "CreateWritePrerender",
"label": "CreateWritePrerender",
"is_group": true,
@ -136,6 +177,110 @@
"type": "boolean",
"key": "reviewable",
"label": "Add reviewable toggle"
},
{
"type": "schema_template",
"name": "template_nuke_knob_inputs",
"template_data": [
{
"label": "Node knobs",
"key": "knobs"
}
]
},
{
"key": "prenodes",
"label": "Pre write nodes",
"type": "dict-modifiable",
"highlight_content": true,
"object_type": {
"type": "dict",
"children": [
{
"key": "nodeclass",
"label": "Node class",
"type": "text"
},
{
"key": "dependent",
"label": "Outside node dependency",
"type": "text"
},
{
"type": "schema_template",
"name": "template_nuke_knob_inputs",
"template_data": [
{
"label": "Node knobs",
"key": "knobs"
}
]
}
]
}
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "CreateWriteStill",
"label": "CreateWriteStill",
"is_group": true,
"children": [
{
"type": "text",
"key": "fpath_template",
"label": "Path template"
},
{
"type": "list",
"key": "defaults",
"label": "Subset name defaults",
"object_type": {
"type": "text"
}
},
{
"type": "schema_template",
"name": "template_nuke_knob_inputs",
"template_data": [
{
"label": "Node knobs",
"key": "knobs"
}
]
},
{
"key": "prenodes",
"label": "Pre write nodes",
"type": "dict-modifiable",
"highlight_content": true,
"object_type": {
"type": "dict",
"children": [
{
"key": "nodeclass",
"label": "Node class",
"type": "text"
},
{
"key": "dependent",
"label": "Outside node dependency",
"type": "text"
},
{
"type": "schema_template",
"name": "template_nuke_knob_inputs",
"template_data": [
{
"label": "Node knobs",
"key": "knobs"
}
]
}
]
}
}
]
}

View file

@ -45,12 +45,6 @@
"type": "text"
}
},
{
"type": "boolean",
"key": "enable_review",
"label": "Enable review",
"tooltip": "Allow to create review from source file/s.\nFiles must be supported to be able create review."
},
{
"type": "separator"
},

View file

@ -272,29 +272,12 @@
"label": "Nuke Node Class"
},
{
"type": "collapsible-wrap",
"label": "Knobs",
"collapsible": true,
"collapsed": true,
"children": [
"type": "schema_template",
"name": "template_nuke_knob_inputs",
"template_data": [
{
"key": "knobs",
"type": "list",
"object_type": {
"type": "dict",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "text",
"key": "value",
"label": "Value"
}
]
}
"label": "Knobs",
"key": "knobs"
}
]
}
@ -333,29 +316,12 @@
"object_type": "text"
},
{
"type": "collapsible-wrap",
"label": "Knobs overrides",
"collapsible": true,
"collapsed": true,
"children": [
"type": "schema_template",
"name": "template_nuke_knob_inputs",
"template_data": [
{
"key": "knobs",
"type": "list",
"object_type": {
"type": "dict",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "text",
"key": "value",
"label": "Value"
}
]
}
"label": "Knobs overrides",
"key": "knobs"
}
]
}

View file

@ -253,108 +253,12 @@
"default": false
},
{
"type": "collapsible-wrap",
"label": "Reformat Node Knobs",
"collapsible": true,
"collapsed": true,
"children": [
"type": "schema_template",
"name": "template_nuke_knob_inputs",
"template_data": [
{
"type": "list",
"key": "reformat_node_config",
"object_type": {
"type": "dict-conditional",
"enum_key": "type",
"enum_label": "Type",
"enum_children": [
{
"key": "string",
"label": "String",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "text",
"key": "value",
"label": "Value"
}
]
},
{
"key": "bool",
"label": "Boolean",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "boolean",
"key": "value",
"label": "Value"
}
]
},
{
"key": "number",
"label": "Number",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "list-strict",
"key": "value",
"label": "Value",
"object_types": [
{
"type": "number",
"key": "number",
"default": 1,
"decimal": 4
}
]
}
]
},
{
"key": "list_numbers",
"label": "2 Numbers",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "list-strict",
"key": "value",
"label": "Value",
"object_types": [
{
"type": "number",
"key": "x",
"default": 1,
"decimal": 4
},
{
"type": "number",
"key": "y",
"default": 1,
"decimal": 4
}
]
}
]
}
]
}
"label": "Reformat Node Knobs",
"key": "reformat_node_config"
}
]
},

View file

@ -0,0 +1,275 @@
[
{
"type": "collapsible-wrap",
"label": "{label}",
"collapsible": true,
"collapsed": true,
"children": [{
"type": "list",
"key": "{key}",
"object_type": {
"type": "dict-conditional",
"enum_key": "type",
"enum_label": "Type",
"enum_children": [
{
"key": "text",
"label": "Text",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "text",
"key": "value",
"label": "Value"
}
]
},
{
"key": "formatable",
"label": "Formate from template",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "text",
"key": "template",
"label": "Template",
"placeholder": "{{key}} or {{key}};{{key}}"
},
{
"type": "enum",
"key": "to_type",
"label": "Knob type",
"enum_items": [
{
"text": "Text"
},
{
"number": "Number"
},
{
"decimal_number": "Decimal number"
},
{
"2d_vector": "2D vector"
}
]
}
]
},
{
"key": "color_gui",
"label": "Color GUI",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "color",
"key": "value",
"label": "Value",
"use_alpha": false
}
]
},
{
"key": "bool",
"label": "Boolean",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "boolean",
"key": "value",
"label": "Value"
}
]
},
{
"key": "number",
"label": "Number",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "number",
"key": "value",
"default": 1,
"decimal": 0,
"maximum": 99999999
}
]
},
{
"key": "decimal_number",
"label": "Decimal number",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "number",
"key": "value",
"default": 1,
"decimal": 4,
"maximum": 99999999
}
]
},
{
"key": "2d_vector",
"label": "2D vector",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "list-strict",
"key": "value",
"label": "Value",
"object_types": [
{
"type": "number",
"key": "x",
"default": 1,
"decimal": 4,
"maximum": 99999999
},
{
"type": "number",
"key": "y",
"default": 1,
"decimal": 4,
"maximum": 99999999
}
]
}
]
},
{
"key": "3d_vector",
"label": "3D vector",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "list-strict",
"key": "value",
"label": "Value",
"object_types": [
{
"type": "number",
"key": "x",
"default": 1,
"decimal": 4,
"maximum": 99999999
},
{
"type": "number",
"key": "y",
"default": 1,
"decimal": 4,
"maximum": 99999999
},
{
"type": "number",
"key": "y",
"default": 1,
"decimal": 4,
"maximum": 99999999
}
]
}
]
},
{
"key": "color",
"label": "Color",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "list-strict",
"key": "value",
"label": "Value",
"object_types": [
{
"type": "number",
"key": "x",
"default": 1,
"decimal": 4,
"maximum": 99999999
},
{
"type": "number",
"key": "x",
"default": 1,
"decimal": 4,
"maximum": 99999999
},
{
"type": "number",
"key": "y",
"default": 1,
"decimal": 4,
"maximum": 99999999
},
{
"type": "number",
"key": "y",
"default": 1,
"decimal": 4,
"maximum": 99999999
}
]
}
]
},
{
"key": "__legacy__",
"label": "_ Legacy type _",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "text",
"key": "value",
"label": "Value"
}
]
}
]
}
}]
}
]

View file

@ -291,6 +291,22 @@ def _system_settings_backwards_compatible_conversion(studio_overrides):
}
def _project_anatomy_backwards_compatible_conversion(project_anatomy):
# Backwards compatibility of node settings in Nuke 3.9.x - 3.10.0
# - source PR - https://github.com/pypeclub/OpenPype/pull/3143
value = project_anatomy
for key in ("imageio", "nuke", "nodes", "requiredNodes"):
if key not in value:
return
value = value[key]
for item in value:
for node in item.get("knobs") or []:
if "type" in node:
break
node["type"] = "__legacy__"
@require_handler
def get_studio_system_settings_overrides(return_version=False):
output = _SETTINGS_HANDLER.get_studio_system_settings_overrides(
@ -326,7 +342,9 @@ def get_project_settings_overrides(project_name, return_version=False):
@require_handler
def get_project_anatomy_overrides(project_name):
return _SETTINGS_HANDLER.get_project_anatomy_overrides(project_name)
output = _SETTINGS_HANDLER.get_project_anatomy_overrides(project_name)
_project_anatomy_backwards_compatible_conversion(output)
return output
@require_handler

View file

@ -856,18 +856,31 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
}
/* New Create/Publish UI */
#CreatorDetailedDescription {
padding-left: 5px;
padding-right: 5px;
padding-top: 5px;
background: transparent;
border: 1px solid {color:border};
}
#CreateDialogHelpButton {
background: rgba(255, 255, 255, 31);
border-top-left-radius: 0.2em;
border-bottom-left-radius: 0.2em;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
font-size: 10pt;
font-weight: bold;
padding: 3px 3px 3px 3px;
padding: 0px;
}
#CreateDialogHelpButton:hover {
background: rgba(255, 255, 255, 63);
}
#CreateDialogHelpButton QWidget {
background: transparent;
}
#PublishLogConsole {
font-family: "Noto Sans Mono";
@ -1014,7 +1027,44 @@ VariantInputsWidget QToolButton {
border-left: 1px solid {color:border};
}
#TasksCombobox[state="invalid"], #AssetNameInput[state="invalid"] {
#AssetNameInputWidget {
background: {color:bg-inputs};
border: 1px solid {color:border};
border-radius: 0.3em;
}
#AssetNameInputWidget QWidget {
background: transparent;
}
#AssetNameInputButton {
border-bottom-left-radius: 0px;
border-top-left-radius: 0px;
padding: 0px;
qproperty-iconSize: 11px 11px;
border-left: 1px solid {color:border};
border-right: none;
border-top: none;
border-bottom: none;
}
#AssetNameInput {
border-bottom-right-radius: 0px;
border-top-right-radius: 0px;
border: none;
}
#AssetNameInputWidget:hover {
border-color: {color:border-hover};
}
#AssetNameInputWidget:focus{
border-color: {color:border-focus};
}
#AssetNameInputWidget:disabled {
background: {color:bg-inputs-disabled};
}
#TasksCombobox[state="invalid"], #AssetNameInputWidget[state="invalid"], #AssetNameInputButton[state="invalid"] {
border-color: {color:publisher:error};
}

View file

@ -15,6 +15,7 @@ from openpype.tools.utils.assets_widget import (
class CreateDialogAssetsWidget(SingleSelectAssetsWidget):
current_context_required = QtCore.Signal()
header_height_changed = QtCore.Signal(int)
def __init__(self, controller, parent):
self._controller = controller
@ -27,6 +28,27 @@ class CreateDialogAssetsWidget(SingleSelectAssetsWidget):
self._last_selection = None
self._enabled = None
self._last_filter_height = None
def _check_header_height(self):
"""Catch header height changes.
Label on top of creaters should have same height so Creators view has
same offset.
"""
height = self.header_widget.height()
if height != self._last_filter_height:
self._last_filter_height = height
self.header_height_changed.emit(height)
def resizeEvent(self, event):
super(CreateDialogAssetsWidget, self).resizeEvent(event)
self._check_header_height()
def showEvent(self, event):
super(CreateDialogAssetsWidget, self).showEvent(event)
self._check_header_height()
def _on_current_asset_click(self):
self.current_context_required.emit()
@ -71,6 +93,7 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel):
Uses controller to load asset hierarchy. All asset documents are stored by
their parents.
"""
def __init__(self, controller):
super(AssetsHierarchyModel, self).__init__()
self._controller = controller
@ -143,6 +166,7 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel):
class AssetsDialog(QtWidgets.QDialog):
"""Dialog to select asset for a context of instance."""
def __init__(self, controller, parent):
super(AssetsDialog, self).__init__(parent)
self.setWindowTitle("Select asset")
@ -196,9 +220,26 @@ class AssetsDialog(QtWidgets.QDialog):
# - adds ability to call reset on multiple places without repeating
self._soft_reset_enabled = True
self._first_show = True
self._default_height = 500
def _on_first_show(self):
center = self.rect().center()
size = self.size()
size.setHeight(self._default_height)
self.resize(size)
new_pos = self.mapToGlobal(center)
new_pos.setX(new_pos.x() - int(self.width() / 2))
new_pos.setY(new_pos.y() - int(self.height() / 2))
self.move(new_pos)
def showEvent(self, event):
"""Refresh asset model on show."""
super(AssetsDialog, self).showEvent(event)
if self._first_show:
self._first_show = False
self._on_first_show()
# Refresh on show
self.reset(False)

View file

@ -3,6 +3,7 @@ import re
import traceback
import copy
import qtawesome
try:
import commonmark
except Exception:
@ -15,7 +16,8 @@ from openpype.pipeline.create import (
)
from openpype.tools.utils import (
ErrorMessageBox,
MessageOverlayObject
MessageOverlayObject,
ClickableFrame,
)
from .widgets import IconValuePixmapLabel
@ -114,6 +116,8 @@ class CreateErrorMessageBox(ErrorMessageBox):
# TODO add creator identifier/label to details
class CreatorShortDescWidget(QtWidgets.QWidget):
height_changed = QtCore.Signal(int)
def __init__(self, parent=None):
super(CreatorShortDescWidget, self).__init__(parent=parent)
@ -152,6 +156,22 @@ class CreatorShortDescWidget(QtWidgets.QWidget):
self._family_label = family_label
self._description_label = description_label
self._last_height = None
def _check_height_change(self):
height = self.height()
if height != self._last_height:
self._last_height = height
self.height_changed.emit(height)
def showEvent(self, event):
super(CreatorShortDescWidget, self).showEvent(event)
self._check_height_change()
def resizeEvent(self, event):
super(CreatorShortDescWidget, self).resizeEvent(event)
self._check_height_change()
def set_plugin(self, plugin=None):
if not plugin:
self._icon_widget.set_icon_def(None)
@ -168,13 +188,43 @@ class CreatorShortDescWidget(QtWidgets.QWidget):
self._description_label.setText(description)
class HelpButton(QtWidgets.QPushButton):
resized = QtCore.Signal()
class HelpButton(ClickableFrame):
resized = QtCore.Signal(int)
question_mark_icon_name = "fa.question"
help_icon_name = "fa.question-circle"
hide_icon_name = "fa.angle-left"
def __init__(self, *args, **kwargs):
super(HelpButton, self).__init__(*args, **kwargs)
self.setObjectName("CreateDialogHelpButton")
question_mark_label = QtWidgets.QLabel(self)
help_widget = QtWidgets.QWidget(self)
help_question = QtWidgets.QLabel(help_widget)
help_label = QtWidgets.QLabel("Help", help_widget)
hide_icon = QtWidgets.QLabel(help_widget)
help_layout = QtWidgets.QHBoxLayout(help_widget)
help_layout.setContentsMargins(0, 0, 5, 0)
help_layout.addWidget(help_question, 0)
help_layout.addWidget(help_label, 0)
help_layout.addStretch(1)
help_layout.addWidget(hide_icon, 0)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(question_mark_label, 0)
layout.addWidget(help_widget, 1)
help_widget.setVisible(False)
self._question_mark_label = question_mark_label
self._help_widget = help_widget
self._help_question = help_question
self._hide_icon = hide_icon
self._expanded = None
self.set_expanded()
@ -184,31 +234,56 @@ class HelpButton(QtWidgets.QPushButton):
return
expanded = False
self._expanded = expanded
if expanded:
text = "<"
self._help_widget.setVisible(expanded)
self._update_content()
def _update_content(self):
width = self.get_icon_width()
if self._expanded:
question_mark_pix = QtGui.QPixmap(width, width)
question_mark_pix.fill(QtCore.Qt.transparent)
else:
text = "?"
self.setText(text)
question_mark_icon = qtawesome.icon(
self.question_mark_icon_name, color=QtCore.Qt.white
)
question_mark_pix = question_mark_icon.pixmap(width, width)
self._update_size()
hide_icon = qtawesome.icon(
self.hide_icon_name, color=QtCore.Qt.white
)
help_question_icon = qtawesome.icon(
self.help_icon_name, color=QtCore.Qt.white
)
self._question_mark_label.setPixmap(question_mark_pix)
self._question_mark_label.setMaximumWidth(width)
self._hide_icon.setPixmap(hide_icon.pixmap(width, width))
self._help_question.setPixmap(help_question_icon.pixmap(width, width))
def _update_size(self):
new_size = self.minimumSizeHint()
if self.size() != new_size:
self.resize(new_size)
self.resized.emit()
def get_icon_width(self):
metrics = self.fontMetrics()
return metrics.height()
def set_pos_and_size(self, pos_x, pos_y, width, height):
update_icon = self.height() != height
self.move(pos_x, pos_y)
self.resize(width, height)
if update_icon:
self._update_content()
self.updateGeometry()
def showEvent(self, event):
super(HelpButton, self).showEvent(event)
self._update_size()
self.resized.emit(self.height())
def resizeEvent(self, event):
super(HelpButton, self).resizeEvent(event)
self._update_size()
self.resized.emit(self.height())
class CreateDialog(QtWidgets.QDialog):
default_size = (900, 500)
default_size = (1000, 560)
def __init__(
self, controller, asset_name=None, task_name=None, parent=None
@ -255,6 +330,14 @@ class CreateDialog(QtWidgets.QDialog):
context_layout.addWidget(tasks_widget, 1)
# --- Creators view ---
creators_header_widget = QtWidgets.QWidget(self)
header_label_widget = QtWidgets.QLabel(
"Choose family:", creators_header_widget
)
creators_header_layout = QtWidgets.QHBoxLayout(creators_header_widget)
creators_header_layout.setContentsMargins(0, 0, 0, 0)
creators_header_layout.addWidget(header_label_widget, 1)
creators_view = QtWidgets.QListView(self)
creators_model = QtGui.QStandardItemModel()
creators_view.setModel(creators_model)
@ -271,7 +354,6 @@ class CreateDialog(QtWidgets.QDialog):
variant_hints_menu = QtWidgets.QMenu(variant_widget)
variant_hints_group = QtWidgets.QActionGroup(variant_hints_menu)
# variant_hints_btn.setMenu(variant_hints_menu)
variant_layout = QtWidgets.QHBoxLayout(variant_widget)
variant_layout.setContentsMargins(0, 0, 0, 0)
@ -282,9 +364,6 @@ class CreateDialog(QtWidgets.QDialog):
subset_name_input = QtWidgets.QLineEdit(self)
subset_name_input.setEnabled(False)
create_btn = QtWidgets.QPushButton("Create", self)
create_btn.setEnabled(False)
form_layout = QtWidgets.QFormLayout()
form_layout.addRow("Variant:", variant_widget)
form_layout.addRow("Subset:", subset_name_input)
@ -292,10 +371,9 @@ class CreateDialog(QtWidgets.QDialog):
mid_widget = QtWidgets.QWidget(self)
mid_layout = QtWidgets.QVBoxLayout(mid_widget)
mid_layout.setContentsMargins(0, 0, 0, 0)
mid_layout.addWidget(QtWidgets.QLabel("Choose family:", self))
mid_layout.addWidget(creators_header_widget, 0)
mid_layout.addWidget(creators_view, 1)
mid_layout.addLayout(form_layout, 0)
mid_layout.addWidget(create_btn, 0)
# ------------
# --- Creator short info and attr defs ---
@ -305,31 +383,62 @@ class CreateDialog(QtWidgets.QDialog):
creator_attrs_widget
)
separator_widget = QtWidgets.QWidget(self)
separator_widget.setObjectName("Separator")
separator_widget.setMinimumHeight(2)
separator_widget.setMaximumHeight(2)
attr_separator_widget = QtWidgets.QWidget(self)
attr_separator_widget.setObjectName("Separator")
attr_separator_widget.setMinimumHeight(1)
attr_separator_widget.setMaximumHeight(1)
# Precreate attributes widget
pre_create_widget = PreCreateWidget(creator_attrs_widget)
# Create button
create_btn_wrapper = QtWidgets.QWidget(creator_attrs_widget)
create_btn = QtWidgets.QPushButton("Create", create_btn_wrapper)
create_btn.setEnabled(False)
create_btn_wrap_layout = QtWidgets.QHBoxLayout(create_btn_wrapper)
create_btn_wrap_layout.setContentsMargins(0, 0, 0, 0)
create_btn_wrap_layout.addStretch(1)
create_btn_wrap_layout.addWidget(create_btn, 0)
creator_attrs_layout = QtWidgets.QVBoxLayout(creator_attrs_widget)
creator_attrs_layout.setContentsMargins(0, 0, 0, 0)
creator_attrs_layout.addWidget(creator_short_desc_widget, 0)
creator_attrs_layout.addWidget(separator_widget, 0)
creator_attrs_layout.addWidget(attr_separator_widget, 0)
creator_attrs_layout.addWidget(pre_create_widget, 1)
creator_attrs_layout.addWidget(create_btn_wrapper, 0)
# -------------------------------------
# --- Detailed information about creator ---
# Detailed description of creator
detail_description_widget = QtWidgets.QTextEdit(self)
detail_description_widget.setObjectName("InfoText")
detail_description_widget.setTextInteractionFlags(
detail_description_widget = QtWidgets.QWidget(self)
detail_placoholder_widget = QtWidgets.QWidget(
detail_description_widget
)
detail_placoholder_widget.setAttribute(
QtCore.Qt.WA_TranslucentBackground
)
detail_description_input = QtWidgets.QTextEdit(
detail_description_widget
)
detail_description_input.setObjectName("CreatorDetailedDescription")
detail_description_input.setTextInteractionFlags(
QtCore.Qt.TextBrowserInteraction
)
detail_description_widget.setVisible(False)
# -------------------------------------------
detail_description_layout = QtWidgets.QVBoxLayout(
detail_description_widget
)
detail_description_layout.setContentsMargins(0, 0, 0, 0)
detail_description_layout.setSpacing(0)
detail_description_layout.addWidget(detail_placoholder_widget, 0)
detail_description_layout.addWidget(detail_description_input, 1)
detail_description_widget.setVisible(False)
# -------------------------------------------
splitter_widget = QtWidgets.QSplitter(self)
splitter_widget.addWidget(context_widget)
splitter_widget.addWidget(mid_widget)
@ -344,17 +453,27 @@ class CreateDialog(QtWidgets.QDialog):
layout.addWidget(splitter_widget, 1)
# Floating help button
# - Create this button as last to be fully visible
help_btn = HelpButton(self)
prereq_timer = QtCore.QTimer()
prereq_timer.setInterval(50)
prereq_timer.setSingleShot(True)
desc_width_anim_timer = QtCore.QTimer()
desc_width_anim_timer.setInterval(10)
prereq_timer.timeout.connect(self._on_prereq_timer)
desc_width_anim_timer.timeout.connect(self._on_desc_animation)
help_btn.clicked.connect(self._on_help_btn)
help_btn.resized.connect(self._on_help_btn_resize)
assets_widget.header_height_changed.connect(
self._on_asset_filter_height_change
)
create_btn.clicked.connect(self._on_create)
variant_widget.resized.connect(self._on_variant_widget_resize)
variant_input.returnPressed.connect(self._on_create)
@ -369,6 +488,10 @@ class CreateDialog(QtWidgets.QDialog):
self._on_current_session_context_request
)
tasks_widget.task_changed.connect(self._on_task_change)
creator_short_desc_widget.height_changed.connect(
self._on_description_height_change
)
splitter_widget.splitterMoved.connect(self._on_splitter_move)
controller.add_plugins_refresh_callback(self._on_plugins_refresh)
@ -387,18 +510,33 @@ class CreateDialog(QtWidgets.QDialog):
self.variant_hints_menu = variant_hints_menu
self.variant_hints_group = variant_hints_group
self._creators_header_widget = creators_header_widget
self.creators_model = creators_model
self.creators_view = creators_view
self.create_btn = create_btn
self._creator_short_desc_widget = creator_short_desc_widget
self._pre_create_widget = pre_create_widget
self._attr_separator_widget = attr_separator_widget
self._detail_placoholder_widget = detail_placoholder_widget
self._detail_description_widget = detail_description_widget
self._detail_description_input = detail_description_input
self._help_btn = help_btn
self._prereq_timer = prereq_timer
self._first_show = True
# Description animation
self._description_size_policy = detail_description_widget.sizePolicy()
self._desc_width_anim_timer = desc_width_anim_timer
self._desc_widget_step = 0
self._last_description_width = None
self._last_full_width = 0
self._expected_description_width = 0
self._last_desc_max_width = None
self._other_widgets_widths = []
def _emit_message(self, message):
self._overlay_object.add_message(message)
@ -465,6 +603,10 @@ class CreateDialog(QtWidgets.QDialog):
def _invalidate_prereq(self):
self._prereq_timer.start()
def _on_asset_filter_height_change(self, height):
self._creators_header_widget.setMinimumHeight(height)
self._creators_header_widget.setMaximumHeight(height)
def _on_prereq_timer(self):
prereq_available = True
creator_btn_tooltips = []
@ -595,6 +737,12 @@ class CreateDialog(QtWidgets.QDialog):
if self._task_name:
self._tasks_widget.select_task_name(self._task_name)
def _on_description_height_change(self):
# Use separator's 'y' position as height
height = self._attr_separator_widget.y()
self._detail_placoholder_widget.setMinimumHeight(height)
self._detail_placoholder_widget.setMaximumHeight(height)
def _on_creator_item_change(self, new_index, _old_index):
identifier = None
if new_index.isValid():
@ -602,54 +750,192 @@ class CreateDialog(QtWidgets.QDialog):
self._set_creator_by_identifier(identifier)
def _update_help_btn(self):
pos_x = self.width() - self._help_btn.width()
point = self._creator_short_desc_widget.rect().topRight()
mapped_point = self._creator_short_desc_widget.mapTo(self, point)
pos_y = mapped_point.y()
self._help_btn.move(max(0, pos_x), max(0, pos_y))
short_desc_rect = self._creator_short_desc_widget.rect()
def _on_help_btn_resize(self):
# point = short_desc_rect.topRight()
point = short_desc_rect.center()
mapped_point = self._creator_short_desc_widget.mapTo(self, point)
# pos_y = mapped_point.y()
center_pos_y = mapped_point.y()
icon_width = self._help_btn.get_icon_width()
_height = int(icon_width * 2.5)
height = min(_height, short_desc_rect.height())
pos_y = center_pos_y - int(height / 2)
pos_x = self.width() - icon_width
if self._detail_placoholder_widget.isVisible():
pos_x -= (
self._detail_placoholder_widget.width()
+ self._splitter_widget.handle(3).width()
)
width = self.width() - pos_x
self._help_btn.set_pos_and_size(
max(0, pos_x), max(0, pos_y),
width, height
)
def _on_help_btn_resize(self, height):
if self._creator_short_desc_widget.height() != height:
self._update_help_btn()
def _on_splitter_move(self, *args):
self._update_help_btn()
def _on_help_btn(self):
if self._desc_width_anim_timer.isActive():
return
final_size = self.size()
cur_sizes = self._splitter_widget.sizes()
spacing = self._splitter_widget.handleWidth()
if self._desc_widget_step == 0:
now_visible = self._detail_description_widget.isVisible()
else:
now_visible = self._desc_widget_step > 0
sizes = []
for idx, value in enumerate(cur_sizes):
if idx < 3:
sizes.append(value)
now_visible = self._detail_description_widget.isVisible()
self._last_full_width = final_size.width()
self._other_widgets_widths = list(sizes)
if now_visible:
width = final_size.width() - (
spacing + self._detail_description_widget.width()
)
cur_desc_width = self._detail_description_widget.width()
if cur_desc_width < 1:
cur_desc_width = 2
step_size = int(cur_desc_width / 5)
if step_size < 1:
step_size = 1
step_size *= -1
expected_width = 0
desc_width = cur_desc_width - 1
width = final_size.width() - 1
min_max = desc_width
self._last_description_width = cur_desc_width
else:
last_size = self._detail_description_widget.sizeHint().width()
width = final_size.width() + spacing + last_size
sizes.append(last_size)
self._detail_description_widget.setVisible(True)
handle = self._splitter_widget.handle(3)
desc_width = handle.sizeHint().width()
if self._last_description_width:
expected_width = self._last_description_width
else:
hint = self._detail_description_widget.sizeHint()
expected_width = hint.width()
width = final_size.width() + desc_width
step_size = int(expected_width / 5)
if step_size < 1:
step_size = 1
min_max = 0
if self._last_desc_max_width is None:
self._last_desc_max_width = (
self._detail_description_widget.maximumWidth()
)
self._detail_description_widget.setMinimumWidth(min_max)
self._detail_description_widget.setMaximumWidth(min_max)
self._expected_description_width = expected_width
self._desc_widget_step = step_size
self._desc_width_anim_timer.start()
sizes.append(desc_width)
final_size.setWidth(width)
self._detail_description_widget.setVisible(not now_visible)
self._splitter_widget.setSizes(sizes)
self.resize(final_size)
self._help_btn.set_expanded(not now_visible)
def _on_desc_animation(self):
current_width = self._detail_description_widget.width()
desc_width = None
last_step = False
growing = self._desc_widget_step > 0
# Growing
if growing:
if current_width < self._expected_description_width:
desc_width = current_width + self._desc_widget_step
if desc_width >= self._expected_description_width:
desc_width = self._expected_description_width
last_step = True
# Decreasing
elif self._desc_widget_step < 0:
if current_width > self._expected_description_width:
desc_width = current_width + self._desc_widget_step
if desc_width <= self._expected_description_width:
desc_width = self._expected_description_width
last_step = True
if desc_width is None:
self._desc_widget_step = 0
self._desc_width_anim_timer.stop()
return
if last_step and not growing:
self._detail_description_widget.setVisible(False)
QtWidgets.QApplication.processEvents()
width = self._last_full_width
handle_width = self._splitter_widget.handle(3).width()
if growing:
width += (handle_width + desc_width)
else:
width -= self._last_description_width
if last_step:
width -= handle_width
else:
width += desc_width
if not last_step or growing:
self._detail_description_widget.setMaximumWidth(desc_width)
self._detail_description_widget.setMinimumWidth(desc_width)
window_size = self.size()
window_size.setWidth(width)
self.resize(window_size)
if not last_step:
return
self._desc_widget_step = 0
self._desc_width_anim_timer.stop()
if not growing:
return
self._detail_description_widget.setMinimumWidth(0)
self._detail_description_widget.setMaximumWidth(
self._last_desc_max_width
)
self._detail_description_widget.setSizePolicy(
self._description_size_policy
)
sizes = list(self._other_widgets_widths)
sizes.append(desc_width)
self._splitter_widget.setSizes(sizes)
def _set_creator_detailed_text(self, creator):
if not creator:
self._detail_description_widget.setPlainText("")
self._detail_description_input.setPlainText("")
return
detailed_description = creator.get_detail_description() or ""
if commonmark:
html = commonmark.commonmark(detailed_description)
self._detail_description_widget.setHtml(html)
self._detail_description_input.setHtml(html)
else:
self._detail_description_widget.setMarkdown(detailed_description)
self._detail_description_input.setMarkdown(detailed_description)
def _set_creator_by_identifier(self, identifier):
creator = self.controller.manual_creators.get(identifier)
@ -806,6 +1092,21 @@ class CreateDialog(QtWidgets.QDialog):
self.variant_input.setProperty("state", state)
self.variant_input.style().polish(self.variant_input)
def _on_first_show(self):
center = self.rect().center()
width, height = self.default_size
self.resize(width, height)
part = int(width / 7)
self._splitter_widget.setSizes(
[part * 2, part * 2, width - (part * 4)]
)
new_pos = self.mapToGlobal(center)
new_pos.setX(new_pos.x() - int(self.width() / 2))
new_pos.setY(new_pos.y() - int(self.height() / 2))
self.move(new_pos)
def moveEvent(self, event):
super(CreateDialog, self).moveEvent(event)
self._last_pos = self.pos()
@ -814,13 +1115,7 @@ class CreateDialog(QtWidgets.QDialog):
super(CreateDialog, self).showEvent(event)
if self._first_show:
self._first_show = False
width, height = self.default_size
self.resize(width, height)
third_size = int(width / 3)
self._splitter_widget.setSizes(
[third_size, third_size, width - (2 * third_size)]
)
self._on_first_show()
if self._last_pos is not None:
self.move(self._last_pos)

View file

@ -14,7 +14,8 @@ from openpype.tools.utils import (
PlaceholderLineEdit,
IconButton,
PixmapLabel,
BaseClickableFrame
BaseClickableFrame,
set_style_property,
)
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
from .assets_widget import AssetsDialog
@ -344,21 +345,42 @@ class AssetsField(BaseClickableFrame):
def __init__(self, controller, parent):
super(AssetsField, self).__init__(parent)
self.setObjectName("AssetNameInputWidget")
dialog = AssetsDialog(controller, self)
# Don't use 'self' for parent!
# - this widget has specific styles
dialog = AssetsDialog(controller, parent)
name_input = ClickableLineEdit(self)
name_input.setObjectName("AssetNameInput")
icon_name = "fa.window-maximize"
icon = qtawesome.icon(icon_name, color="white")
icon_btn = QtWidgets.QPushButton(self)
icon_btn.setIcon(icon)
icon_btn.setObjectName("AssetNameInputButton")
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(name_input, 1)
layout.addWidget(icon_btn, 0)
# Make sure all widgets are vertically extended to highest widget
for widget in (
name_input,
icon_btn
):
size_policy = widget.sizePolicy()
size_policy.setVerticalPolicy(size_policy.MinimumExpanding)
widget.setSizePolicy(size_policy)
name_input.clicked.connect(self._mouse_release_callback)
icon_btn.clicked.connect(self._mouse_release_callback)
dialog.finished.connect(self._on_dialog_finish)
self._dialog = dialog
self._name_input = name_input
self._icon_btn = icon_btn
self._origin_value = []
self._origin_selection = []
@ -406,10 +428,9 @@ class AssetsField(BaseClickableFrame):
self._set_state_property(state)
def _set_state_property(self, state):
current_value = self._name_input.property("state")
if current_value != state:
self._name_input.setProperty("state", state)
self._name_input.style().polish(self._name_input)
set_style_property(self, "state", state)
set_style_property(self._name_input, "state", state)
set_style_property(self._icon_btn, "state", state)
def is_valid(self):
"""Is asset valid."""
@ -842,6 +863,8 @@ class VariantInputWidget(PlaceholderLineEdit):
self._ignore_value_change = True
self._has_value_changed = False
self._origin_value = list(variants)
self._current_value = list(variants)
@ -892,11 +915,23 @@ class MultipleItemWidget(QtWidgets.QWidget):
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(view)
model.rowsInserted.connect(self._on_insert)
self._view = view
self._model = model
self._value = []
def _on_insert(self):
self._update_size()
def _update_size(self):
model = self._view.model()
if model.rowCount() == 0:
return
height = self._view.sizeHintForRow(0)
self.setMaximumHeight(height + (2 * self._view.spacing()))
def showEvent(self, event):
super(MultipleItemWidget, self).showEvent(event)
tmp_item = None
@ -904,13 +939,15 @@ class MultipleItemWidget(QtWidgets.QWidget):
# Add temp item to be able calculate maximum height of widget
tmp_item = QtGui.QStandardItem("tmp")
self._model.appendRow(tmp_item)
height = self._view.sizeHintForRow(0)
self.setMaximumHeight(height + (2 * self._view.spacing()))
self._update_size()
if tmp_item is not None:
self._model.clear()
def resizeEvent(self, event):
super(MultipleItemWidget, self).resizeEvent(event)
self._update_size()
def set_value(self, value=None):
"""Set value/s of currently selected instance."""
if value is None:
@ -1235,7 +1272,11 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
)
content_widget = QtWidgets.QWidget(self._scroll_area)
content_layout = QtWidgets.QFormLayout(content_widget)
content_layout = QtWidgets.QGridLayout(content_widget)
content_layout.setColumnStretch(0, 0)
content_layout.setColumnStretch(1, 1)
row = 0
for attr_def, attr_instances, values in result:
widget = create_widget_for_attr_def(attr_def, content_widget)
if attr_def.is_value_def:
@ -1246,10 +1287,28 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
else:
widget.set_value(values, True)
label = attr_def.label or attr_def.key
content_layout.addRow(label, widget)
widget.value_changed.connect(self._input_value_changed)
expand_cols = 2
if attr_def.is_value_def and attr_def.is_label_horizontal:
expand_cols = 1
col_num = 2 - expand_cols
label = attr_def.label or attr_def.key
if label:
label_widget = QtWidgets.QLabel(label, self)
content_layout.addWidget(
label_widget, row, 0, 1, expand_cols
)
if not attr_def.is_label_horizontal:
row += 1
content_layout.addWidget(
widget, row, col_num, 1, expand_cols
)
row += 1
widget.value_changed.connect(self._input_value_changed)
self._attr_def_id_to_instances[attr_def.id] = attr_instances
self._attr_def_id_to_attr_def[attr_def.id] = attr_def

View file

@ -38,7 +38,7 @@ class DropDataFrame(QtWidgets.QFrame):
}
sequence_types = [
".bgeo", ".vdb"
".bgeo", ".vdb", ".bgeosc", ".bgeogz"
]
def __init__(self, parent):

View file

@ -589,10 +589,12 @@ class AssetsWidget(QtWidgets.QWidget):
view = AssetsView(self)
view.setModel(proxy)
header_widget = QtWidgets.QWidget(self)
current_asset_icon = qtawesome.icon(
"fa.arrow-down", color=get_default_tools_icon_color()
)
current_asset_btn = QtWidgets.QPushButton(self)
current_asset_btn = QtWidgets.QPushButton(header_widget)
current_asset_btn.setIcon(current_asset_icon)
current_asset_btn.setToolTip("Go to Asset from current Session")
# Hide by default
@ -601,25 +603,35 @@ class AssetsWidget(QtWidgets.QWidget):
refresh_icon = qtawesome.icon(
"fa.refresh", color=get_default_tools_icon_color()
)
refresh_btn = QtWidgets.QPushButton(self)
refresh_btn = QtWidgets.QPushButton(header_widget)
refresh_btn.setIcon(refresh_icon)
refresh_btn.setToolTip("Refresh items")
filter_input = PlaceholderLineEdit(self)
filter_input = PlaceholderLineEdit(header_widget)
filter_input.setPlaceholderText("Filter assets..")
# Header
header_layout = QtWidgets.QHBoxLayout()
header_layout = QtWidgets.QHBoxLayout(header_widget)
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.addWidget(filter_input)
header_layout.addWidget(current_asset_btn)
header_layout.addWidget(refresh_btn)
# Make header widgets expand vertically if there is a place
for widget in (
current_asset_btn,
refresh_btn,
filter_input,
):
size_policy = widget.sizePolicy()
size_policy.setVerticalPolicy(size_policy.MinimumExpanding)
widget.setSizePolicy(size_policy)
# Layout
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
layout.addLayout(header_layout)
layout.addWidget(view)
layout.addWidget(header_widget, 0)
layout.addWidget(view, 1)
# Signals/Slots
filter_input.textChanged.connect(self._on_filter_text_change)
@ -630,6 +642,8 @@ class AssetsWidget(QtWidgets.QWidget):
current_asset_btn.clicked.connect(self._on_current_asset_click)
view.doubleClicked.connect(self.double_clicked)
self._header_widget = header_widget
self._filter_input = filter_input
self._refresh_btn = refresh_btn
self._current_asset_btn = current_asset_btn
self._model = model
@ -637,8 +651,14 @@ class AssetsWidget(QtWidgets.QWidget):
self._view = view
self._last_project_name = None
self._last_btns_height = None
self.model_selection = {}
@property
def header_widget(self):
return self._header_widget
def _create_source_model(self):
model = AssetModel(dbcon=self.dbcon, parent=self)
model.refreshed.connect(self._on_model_refresh)
@ -669,6 +689,7 @@ class AssetsWidget(QtWidgets.QWidget):
This separation gives ability to override this method and use it
in differnt way.
"""
self.set_current_session_asset()
def set_current_session_asset(self):
@ -681,6 +702,7 @@ class AssetsWidget(QtWidgets.QWidget):
Some tools may have their global refresh button or do not support
refresh at all.
"""
if visible is None:
visible = not self._refresh_btn.isVisible()
self._refresh_btn.setVisible(visible)
@ -690,6 +712,7 @@ class AssetsWidget(QtWidgets.QWidget):
Not all tools support using of current context asset.
"""
if visible is None:
visible = not self._current_asset_btn.isVisible()
self._current_asset_btn.setVisible(visible)
@ -723,6 +746,7 @@ class AssetsWidget(QtWidgets.QWidget):
so if you're modifying model keep in mind that this method should be
called when refresh is done.
"""
self._proxy.sort(0)
self._set_loading_state(loading=False, empty=not has_item)
self.refreshed.emit()
@ -767,6 +791,7 @@ class SingleSelectAssetsWidget(AssetsWidget):
Contain single selection specific api methods.
"""
def get_selected_asset_id(self):
"""Currently selected asset id."""
selection_model = self._view.selectionModel()

View file

@ -151,7 +151,7 @@ class FilesModel(QtGui.QStandardItemModel):
item = QtGui.QStandardItem()
item_id = str(uuid.uuid4())
item.setData(item_id, ITEM_ID_ROLE)
item.setData(file_item.label, ITEM_LABEL_ROLE)
item.setData(file_item.label or "< empty >", ITEM_LABEL_ROLE)
item.setData(file_item.filenames, FILENAMES_ROLE)
item.setData(file_item.directory, DIRPATH_ROLE)
item.setData(icon_pixmap, ITEM_ICON_ROLE)
@ -251,7 +251,7 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel):
class ItemWidget(QtWidgets.QWidget):
split_requested = QtCore.Signal(str)
context_menu_requested = QtCore.Signal(QtCore.QPoint)
def __init__(
self, item_id, label, pixmap_icon, is_sequence, multivalue, parent=None
@ -316,19 +316,9 @@ class ItemWidget(QtWidgets.QWidget):
self._update_btn_size()
def _on_actions_clicked(self):
menu = QtWidgets.QMenu(self._split_btn)
action = QtWidgets.QAction("Split sequence", menu)
action.triggered.connect(self._on_split_sequence)
menu.addAction(action)
pos = self._split_btn.rect().bottomLeft()
point = self._split_btn.mapToGlobal(pos)
menu.popup(point)
def _on_split_sequence(self):
self.split_requested.emit(self._item_id)
self.context_menu_requested.emit(point)
class InViewButton(IconButton):
@ -339,6 +329,7 @@ class FilesView(QtWidgets.QListView):
"""View showing instances and their groups."""
remove_requested = QtCore.Signal()
context_menu_requested = QtCore.Signal(QtCore.QPoint)
def __init__(self, *args, **kwargs):
super(FilesView, self).__init__(*args, **kwargs)
@ -347,6 +338,7 @@ class FilesView(QtWidgets.QListView):
self.setSelectionMode(
QtWidgets.QAbstractItemView.ExtendedSelection
)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
remove_btn = InViewButton(self)
pix_enabled = paint_image_with_color(
@ -361,6 +353,7 @@ class FilesView(QtWidgets.QListView):
remove_btn.setEnabled(False)
remove_btn.clicked.connect(self._on_remove_clicked)
self.customContextMenuRequested.connect(self._on_context_menu_request)
self._remove_btn = remove_btn
@ -397,6 +390,12 @@ class FilesView(QtWidgets.QListView):
selected_item_ids.add(instance_id)
return selected_item_ids
def has_selected_sequence(self):
for index in self.selectionModel().selectedIndexes():
if index.data(IS_SEQUENCE_ROLE):
return True
return False
def event(self, event):
if event.type() == QtCore.QEvent.KeyPress:
if (
@ -408,6 +407,12 @@ class FilesView(QtWidgets.QListView):
return super(FilesView, self).event(event)
def _on_context_menu_request(self, pos):
index = self.indexAt(pos)
if index.isValid():
point = self.viewport().mapToGlobal(pos)
self.context_menu_requested.emit(point)
def _on_selection_change(self):
self._remove_btn.setEnabled(self.has_selected_item_ids())
@ -456,6 +461,9 @@ class FilesWidget(QtWidgets.QFrame):
files_proxy_model.rowsInserted.connect(self._on_rows_inserted)
files_proxy_model.rowsRemoved.connect(self._on_rows_removed)
files_view.remove_requested.connect(self._on_remove_requested)
files_view.context_menu_requested.connect(
self._on_context_menu_requested
)
self._in_set_value = False
self._single_item = single_item
self._multivalue = False
@ -504,7 +512,9 @@ class FilesWidget(QtWidgets.QFrame):
return file_items
if file_items:
return file_items[0]
return FileDefItem.create_empty_item()
empty_item = FileDefItem.create_empty_item()
return empty_item.to_dict()
def set_filters(self, folders_allowed, exts_filter):
self._files_proxy_model.set_allow_folders(folders_allowed)
@ -527,7 +537,9 @@ class FilesWidget(QtWidgets.QFrame):
is_sequence,
self._multivalue
)
widget.split_requested.connect(self._on_split_request)
widget.context_menu_requested.connect(
self._on_context_menu_requested
)
self._files_view.setIndexWidget(index, widget)
self._files_proxy_model.setData(
index, widget.sizeHint(), QtCore.Qt.SizeHintRole
@ -559,17 +571,22 @@ class FilesWidget(QtWidgets.QFrame):
if not self._in_set_value:
self.value_changed.emit()
def _on_split_request(self, item_id):
def _on_split_request(self):
if self._multivalue:
return
file_item = self._files_model.get_file_item_by_id(item_id)
if not file_item:
item_ids = self._files_view.get_selected_item_ids()
if not item_ids:
return
new_items = file_item.split_sequence()
self._remove_item_by_ids([item_id])
self._add_filepaths(new_items)
for item_id in item_ids:
file_item = self._files_model.get_file_item_by_id(item_id)
if not file_item:
return
new_items = file_item.split_sequence()
self._add_filepaths(new_items)
self._remove_item_by_ids(item_ids)
def _on_remove_requested(self):
if self._multivalue:
@ -579,6 +596,23 @@ class FilesWidget(QtWidgets.QFrame):
if items_to_delete:
self._remove_item_by_ids(items_to_delete)
def _on_context_menu_requested(self, pos):
if self._multivalue:
return
menu = QtWidgets.QMenu(self._files_view)
if self._files_view.has_selected_sequence():
split_action = QtWidgets.QAction("Split sequence", menu)
split_action.triggered.connect(self._on_split_request)
menu.addAction(split_action)
remove_action = QtWidgets.QAction("Remove", menu)
remove_action.triggered.connect(self._on_remove_requested)
menu.addAction(remove_action)
menu.popup(pos)
def sizeHint(self):
# Get size hints of widget and visible widgets
result = super(FilesWidget, self).sizeHint()

View file

@ -91,6 +91,8 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
layout.deleteLater()
new_layout = QtWidgets.QGridLayout()
new_layout.setColumnStretch(0, 0)
new_layout.setColumnStretch(1, 1)
self.setLayout(new_layout)
def set_attr_defs(self, attr_defs):

View file

@ -1,494 +0,0 @@
import os
import getpass
import platform
from Qt import QtCore, QtGui, QtWidgets
from avalon import style
import ftrack_api
class Project_name_getUI(QtWidgets.QWidget):
'''
Project setting ui: here all the neceserry ui widgets are created
they are going to be used i later proces for dynamic linking of project
in list to project's attributes
'''
def __init__(self, parent=None):
super(Project_name_getUI, self).__init__(parent)
self.platform = platform.system()
self.new_index = 0
# get projects from ftrack
self.session = ftrack_api.Session()
self.projects_from_ft = self.session.query(
'Project where status is active')
self.disks_from_ft = self.session.query('Disk')
self.schemas_from_ft = self.session.query('ProjectSchema')
self.projects = self._get_projects_ftrack()
# define window geometry
self.setWindowTitle('Set project attributes')
self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
self.resize(550, 340)
self.setStyleSheet(style.load_stylesheet())
# define disk combobox widget
self.disks = self._get_all_disks()
self.disk_combobox_label = QtWidgets.QLabel('Destination storage:')
self.disk_combobox = QtWidgets.QComboBox()
# define schema combobox widget
self.schemas = self._get_all_schemas()
self.schema_combobox_label = QtWidgets.QLabel('Project schema:')
self.schema_combobox = QtWidgets.QComboBox()
# define fps widget
self.fps_label = QtWidgets.QLabel('Fps:')
self.fps_label.setAlignment(
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.fps = QtWidgets.QLineEdit()
# define project dir widget
self.project_dir_label = QtWidgets.QLabel('Project dir:')
self.project_dir_label.setAlignment(
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.project_dir = QtWidgets.QLineEdit()
self.project_path_label = QtWidgets.QLabel(
'Project_path (if not then created):')
self.project_path_label.setAlignment(
QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
project_path_font = QtGui.QFont(
"Helvetica [Cronyx]", 12, QtGui.QFont.Bold)
self.project_path = QtWidgets.QLabel()
self.project_path.setObjectName('nom_plan_label')
self.project_path.setStyleSheet(
'QtWidgets.QLabel#nom_plan_label {color: red}')
self.project_path.setAlignment(
QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
self.project_path.setFont(project_path_font)
# define handles widget
self.handles_label = QtWidgets.QLabel('Handles:')
self.handles_label.setAlignment(
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.handles = QtWidgets.QLineEdit()
# define resolution widget
self.resolution_w_label = QtWidgets.QLabel('W:')
self.resolution_w = QtWidgets.QLineEdit()
self.resolution_h_label = QtWidgets.QLabel('H:')
self.resolution_h = QtWidgets.QLineEdit()
devider = QtWidgets.QFrame()
# devider.Shape(QFrame.HLine)
devider.setFrameShape(QtWidgets.QFrame.HLine)
devider.setFrameShadow(QtWidgets.QFrame.Sunken)
self.generate_lines()
# define push buttons
self.set_pushbutton = QtWidgets.QPushButton('Set project')
self.cancel_pushbutton = QtWidgets.QPushButton('Cancel')
# definition of layouts
############################################
action_layout = QtWidgets.QHBoxLayout()
action_layout.addWidget(self.set_pushbutton)
action_layout.addWidget(self.cancel_pushbutton)
# schema property
schema_layout = QtWidgets.QGridLayout()
schema_layout.addWidget(self.schema_combobox, 0, 1)
schema_layout.addWidget(self.schema_combobox_label, 0, 0)
# storage property
storage_layout = QtWidgets.QGridLayout()
storage_layout.addWidget(self.disk_combobox, 0, 1)
storage_layout.addWidget(self.disk_combobox_label, 0, 0)
# fps property
fps_layout = QtWidgets.QGridLayout()
fps_layout.addWidget(self.fps, 1, 1)
fps_layout.addWidget(self.fps_label, 1, 0)
# project dir property
project_dir_layout = QtWidgets.QGridLayout()
project_dir_layout.addWidget(self.project_dir, 1, 1)
project_dir_layout.addWidget(self.project_dir_label, 1, 0)
# project path property
project_path_layout = QtWidgets.QGridLayout()
spacer_1_item = QtWidgets.QSpacerItem(10, 10)
project_path_layout.addItem(spacer_1_item, 0, 1)
project_path_layout.addWidget(self.project_path_label, 1, 1)
project_path_layout.addWidget(self.project_path, 2, 1)
spacer_2_item = QtWidgets.QSpacerItem(20, 20)
project_path_layout.addItem(spacer_2_item, 3, 1)
# handles property
handles_layout = QtWidgets.QGridLayout()
handles_layout.addWidget(self.handles, 1, 1)
handles_layout.addWidget(self.handles_label, 1, 0)
# resolution property
resolution_layout = QtWidgets.QGridLayout()
resolution_layout.addWidget(self.resolution_w_label, 1, 1)
resolution_layout.addWidget(self.resolution_w, 2, 1)
resolution_layout.addWidget(self.resolution_h_label, 1, 2)
resolution_layout.addWidget(self.resolution_h, 2, 2)
# form project property layout
p_layout = QtWidgets.QGridLayout()
p_layout.addLayout(storage_layout, 1, 0)
p_layout.addLayout(schema_layout, 2, 0)
p_layout.addLayout(project_dir_layout, 3, 0)
p_layout.addLayout(fps_layout, 4, 0)
p_layout.addLayout(handles_layout, 5, 0)
p_layout.addLayout(resolution_layout, 6, 0)
p_layout.addWidget(devider, 7, 0)
spacer_item = QtWidgets.QSpacerItem(
150,
40,
QtWidgets.QSizePolicy.Minimum,
QtWidgets.QSizePolicy.Expanding
)
p_layout.addItem(spacer_item, 8, 0)
# form with list to one layout with project property
list_layout = QtWidgets.QGridLayout()
list_layout.addLayout(p_layout, 1, 0)
list_layout.addWidget(self.listWidget, 1, 1)
root_layout = QtWidgets.QVBoxLayout()
root_layout.addLayout(project_path_layout)
root_layout.addWidget(devider)
root_layout.addLayout(list_layout)
root_layout.addLayout(action_layout)
self.setLayout(root_layout)
def generate_lines(self):
'''
Will generate lines of project list
'''
self.listWidget = QtWidgets.QListWidget()
for self.index, p in enumerate(self.projects):
item = QtWidgets.QListWidgetItem("{full_name}".format(**p))
# item.setSelected(False)
self.listWidget.addItem(item)
print(self.listWidget.indexFromItem(item))
# self.listWidget.setCurrentItem(self.listWidget.itemFromIndex(1))
# add options to schemas widget
self.schema_combobox.addItems(self.schemas)
# add options to disk widget
self.disk_combobox.addItems(self.disks)
# populate content of project info widgets
self.projects[1] = self._fill_project_attributes_widgets(p, None)
def _fill_project_attributes_widgets(self, p=None, index=None):
'''
will generate actual informations wich are saved on ftrack
'''
if index is None:
self.new_index = 1
if not p:
pass
# change schema selection
for i, schema in enumerate(self.schemas):
if p['project_schema']['name'] in schema:
break
self.schema_combobox.setCurrentIndex(i)
disk_name, disk_path = self._build_disk_path()
for i, disk in enumerate(self.disks):
if disk_name in disk:
break
# change disk selection
self.disk_combobox.setCurrentIndex(i)
# change project_dir selection
if "{root}".format(**p):
self.project_dir.setPlaceholderText("{root}".format(**p))
else:
print("not root so it was replaced with name")
self.project_dir.setPlaceholderText("{name}".format(**p))
p['root'] = p['name']
# set project path to show where it will be created
self.project_path.setText(
os.path.join(self.disks[i].split(' ')[-1],
self.project_dir.text()))
# change fps selection
self.fps.setPlaceholderText("{custom_attributes[fps]}".format(**p))
# change handles selection
self.handles.setPlaceholderText(
"{custom_attributes[handles]}".format(**p))
# change resolution selection
self.resolution_w.setPlaceholderText(
"{custom_attributes[resolution_width]}".format(**p))
self.resolution_h.setPlaceholderText(
"{custom_attributes[resolution_height]}".format(**p))
self.update_disk()
return p
def fix_project_path_literals(self, dir):
return dir.replace(' ', '_').lower()
def update_disk(self):
disk = self.disk_combobox.currentText().split(' ')[-1]
dir = self.project_dir.text()
if not dir:
dir = "{root}".format(**self.projects[self.new_index])
self.projects[self.new_index]['project_path'] = os.path.normpath(
self.fix_project_path_literals(os.path.join(disk, dir)))
else:
self.projects[self.new_index]['project_path'] = os.path.normpath(
self.fix_project_path_literals(os.path.join(disk, dir)))
self.projects[self.new_index]['disk'] = self.disks_from_ft[
self.disk_combobox.currentIndex()]
self.projects[self.new_index]['disk_id'] = self.projects[
self.new_index]['disk']['id']
# set project path to show where it will be created
self.project_path.setText(
self.projects[self.new_index]['project_path'])
def update_resolution(self):
# update all values in resolution
if self.resolution_w.text():
self.projects[self.new_index]['custom_attributes'][
"resolutionWidth"] = int(self.resolution_w.text())
if self.resolution_h.text():
self.projects[self.new_index]['custom_attributes'][
"resolutionHeight"] = int(self.resolution_h.text())
def _update_attributes_by_list_selection(self):
# generate actual selection index
self.new_index = self.listWidget.currentRow()
self.project_dir.setText('')
self.fps.setText('')
self.handles.setText('')
self.resolution_w.setText('')
self.resolution_h.setText('')
# update project properities widgets and write changes
# into project dictionaries
self.projects[self.new_index] = self._fill_project_attributes_widgets(
self.projects[self.new_index], self.new_index)
self.update_disk()
def _build_disk_path(self):
if self.platform == "Windows":
print(self.projects[self.index].keys())
print(self.projects[self.new_index]['disk'])
return self.projects[self.new_index]['disk'][
'name'], self.projects[self.new_index]['disk']['windows']
else:
return self.projects[self.new_index]['disk'][
'name'], self.projects[self.new_index]['disk']['unix']
def _get_all_schemas(self):
schemas_list = []
for s in self.schemas_from_ft:
# print d.keys()
# if 'Pokus' in s['name']:
# continue
schemas_list.append('{}'.format(s['name']))
print("\nschemas in ftrack: {}\n".format(schemas_list))
return schemas_list
def _get_all_disks(self):
disks_list = []
for d in self.disks_from_ft:
# print d.keys()
if self.platform == "Windows":
if 'Local drive' in d['name']:
d['windows'] = os.path.join(d['windows'],
os.getenv('USERNAME')
or os.getenv('USER')
or os.getenv('LOGNAME'))
disks_list.append('"{}" at {}'.format(d['name'], d['windows']))
else:
if 'Local drive' in d['name']:
d['unix'] = os.path.join(d['unix'], getpass.getuser())
disks_list.append('"{}" at {}'.format(d['name'], d['unix']))
return disks_list
def _get_projects_ftrack(self):
projects_lst = []
for project in self.projects_from_ft:
# print project.keys()
projects_dict = {}
for k in project.keys():
''' # TODO: delete this in production version '''
# if 'test' not in project['name']:
# continue
# print '{}: {}\n'.format(k, project[k])
if '_link' == k:
# print project[k]
content = project[k]
for kc in content[0].keys():
if content[0]['name']:
content[0][kc] = content[0][kc].encode(
'ascii', 'ignore').decode('ascii')
print('{}: {}\n'.format(kc, content[0][kc]))
projects_dict[k] = content
print(project[k])
print(projects_dict[k])
elif 'root' == k:
print('{}: {}\n'.format(k, project[k]))
projects_dict[k] = project[k]
elif 'disk' == k:
print('{}: {}\n'.format(k, project[k]))
projects_dict[k] = project[k]
elif 'name' == k:
print('{}: {}\n'.format(k, project[k]))
projects_dict[k] = project[k].encode(
'ascii', 'ignore').decode('ascii')
elif 'disk_id' == k:
print('{}: {}\n'.format(k, project[k]))
projects_dict[k] = project[k]
elif 'id' == k:
print('{}: {}\n'.format(k, project[k]))
projects_dict[k] = project[k]
elif 'full_name' == k:
print('{}: {}\n'.format(k, project[k]))
projects_dict[k] = project[k].encode(
'ascii', 'ignore').decode('ascii')
elif 'project_schema_id' == k:
print('{}: {}\n'.format(k, project[k]))
projects_dict[k] = project[k]
elif 'project_schema' == k:
print('{}: {}\n'.format(k, project[k]))
projects_dict[k] = project[k]
elif 'custom_attributes' == k:
print('{}: {}\n'.format(k, project[k]))
projects_dict[k] = project[k]
else:
pass
if projects_dict:
projects_lst.append(projects_dict)
return projects_lst
class Project_name_get(Project_name_getUI):
def __init__(self, parent=None):
super(Project_name_get, self).__init__(parent)
# self.input_project_name.textChanged.connect(self.input_project_name.placeholderText)
self.set_pushbutton.clicked.connect(lambda: self.execute())
self.cancel_pushbutton.clicked.connect(self.close)
self.listWidget.itemSelectionChanged.connect(
self._update_attributes_by_list_selection)
self.disk_combobox.currentIndexChanged.connect(self.update_disk)
self.schema_combobox.currentIndexChanged.connect(self.update_schema)
self.project_dir.textChanged.connect(self.update_disk)
self.fps.textChanged.connect(self.update_fps)
self.handles.textChanged.connect(self.update_handles)
self.resolution_w.textChanged.connect(self.update_resolution)
self.resolution_h.textChanged.connect(self.update_resolution)
def update_handles(self):
self.projects[self.new_index]['custom_attributes']['handles'] = int(
self.handles.text())
def update_fps(self):
self.projects[self.new_index]['custom_attributes']['fps'] = int(
self.fps.text())
def update_schema(self):
self.projects[self.new_index]['project_schema'] = self.schemas_from_ft[
self.schema_combobox.currentIndex()]
self.projects[self.new_index]['project_schema_id'] = self.projects[
self.new_index]['project_schema']['id']
def execute(self):
# import ft_utils
# import hiero
# get the project which has been selected
print("well and what")
# set the project as context and create entity
# entity is task created with the name of user which is creating it
# get the project_path and create dir if there is not any
print(self.projects[self.new_index]['project_path'].replace(
self.disk_combobox.currentText().split(' ')[-1].lower(), ''))
# get the schema and recreate a starting project regarding the selection
# set_hiero_template(project_schema=self.projects[self.new_index][
# 'project_schema']['name'])
# set all project properities
# project = hiero.core.Project()
# project.setFramerate(
# int(self.projects[self.new_index]['custom_attributes']['fps']))
# project.projectRoot()
# print 'handles: {}'.format(self.projects[self.new_index]['custom_attributes']['handles'])
# print 'resolution_width: {}'.format(self.projects[self.new_index]['custom_attributes']["resolutionWidth"])
# print 'resolution_width: {}'.format(self.projects[self.new_index]['custom_attributes']["resolutionHeight"])
# print "<< {}".format(self.projects[self.new_index])
# get path for the hrox file
# root = context.data('ftrackData')['Project']['root']
# hrox_script_path = ft_utils.getPathsYaml(taskid, templateList=templates, root=root)
# save the hrox into the correct path
self.session.commit()
self.close()
#
# def set_hiero_template(project_schema=None):
# import hiero
# hiero.core.closeAllProjects()
# hiero_plugin_path = [
# p for p in os.environ['HIERO_PLUGIN_PATH'].split(';')
# if 'hiero_plugin_path' in p
# ][0]
# path = os.path.normpath(
# os.path.join(hiero_plugin_path, 'Templates', project_schema + '.hrox'))
# print('---> path to template: {}'.format(path))
# return hiero.core.openProject(path)
# def set_out_ft_session():
# session = ftrack_api.Session()
# projects_to_ft = session.query('Project where status is active')
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
panel = Project_name_get()
panel.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

81
schema/session-3.0.json Normal file
View file

@ -0,0 +1,81 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "openpype:session-3.0",
"description": "The Avalon environment",
"type": "object",
"additionalProperties": true,
"required": [
"AVALON_PROJECT",
"AVALON_ASSET"
],
"properties": {
"AVALON_PROJECTS": {
"description": "Absolute path to root of project directories",
"type": "string",
"example": "/nas/projects"
},
"AVALON_PROJECT": {
"description": "Name of project",
"type": "string",
"pattern": "^\\w*$",
"example": "Hulk"
},
"AVALON_ASSET": {
"description": "Name of asset",
"type": "string",
"pattern": "^\\w*$",
"example": "Bruce"
},
"AVALON_TASK": {
"description": "Name of task",
"type": "string",
"pattern": "^\\w*$",
"example": "modeling"
},
"AVALON_APP": {
"description": "Name of host",
"type": "string",
"pattern": "^\\w*$",
"example": "maya2016"
},
"AVALON_DB": {
"description": "Name of database",
"type": "string",
"pattern": "^\\w*$",
"example": "avalon",
"default": "avalon"
},
"AVALON_LABEL": {
"description": "Nice name of Avalon, used in e.g. graphical user interfaces",
"type": "string",
"example": "Mindbender",
"default": "Avalon"
},
"AVALON_TIMEOUT": {
"description": "Wherever there is a need for a timeout, this is the default value.",
"type": "string",
"pattern": "^[0-9]*$",
"default": "1000",
"example": "1000"
},
"AVALON_INSTANCE_ID": {
"description": "Unique identifier for instances in a working file",
"type": "string",
"pattern": "^[\\w.]*$",
"default": "avalon.instance",
"example": "avalon.instance"
},
"AVALON_CONTAINER_ID": {
"description": "Unique identifier for a loaded representation in a working file",
"type": "string",
"pattern": "^[\\w.]*$",
"default": "avalon.container",
"example": "avalon.container"
}
}
}

View file

@ -106,7 +106,9 @@ install_requires = [
"dns",
# Python defaults (cx_Freeze skip them by default)
"dbm",
"sqlite3"
"sqlite3",
"dataclasses",
"timeit"
]
includes = []

105
start.py
View file

@ -897,6 +897,56 @@ def _bootstrap_from_code(use_version, use_staging):
return version_path
def _boot_validate_versions(use_version, local_version):
_print(f">>> Validating version [ {use_version} ]")
openpype_versions = bootstrap.find_openpype(include_zips=True,
staging=True)
openpype_versions += bootstrap.find_openpype(include_zips=True,
staging=False)
v: OpenPypeVersion
found = [v for v in openpype_versions if str(v) == use_version]
if not found:
_print(f"!!! Version [ {use_version} ] not found.")
list_versions(openpype_versions, local_version)
sys.exit(1)
# print result
version_path = bootstrap.get_version_path_from_list(
use_version, openpype_versions
)
valid, message = bootstrap.validate_openpype_version(version_path)
_print("{}{}".format(">>> " if valid else "!!! ", message))
def _boot_print_versions(use_staging, local_version, openpype_root):
if not use_staging:
_print("--- This will list only non-staging versions detected.")
_print(" To see staging versions, use --use-staging argument.")
else:
_print("--- This will list only staging versions detected.")
_print(" To see other version, omit --use-staging argument.")
openpype_versions = bootstrap.find_openpype(include_zips=True,
staging=use_staging)
if getattr(sys, 'frozen', False):
local_version = bootstrap.get_version(Path(openpype_root))
else:
local_version = OpenPypeVersion.get_installed_version_str()
list_versions(openpype_versions, local_version)
def _boot_handle_missing_version(local_version, use_staging, message):
_print(message)
if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1":
openpype_versions = bootstrap.find_openpype(
include_zips=True, staging=use_staging
)
list_versions(openpype_versions, local_version)
else:
igniter.show_message_dialog("Version not found", message)
def boot():
"""Bootstrap OpenPype."""
@ -966,30 +1016,7 @@ def boot():
local_version = OpenPypeVersion.get_installed_version_str()
if "validate" in commands:
_print(f">>> Validating version [ {use_version} ]")
openpype_versions = bootstrap.find_openpype(include_zips=True,
staging=True)
openpype_versions += bootstrap.find_openpype(include_zips=True,
staging=False)
v: OpenPypeVersion
found = [v for v in openpype_versions if str(v) == use_version]
if not found:
_print(f"!!! Version [ {use_version} ] not found.")
list_versions(openpype_versions, local_version)
sys.exit(1)
# print result
result = bootstrap.validate_openpype_version(
bootstrap.get_version_path_from_list(
use_version, openpype_versions))
_print("{}{}".format(
">>> " if result[0] else "!!! ",
bootstrap.validate_openpype_version(
bootstrap.get_version_path_from_list(
use_version, openpype_versions)
)[1])
)
_boot_validate_versions(use_version, local_version)
sys.exit(1)
if not openpype_path:
@ -999,21 +1026,7 @@ def boot():
os.environ["OPENPYPE_PATH"] = openpype_path
if "print_versions" in commands:
if not use_staging:
_print("--- This will list only non-staging versions detected.")
_print(" To see staging versions, use --use-staging argument.")
else:
_print("--- This will list only staging versions detected.")
_print(" To see other version, omit --use-staging argument.")
_openpype_root = OPENPYPE_ROOT
openpype_versions = bootstrap.find_openpype(include_zips=True,
staging=use_staging)
if getattr(sys, 'frozen', False):
local_version = bootstrap.get_version(Path(_openpype_root))
else:
local_version = OpenPypeVersion.get_installed_version_str()
list_versions(openpype_versions, local_version)
_boot_print_versions(use_staging, local_version, OPENPYPE_ROOT)
sys.exit(1)
# ------------------------------------------------------------------------
@ -1026,12 +1039,7 @@ def boot():
try:
version_path = _find_frozen_openpype(use_version, use_staging)
except OpenPypeVersionNotFound as exc:
message = str(exc)
_print(message)
if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1":
list_versions(openpype_versions, local_version)
else:
igniter.show_message_dialog("Version not found", message)
_boot_handle_missing_version(local_version, use_staging, str(exc))
sys.exit(1)
except RuntimeError as e:
@ -1050,12 +1058,7 @@ def boot():
version_path = _bootstrap_from_code(use_version, use_staging)
except OpenPypeVersionNotFound as exc:
message = str(exc)
_print(message)
if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1":
list_versions(openpype_versions, local_version)
else:
igniter.show_message_dialog("Version not found", message)
_boot_handle_missing_version(local_version, use_staging, str(exc))
sys.exit(1)
# set this to point either to `python` from venv in case of live code

View file

@ -153,7 +153,7 @@ class ModuleUnitTest(BaseTest):
Database prepared from dumps with 'db_setup' fixture.
"""
from avalon.api import AvalonMongoDB
from openpype.pipeline import AvalonMongoDB
dbcon = AvalonMongoDB()
dbcon.Session["AVALON_PROJECT"] = self.TEST_PROJECT_NAME
yield dbcon