[Automated] Merged develop into main

This commit is contained in:
pypebot 2022-02-23 04:28:32 +01:00 committed by GitHub
commit e11c9f123c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1154 additions and 3489 deletions

View file

@ -17,11 +17,12 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
"nuke",
"nukex",
"hiero",
"houdini",
"nukestudio",
"blender",
"photoshop",
"tvpaint",
"afftereffects"
"aftereffects"
]
def execute(self):

View file

@ -19,7 +19,7 @@ class CopyTemplateWorkfile(PreLaunchHook):
# Before `AddLastWorkfileToLaunchArgs`
order = 0
app_groups = ["blender", "photoshop", "tvpaint", "afftereffects"]
app_groups = ["blender", "photoshop", "tvpaint", "aftereffects"]
def execute(self):
"""Check if can copy template for context and do it if possible.
@ -44,7 +44,7 @@ class CopyTemplateWorkfile(PreLaunchHook):
return
if os.path.exists(last_workfile):
self.log.debug("Last workfile exits. Skipping {} process.".format(
self.log.debug("Last workfile exists. Skipping {} process.".format(
self.__class__.__name__
))
return
@ -120,7 +120,7 @@ class CopyTemplateWorkfile(PreLaunchHook):
f"Creating workfile from template: \"{template_path}\""
)
# Copy template workfile to new destinantion
# Copy template workfile to new destination
shutil.copy2(
os.path.normpath(template_path),
os.path.normpath(last_workfile)

View file

@ -527,6 +527,7 @@ def get_segment_attributes(segment):
# Add timeline segment to tree
clip_data = {
"shot_name": segment.shot_name.get_value(),
"segment_name": segment.name.get_value(),
"segment_comment": segment.comment.get_value(),
"tape_name": segment.tape_name,

View file

@ -361,6 +361,7 @@ class PublishableClip:
vertical_sync_default = False
driving_layer_default = ""
index_from_segment_default = False
use_shot_name_default = False
def __init__(self, segment, **kwargs):
self.rename_index = kwargs["rename_index"]
@ -376,6 +377,7 @@ class PublishableClip:
# segment (clip) main attributes
self.cs_name = self.clip_data["segment_name"]
self.cs_index = int(self.clip_data["segment"])
self.shot_name = self.clip_data["shot_name"]
# get track name and index
self.track_index = int(self.clip_data["track"])
@ -419,18 +421,21 @@ class PublishableClip:
# deal with clip name
new_name = self.marker_data.pop("newClipName")
if self.rename:
if self.rename and not self.use_shot_name:
# rename segment
self.current_segment.name = str(new_name)
self.marker_data["asset"] = str(new_name)
elif self.use_shot_name:
self.marker_data["asset"] = self.shot_name
self.marker_data["hierarchyData"]["shot"] = self.shot_name
else:
self.marker_data["asset"] = self.cs_name
self.marker_data["hierarchyData"]["shot"] = self.cs_name
if self.marker_data["heroTrack"] and self.review_layer:
self.marker_data.update({"reviewTrack": self.review_layer})
self.marker_data["reviewTrack"] = self.review_layer
else:
self.marker_data.update({"reviewTrack": None})
self.marker_data["reviewTrack"] = None
# create pype tag on track_item and add data
fpipeline.imprint(self.current_segment, self.marker_data)
@ -463,6 +468,8 @@ class PublishableClip:
# ui_inputs data or default values if gui was not used
self.rename = self.ui_inputs.get(
"clipRename", {}).get("value") or self.rename_default
self.use_shot_name = self.ui_inputs.get(
"useShotName", {}).get("value") or self.use_shot_name_default
self.clip_name = self.ui_inputs.get(
"clipName", {}).get("value") or self.clip_name_default
self.hierarchy = self.ui_inputs.get(

View file

@ -87,41 +87,48 @@ class CreateShotClip(opfapi.Creator):
"target": "tag",
"toolTip": "Parents folder for shot root folder, Template filled with `Hierarchy Data` section", # noqa
"order": 0},
"useShotName": {
"value": True,
"type": "QCheckBox",
"label": "Use Shot Name",
"target": "ui",
"toolTip": "Use name form Shot name clip attribute", # noqa
"order": 1},
"clipRename": {
"value": False,
"type": "QCheckBox",
"label": "Rename clips",
"target": "ui",
"toolTip": "Renaming selected clips on fly", # noqa
"order": 1},
"order": 2},
"clipName": {
"value": "{sequence}{shot}",
"type": "QLineEdit",
"label": "Clip Name Template",
"target": "ui",
"toolTip": "template for creating shot namespaused for renaming (use rename: on)", # noqa
"order": 2},
"order": 3},
"segmentIndex": {
"value": True,
"type": "QCheckBox",
"label": "Segment index",
"target": "ui",
"toolTip": "Take number from segment index", # noqa
"order": 3},
"order": 4},
"countFrom": {
"value": 10,
"type": "QSpinBox",
"label": "Count sequence from",
"target": "ui",
"toolTip": "Set when the sequence number stafrom", # noqa
"order": 4},
"order": 5},
"countSteps": {
"value": 10,
"type": "QSpinBox",
"label": "Stepping number",
"target": "ui",
"toolTip": "What number is adding every new step", # noqa
"order": 5},
"order": 6},
}
},
"hierarchyData": {

View file

@ -542,3 +542,37 @@ def maintained_selection():
if previous_selection:
for node in previous_selection:
node.setSelected(on=True)
def reset_framerange():
"""Set frame range to current asset"""
asset_name = api.Session["AVALON_ASSET"]
asset = io.find_one({"name": asset_name, "type": "asset"})
frame_start = asset["data"].get("frameStart")
frame_end = asset["data"].get("frameEnd")
# Backwards compatibility
if frame_start is None or frame_end is None:
frame_start = asset["data"].get("edit_in")
frame_end = asset["data"].get("edit_out")
if frame_start is None or frame_end is None:
log.warning("No edit information found for %s" % asset_name)
return
handles = asset["data"].get("handles") or 0
handle_start = asset["data"].get("handleStart")
if handle_start is None:
handle_start = handles
handle_end = asset["data"].get("handleEnd")
if handle_end is None:
handle_end = handles
frame_start -= int(handle_start)
frame_end += int(handle_end)
hou.playbar.setFrameRange(frame_start, frame_end)
hou.playbar.setPlaybackRange(frame_start, frame_end)
hou.setFrame(frame_start)

View file

@ -4,6 +4,7 @@ import logging
import contextlib
import hou
import hdefereval
import pyblish.api
import avalon.api
@ -66,9 +67,10 @@ def install():
sys.path.append(hou_pythonpath)
# Set asset FPS for the empty scene directly after launch of Houdini
# so it initializes into the correct scene FPS
_set_asset_fps()
# Set asset settings for the empty scene directly after launch of Houdini
# so it initializes into the correct scene FPS, Frame Range, etc.
# todo: make sure this doesn't trigger when opening with last workfile
_set_context_settings()
def uninstall():
@ -279,18 +281,49 @@ def on_open(*args):
def on_new(_):
"""Set project resolution and fps when create a new file"""
if hou.hipFile.isLoadingHipFile():
# This event also triggers when Houdini opens a file due to the
# new event being registered to 'afterClear'. As such we can skip
# 'new' logic if the user is opening a file anyway
log.debug("Skipping on new callback due to scene being opened.")
return
log.info("Running callback on new..")
_set_asset_fps()
_set_context_settings()
# It seems that the current frame always gets reset to frame 1 on
# new scene. So we enforce current frame to be at the start of the playbar
# with execute deferred
def _enforce_start_frame():
start = hou.playbar.playbackRange()[0]
hou.setFrame(start)
hdefereval.executeDeferred(_enforce_start_frame)
def _set_asset_fps():
"""Set Houdini scene FPS to the default required for current asset"""
def _set_context_settings():
"""Apply the project settings from the project definition
Settings can be overwritten by an asset if the asset.data contains
any information regarding those settings.
Examples of settings:
fps
resolution
renderer
Returns:
None
"""
# Set new scene fps
fps = get_asset_fps()
print("Setting scene FPS to %i" % fps)
lib.set_scene_fps(fps)
lib.reset_framerange()
def on_pyblish_instance_toggled(instance, new_value, old_value):
"""Toggle saver tool passthrough states on instance toggles."""

View file

@ -2,26 +2,14 @@ import pyblish.api
import avalon.api
class SaveCurrentScene(pyblish.api.InstancePlugin):
class SaveCurrentScene(pyblish.api.ContextPlugin):
"""Save current scene"""
label = "Save current file"
order = pyblish.api.IntegratorOrder - 0.49
order = pyblish.api.ExtractorOrder - 0.49
hosts = ["houdini"]
families = ["usdrender",
"redshift_rop"]
targets = ["local"]
def process(self, instance):
# This should be a ContextPlugin, but this is a workaround
# for a bug in pyblish to run once for a family: issue #250
context = instance.context
key = "__hasRun{}".format(self.__class__.__name__)
if context.data.get(key, False):
return
else:
context.data[key] = True
def process(self, context):
# Filename must not have changed since collecting
host = avalon.api.registered_host()

View file

@ -1,23 +0,0 @@
import pyblish.api
class SaveCurrentSceneDeadline(pyblish.api.ContextPlugin):
"""Save current scene"""
label = "Save current file"
order = pyblish.api.IntegratorOrder - 0.49
hosts = ["houdini"]
targets = ["deadline"]
def process(self, context):
import hou
assert (
context.data["currentFile"] == hou.hipFile.path()
), "Collected filename from current scene name."
if hou.hipFile.hasUnsavedChanges():
self.log.info("Saving current file..")
hou.hipFile.save(save_to_recent_files=True)
else:
self.log.debug("No unsaved changes, skipping file save..")

View file

@ -1,77 +0,0 @@
import pyblish.api
class ValidateOutputNode(pyblish.api.InstancePlugin):
"""Validate the instance SOP Output Node.
This will ensure:
- The SOP Path is set.
- The SOP Path refers to an existing object.
- The SOP Path node is a SOP node.
- The SOP Path node has at least one input connection (has an input)
- The SOP Path has geometry data.
"""
order = pyblish.api.ValidatorOrder
families = ["pointcache", "vdbcache"]
hosts = ["houdini"]
label = "Validate Output Node"
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
"Output node(s) `%s` are incorrect. "
"See plug-in log for details." % invalid
)
@classmethod
def get_invalid(cls, instance):
import hou
output_node = instance.data["output_node"]
if output_node is None:
node = instance[0]
cls.log.error(
"SOP Output node in '%s' does not exist. "
"Ensure a valid SOP output path is set." % node.path()
)
return [node.path()]
# Output node must be a Sop node.
if not isinstance(output_node, hou.SopNode):
cls.log.error(
"Output node %s is not a SOP node. "
"SOP Path must point to a SOP node, "
"instead found category type: %s"
% (output_node.path(), output_node.type().category().name())
)
return [output_node.path()]
# For the sake of completeness also assert the category type
# is Sop to avoid potential edge case scenarios even though
# the isinstance check above should be stricter than this category
assert output_node.type().category().name() == "Sop", (
"Output node %s is not of category Sop. This is a bug.."
% output_node.path()
)
# Check if output node has incoming connections
if not output_node.inputConnections():
cls.log.error(
"Output node `%s` has no incoming connections"
% output_node.path()
)
return [output_node.path()]
# Ensure the output node has at least Geometry data
if not output_node.geometry():
cls.log.error(
"Output node `%s` has no geometry data." % output_node.path()
)
return [output_node.path()]

View file

@ -66,6 +66,14 @@ host_tools.show_workfiles(parent)
]]></scriptCode>
</scriptItem>
<scriptItem id="reset_frame_range">
<label>Reset Frame Range</label>
<scriptCode><![CDATA[
import openpype.hosts.houdini.api.lib
openpype.hosts.houdini.api.lib.reset_framerange()
]]></scriptCode>
</scriptItem>
<separatorItem/>
<scriptItem id="experimental_tools">
<label>Experimental tools...</label>

View file

@ -10,12 +10,6 @@ from .pipeline import (
ls,
containerise,
lock,
unlock,
is_locked,
lock_ignored,
)
from .plugin import (
Creator,
@ -38,11 +32,9 @@ from .lib import (
read,
apply_shaders,
without_extension,
maintained_selection,
suspended_refresh,
unique_name,
unique_namespace,
)
@ -54,11 +46,6 @@ __all__ = [
"ls",
"containerise",
"lock",
"unlock",
"is_locked",
"lock_ignored",
"Creator",
"Loader",
@ -76,11 +63,9 @@ __all__ = [
"lsattrs",
"read",
"unique_name",
"unique_namespace",
"apply_shaders",
"without_extension",
"maintained_selection",
"suspended_refresh",

View file

@ -2,7 +2,6 @@
import os
import sys
import re
import platform
import uuid
import math
@ -154,73 +153,59 @@ def maintained_selection():
cmds.select(clear=True)
def unique_name(name, format="%02d", namespace="", prefix="", suffix=""):
"""Return unique `name`
The function takes into consideration an optional `namespace`
and `suffix`. The suffix is included in evaluating whether a
name exists - such as `name` + "_GRP" - but isn't included
in the returned value.
If a namespace is provided, only names within that namespace
are considered when evaluating whether the name is unique.
Arguments:
format (str, optional): The `name` is given a number, this determines
how this number is formatted. Defaults to a padding of 2.
E.g. my_name01, my_name02.
namespace (str, optional): Only consider names within this namespace.
suffix (str, optional): Only consider names with this suffix.
Example:
>>> name = cmds.createNode("transform", name="MyName")
>>> cmds.objExists(name)
True
>>> unique = unique_name(name)
>>> cmds.objExists(unique)
False
"""
iteration = 1
unique = prefix + (name + format % iteration) + suffix
while cmds.objExists(namespace + ":" + unique):
iteration += 1
unique = prefix + (name + format % iteration) + suffix
if suffix:
return unique[:-len(suffix)]
return unique
def unique_namespace(namespace, format="%02d", prefix="", suffix=""):
"""Return unique namespace
Similar to :func:`unique_name` but evaluating namespaces
as opposed to object names.
Arguments:
namespace (str): Name of namespace to consider
format (str, optional): Formatting of the given iteration number
suffix (str, optional): Only consider namespaces with this suffix.
>>> unique_namespace("bar")
# bar01
>>> unique_namespace(":hello")
# :hello01
>>> unique_namespace("bar:", suffix="_NS")
# bar01_NS:
"""
def current_namespace():
current = cmds.namespaceInfo(currentNamespace=True,
absoluteName=True)
# When inside a namespace Maya adds no trailing :
if not current.endswith(":"):
current += ":"
return current
# Always check against the absolute namespace root
# There's no clash with :x if we're defining namespace :a:x
ROOT = ":" if namespace.startswith(":") else current_namespace()
# Strip trailing `:` tokens since we might want to add a suffix
start = ":" if namespace.startswith(":") else ""
end = ":" if namespace.endswith(":") else ""
namespace = namespace.strip(":")
if ":" in namespace:
# Split off any nesting that we don't uniqify anyway.
parents, namespace = namespace.rsplit(":", 1)
start += parents + ":"
ROOT += start
def exists(n):
# Check for clash with nodes and namespaces
fullpath = ROOT + n
return cmds.objExists(fullpath) or cmds.namespace(exists=fullpath)
iteration = 1
unique = prefix + (namespace + format % iteration) + suffix
while True:
nr_namespace = namespace + format % iteration
unique = prefix + nr_namespace + suffix
if not exists(unique):
return start + unique + end
# The `existing` set does not just contain the namespaces but *all* nodes
# within "current namespace". We need all because the namespace could
# also clash with a node name. To be truly unique and valid one needs to
# check against all.
existing = set(cmds.namespaceInfo(listNamespace=True))
while unique in existing:
iteration += 1
unique = prefix + (namespace + format % iteration) + suffix
return unique
def read(node):
@ -286,153 +271,6 @@ def pairwise(iterable):
return itertools.izip(a, a)
def unique(name):
assert isinstance(name, string_types), "`name` must be string"
while cmds.objExists(name):
matches = re.findall(r"\d+$", name)
if matches:
match = matches[-1]
name = name.rstrip(match)
number = int(match) + 1
else:
number = 1
name = name + str(number)
return name
def uv_from_element(element):
"""Return the UV coordinate of given 'element'
Supports components, meshes, nurbs.
"""
supported = ["mesh", "nurbsSurface"]
uv = [0.5, 0.5]
if "." not in element:
type = cmds.nodeType(element)
if type == "transform":
geometry_shape = cmds.listRelatives(element, shapes=True)
if len(geometry_shape) >= 1:
geometry_shape = geometry_shape[0]
else:
return
elif type in supported:
geometry_shape = element
else:
cmds.error("Could not do what you wanted..")
return
else:
# If it is indeed a component - get the current Mesh
try:
parent = element.split(".", 1)[0]
# Maya is funny in that when the transform of the shape
# of the component element has children, the name returned
# by that elementection is the shape. Otherwise, it is
# the transform. So lets see what type we're dealing with here.
if cmds.nodeType(parent) in supported:
geometry_shape = parent
else:
geometry_shape = cmds.listRelatives(parent, shapes=1)[0]
if not geometry_shape:
cmds.error("Skipping %s: Could not find shape." % element)
return
if len(cmds.ls(geometry_shape)) > 1:
cmds.warning("Multiple shapes with identical "
"names found. This might not work")
except TypeError as e:
cmds.warning("Skipping %s: Didn't find a shape "
"for component elementection. %s" % (element, e))
return
try:
type = cmds.nodeType(geometry_shape)
if type == "nurbsSurface":
# If a surfacePoint is elementected on a nurbs surface
root, u, v = element.rsplit("[", 2)
uv = [float(u[:-1]), float(v[:-1])]
if type == "mesh":
# -----------
# Average the U and V values
# ===========
uvs = cmds.polyListComponentConversion(element, toUV=1)
if not uvs:
cmds.warning("Couldn't derive any UV's from "
"component, reverting to default U and V")
raise TypeError
# Flatten list of Uv's as sometimes it returns
# neighbors like this [2:3] instead of [2], [3]
flattened = []
for uv in uvs:
flattened.extend(cmds.ls(uv, flatten=True))
uvs = flattened
sumU = 0
sumV = 0
for uv in uvs:
try:
u, v = cmds.polyEditUV(uv, query=True)
except Exception:
cmds.warning("Couldn't find any UV coordinated, "
"reverting to default U and V")
raise TypeError
sumU += u
sumV += v
averagedU = sumU / len(uvs)
averagedV = sumV / len(uvs)
uv = [averagedU, averagedV]
except TypeError:
pass
return uv
def shape_from_element(element):
"""Return shape of given 'element'
Supports components, meshes, and surfaces
"""
try:
# Get either shape or transform, based on element-type
node = cmds.ls(element, objectsOnly=True)[0]
except Exception:
cmds.warning("Could not find node in %s" % element)
return None
if cmds.nodeType(node) == 'transform':
try:
return cmds.listRelatives(node, shapes=True)[0]
except Exception:
cmds.warning("Could not find shape in %s" % element)
return None
else:
return node
def export_alembic(nodes,
file,
frame_range=None,
@ -577,115 +415,6 @@ def imprint(node, data):
cmds.setAttr(node + "." + key, value, **set_type)
def serialise_shaders(nodes):
"""Generate a shader set dictionary
Arguments:
nodes (list): Absolute paths to nodes
Returns:
dictionary of (shader: id) pairs
Schema:
{
"shader1": ["id1", "id2"],
"shader2": ["id3", "id1"]
}
Example:
{
"Bazooka_Brothers01_:blinn4SG": [
"f9520572-ac1d-11e6-b39e-3085a99791c9.f[4922:5001]",
"f9520572-ac1d-11e6-b39e-3085a99791c9.f[4587:4634]",
"f9520572-ac1d-11e6-b39e-3085a99791c9.f[1120:1567]",
"f9520572-ac1d-11e6-b39e-3085a99791c9.f[4251:4362]"
],
"lambert2SG": [
"f9520571-ac1d-11e6-9dbb-3085a99791c9"
]
}
"""
valid_nodes = cmds.ls(
nodes,
long=True,
recursive=True,
showType=True,
objectsOnly=True,
type="transform"
)
meshes_by_id = {}
for mesh in valid_nodes:
shapes = cmds.listRelatives(valid_nodes[0],
shapes=True,
fullPath=True) or list()
if shapes:
shape = shapes[0]
if not cmds.nodeType(shape):
continue
try:
id_ = cmds.getAttr(mesh + ".mbID")
if id_ not in meshes_by_id:
meshes_by_id[id_] = list()
meshes_by_id[id_].append(mesh)
except ValueError:
continue
meshes_by_shader = dict()
for mesh in meshes_by_id.values():
shape = cmds.listRelatives(mesh,
shapes=True,
fullPath=True) or list()
for shader in cmds.listConnections(shape,
type="shadingEngine") or list():
# Objects in this group are those that haven't got
# any shaders. These are expected to be managed
# elsewhere, such as by the default model loader.
if shader == "initialShadingGroup":
continue
if shader not in meshes_by_shader:
meshes_by_shader[shader] = list()
shaded = cmds.sets(shader, query=True) or list()
meshes_by_shader[shader].extend(shaded)
shader_by_id = {}
for shader, shaded in meshes_by_shader.items():
if shader not in shader_by_id:
shader_by_id[shader] = list()
for mesh in shaded:
# Enable shader assignment to faces.
name = mesh.split(".f[")[0]
transform = name
if cmds.objectType(transform) == "mesh":
transform = cmds.listRelatives(name, parent=True)[0]
try:
id_ = cmds.getAttr(transform + ".mbID")
shader_by_id[shader].append(mesh.replace(name, id_))
except KeyError:
continue
# Remove duplicates
shader_by_id[shader] = list(set(shader_by_id[shader]))
return shader_by_id
def lsattr(attr, value=None):
"""Return nodes matching `key` and `value`
@ -764,17 +493,6 @@ def lsattrs(attrs):
return list(matches)
@contextlib.contextmanager
def without_extension():
"""Use cmds.file with defaultExtensions=False"""
previous_setting = cmds.file(defaultExtensions=True, query=True)
try:
cmds.file(defaultExtensions=False)
yield
finally:
cmds.file(defaultExtensions=previous_setting)
@contextlib.contextmanager
def attribute_values(attr_values):
"""Remaps node attributes to values during context.
@ -853,26 +571,6 @@ def evaluation(mode="off"):
cmds.evaluationManager(mode=original)
@contextlib.contextmanager
def no_refresh():
"""Temporarily disables Maya's UI updates
Note:
This only disabled the main pane and will sometimes still
trigger updates in torn off panels.
"""
pane = _get_mel_global('gMainPane')
state = cmds.paneLayout(pane, query=True, manage=True)
cmds.paneLayout(pane, edit=True, manage=False)
try:
yield
finally:
cmds.paneLayout(pane, edit=True, manage=state)
@contextlib.contextmanager
def empty_sets(sets, force=False):
"""Remove all members of the sets during the context"""
@ -1539,15 +1237,6 @@ def extract_alembic(file,
return file
def maya_temp_folder():
scene_dir = os.path.dirname(cmds.file(query=True, sceneName=True))
tmp_dir = os.path.abspath(os.path.join(scene_dir, "..", "tmp"))
if not os.path.isdir(tmp_dir):
os.makedirs(tmp_dir)
return tmp_dir
# region ID
def get_id_required_nodes(referenced_nodes=False, nodes=None):
"""Filter out any node which are locked (reference) or readOnly
@ -1732,22 +1421,6 @@ def set_id(node, unique_id, overwrite=False):
cmds.setAttr(attr, unique_id, type="string")
def remove_id(node):
"""Remove the id attribute from the input node.
Args:
node (str): The node name
Returns:
bool: Whether an id attribute was deleted
"""
if cmds.attributeQuery("cbId", node=node, exists=True):
cmds.deleteAttr("{}.cbId".format(node))
return True
return False
# endregion ID
def get_reference_node(path):
"""
@ -2423,6 +2096,7 @@ def reset_scene_resolution():
set_scene_resolution(width, height, pixelAspect)
def set_context_settings():
"""Apply the project settings from the project definition
@ -2881,7 +2555,7 @@ def get_attr_in_layer(attr, layer):
def fix_incompatible_containers():
"""Return whether the current scene has any outdated content"""
"""Backwards compatibility: old containers to use new ReferenceLoader"""
host = api.registered_host()
for container in host.ls():
@ -3120,7 +2794,7 @@ class RenderSetupListObserver:
cmds.delete(render_layer_set_name)
class RenderSetupItemObserver():
class RenderSetupItemObserver:
"""Handle changes in render setup items."""
def __init__(self, item):
@ -3356,7 +3030,7 @@ def set_colorspace():
@contextlib.contextmanager
def root_parent(nodes):
# type: (list) -> list
"""Context manager to un-parent provided nodes and return then back."""
"""Context manager to un-parent provided nodes and return them back."""
import pymel.core as pm # noqa
node_parents = []

View file

@ -1,924 +0,0 @@
[
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\others\\save_scene_incremental.py",
"sourcetype": "file",
"title": "# Version Up",
"tooltip": "Incremental save with a specific format"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\others\\open_current_folder.py",
"sourcetype": "file",
"title": "Open working folder..",
"tooltip": "Show current scene in Explorer"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\avalon\\launch_manager.py",
"sourcetype": "file",
"title": "# Project Manager",
"tooltip": "Add assets to the project"
},
{
"type": "action",
"command": "from openpype.tools.assetcreator import app as assetcreator; assetcreator.show(context='maya')",
"sourcetype": "python",
"title": "Asset Creator",
"tooltip": "Open the Asset Creator"
},
{
"type": "separator"
},
{
"type": "menu",
"title": "Modeling",
"items": [
{
"type": "action",
"command": "import easyTreezSource; reload(easyTreezSource); easyTreezSource.easyTreez()",
"sourcetype": "python",
"tags": ["modeling", "trees", "generate", "create", "plants"],
"title": "EasyTreez",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\modeling\\separateMeshPerShader.py",
"sourcetype": "file",
"tags": ["modeling", "separateMeshPerShader"],
"title": "# Separate Mesh Per Shader",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\modeling\\polyDetachSeparate.py",
"sourcetype": "file",
"tags": ["modeling", "poly", "detach", "separate"],
"title": "# Polygon Detach and Separate",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\modeling\\polySelectEveryNthEdgeUI.py",
"sourcetype": "file",
"tags": ["modeling", "select", "nth", "edge", "ui"],
"title": "# Select Every Nth Edge"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\modeling\\djPFXUVs.py",
"sourcetype": "file",
"tags": ["modeling", "djPFX", "UVs"],
"title": "# dj PFX UVs",
"tooltip": ""
}
]
},
{
"type": "menu",
"title": "Rigging",
"items": [
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\rigging\\advancedSkeleton.py",
"sourcetype": "file",
"tags": [
"rigging",
"autorigger",
"advanced",
"skeleton",
"advancedskeleton",
"file"
],
"title": "Advanced Skeleton"
}
]
},
{
"type": "menu",
"title": "Shading",
"items": [
{
"type": "menu",
"title": "# VRay",
"items": [
{
"type": "action",
"title": "# Import Proxies",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayImportProxies.py",
"sourcetype": "file",
"tags": ["shading", "vray", "import", "proxies"],
"tooltip": ""
},
{
"type": "separator"
},
{
"type": "action",
"title": "# Select All GES",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\selectAllGES.py",
"sourcetype": "file",
"tooltip": "",
"tags": ["shading", "vray", "select All GES"]
},
{
"type": "action",
"title": "# Select All GES Under Selection",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\selectAllGESUnderSelection.py",
"sourcetype": "file",
"tooltip": "",
"tags": ["shading", "vray", "select", "all", "GES"]
},
{
"type": "separator"
},
{
"type": "action",
"title": "# Selection To VRay Mesh",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\selectionToVrayMesh.py",
"sourcetype": "file",
"tooltip": "",
"tags": ["shading", "vray", "selection", "vraymesh"]
},
{
"type": "action",
"title": "# Add VRay Round Edges Attribute",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVrayRoundEdgesAttribute.py",
"sourcetype": "file",
"tooltip": "",
"tags": ["shading", "vray", "round edges", "attribute"]
},
{
"type": "action",
"title": "# Add Gamma",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayAddGamma.py",
"sourcetype": "file",
"tooltip": "",
"tags": ["shading", "vray", "add gamma"]
},
{
"type": "separator"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\select_vraymesh_materials_with_unconnected_shader_slots.py",
"sourcetype": "file",
"title": "# Select Unconnected Shader Materials",
"tags": [
"shading",
"vray",
"select",
"vraymesh",
"materials",
"unconnected shader slots"
],
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayMergeSimilarVRayMeshMaterials.py",
"sourcetype": "file",
"title": "# Merge Similar VRay Mesh Materials",
"tags": [
"shading",
"vray",
"Merge",
"VRayMesh",
"Materials"
],
"tooltip": ""
},
{
"type": "action",
"title": "# Create Two Sided Material",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayCreate2SidedMtlForSelectedMtlRenamed.py",
"sourcetype": "file",
"tooltip": "Creates two sided material for selected material and renames it",
"tags": ["shading", "vray", "two sided", "material"]
},
{
"type": "action",
"title": "# Create Two Sided Material For Selected",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayCreate2SidedMtlForSelectedMtl.py",
"sourcetype": "file",
"tooltip": "Select material to create a two sided version from it",
"tags": [
"shading",
"vray",
"Create2SidedMtlForSelectedMtl.py"
]
},
{
"type": "separator"
},
{
"type": "action",
"title": "# Add OpenSubdiv Attribute",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVrayOpenSubdivAttribute.py",
"sourcetype": "file",
"tooltip": "",
"tags": [
"shading",
"vray",
"add",
"open subdiv",
"attribute"
]
},
{
"type": "action",
"title": "# Remove OpenSubdiv Attribute",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\removeVrayOpenSubdivAttribute.py",
"sourcetype": "file",
"tooltip": "",
"tags": [
"shading",
"vray",
"remove",
"opensubdiv",
"attributee"
]
},
{
"type": "separator"
},
{
"type": "action",
"title": "# Add Subdivision Attribute",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVraySubdivisionAttribute.py",
"sourcetype": "file",
"tooltip": "",
"tags": [
"shading",
"vray",
"addVraySubdivisionAttribute"
]
},
{
"type": "action",
"title": "# Remove Subdivision Attribute.py",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\removeVraySubdivisionAttribute.py",
"sourcetype": "file",
"tooltip": "",
"tags": [
"shading",
"vray",
"remove",
"subdivision",
"attribute"
]
},
{
"type": "separator"
},
{
"type": "action",
"title": "# Add Vray Object Ids",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVrayObjectIds.py",
"sourcetype": "file",
"tooltip": "",
"tags": ["shading", "vray", "add", "object id"]
},
{
"type": "action",
"title": "# Add Vray Material Ids",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVrayMaterialIds.py",
"sourcetype": "file",
"tooltip": "",
"tags": ["shading", "vray", "addVrayMaterialIds.py"]
},
{
"type": "separator"
},
{
"type": "action",
"title": "# Set Physical DOF Depth",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayPhysicalDOFSetDepth.py",
"sourcetype": "file",
"tooltip": "",
"tags": ["shading", "vray", "physical", "DOF ", "Depth"]
},
{
"type": "action",
"title": "# Magic Vray Proxy UI",
"command": "$OPENPYPE_SCRIPTS\\shading\\vray\\magicVrayProxyUI.py",
"sourcetype": "file",
"tooltip": "",
"tags": ["shading", "vray", "magicVrayProxyUI"]
}
]
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\pyblish\\lighting\\set_filename_prefix.py",
"sourcetype": "file",
"tags": [
"shading",
"lookdev",
"assign",
"shaders",
"prefix",
"filename",
"render"
],
"title": "# Set filename prefix",
"tooltip": "Set the render file name prefix."
},
{
"type": "action",
"command": "import mayalookassigner; mayalookassigner.show()",
"sourcetype": "python",
"tags": ["shading", "look", "assign", "shaders", "auto"],
"title": "Look Manager",
"tooltip": "Open the Look Manager UI for look assignment"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\shading\\LightLinkUi.py",
"sourcetype": "file",
"tags": ["shading", "light", "link", "ui"],
"title": "# Light Link UI",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\shading\\vdviewer_ui.py",
"sourcetype": "file",
"tags": [
"shading",
"look",
"vray",
"displacement",
"shaders",
"auto"
],
"title": "# VRay Displ Viewer",
"tooltip": "Open the VRay Displacement Viewer, select and control the content of the set"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\shading\\setTexturePreviewToCLRImage.py",
"sourcetype": "file",
"tags": ["shading", "CLRImage", "textures", "preview"],
"title": "# Set Texture Preview To CLRImage",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\shading\\fixDefaultShaderSetBehavior.py",
"sourcetype": "file",
"tags": ["shading", "fix", "DefaultShaderSet", "Behavior"],
"title": "# Fix Default Shader Set Behavior",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\shading\\fixSelectedShapesReferenceAssignments.py",
"sourcetype": "file",
"tags": [
"shading",
"fix",
"Selected",
"Shapes",
"Reference",
"Assignments"
],
"title": "# Fix Shapes Reference Assignments",
"tooltip": "Select shapes to fix the reference assignments"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\shading\\selectLambert1Members.py",
"sourcetype": "file",
"tags": ["shading", "selectLambert1Members"],
"title": "# Select Lambert1 Members",
"tooltip": "Selects all objects which have the Lambert1 shader assigned"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\shading\\selectShapesWithoutShader.py",
"sourcetype": "file",
"tags": ["shading", "selectShapesWithoutShader"],
"title": "# Select Shapes Without Shader",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\shading\\fixRenderLayerOutAdjustmentErrors.py",
"sourcetype": "file",
"tags": ["shading", "fixRenderLayerOutAdjustmentErrors"],
"title": "# Fix RenderLayer Out Adjustment Errors",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\shading\\fix_renderlayer_missing_node_override.py",
"sourcetype": "file",
"tags": [
"shading",
"renderlayer",
"missing",
"reference",
"switch",
"layer"
],
"title": "# Fix RenderLayer Missing Referenced Nodes Overrides",
"tooltip": ""
},
{
"type": "action",
"title": "# Image 2 Tiled EXR",
"command": "$OPENPYPE_SCRIPTS\\shading\\open_img2exr.py",
"sourcetype": "file",
"tooltip": "",
"tags": ["shading", "vray", "exr"]
}
]
},
{
"type": "menu",
"title": "# Rendering",
"items": [
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\pyblish\\open_deadline_submission_settings.py",
"sourcetype": "file",
"tags": ["settings", "deadline", "globals", "render"],
"title": "# DL Submission Settings UI",
"tooltip": "Open the Deadline Submission Settings UI"
}
]
},
{
"type": "menu",
"title": "Animation",
"items": [
{
"type": "menu",
"title": "# Attributes",
"tooltip": "",
"items": [
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyValues.py",
"sourcetype": "file",
"tags": ["animation", "copy", "attributes"],
"title": "# Copy Values",
"tooltip": "Copy attribute values"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyInConnections.py",
"sourcetype": "file",
"tags": [
"animation",
"copy",
"attributes",
"connections",
"incoming"
],
"title": "# Copy In Connections",
"tooltip": "Copy incoming connections"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyOutConnections.py",
"sourcetype": "file",
"tags": [
"animation",
"copy",
"attributes",
"connections",
"out"
],
"title": "# Copy Out Connections",
"tooltip": "Copy outcoming connections"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyTransformLocal.py",
"sourcetype": "file",
"tags": [
"animation",
"copy",
"attributes",
"transforms",
"local"
],
"title": "# Copy Local Transforms",
"tooltip": "Copy local transforms"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyTransformMatrix.py",
"sourcetype": "file",
"tags": [
"animation",
"copy",
"attributes",
"transforms",
"matrix"
],
"title": "# Copy Matrix Transforms",
"tooltip": "Copy Matrix transforms"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyTransformUI.py",
"sourcetype": "file",
"tags": [
"animation",
"copy",
"attributes",
"transforms",
"UI"
],
"title": "# Copy Transforms UI",
"tooltip": "Open the Copy Transforms UI"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\simpleCopyUI.py",
"sourcetype": "file",
"tags": [
"animation",
"copy",
"attributes",
"transforms",
"UI",
"simple"
],
"title": "# Simple Copy UI",
"tooltip": "Open the simple Copy Transforms UI"
}
]
},
{
"type": "menu",
"title": "# Optimize",
"tooltip": "Optimization scripts",
"items": [
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\animation\\optimize\\toggleFreezeHierarchy.py",
"sourcetype": "file",
"tags": ["animation", "hierarchy", "toggle", "freeze"],
"title": "# Toggle Freeze Hierarchy",
"tooltip": "Freeze and unfreeze hierarchy"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\animation\\optimize\\toggleParallelNucleus.py",
"sourcetype": "file",
"tags": ["animation", "nucleus", "toggle", "parallel"],
"title": "# Toggle Parallel Nucleus",
"tooltip": "Toggle parallel nucleus"
}
]
},
{
"sourcetype": "file",
"command": "$OPENPYPE_SCRIPTS\\animation\\bakeSelectedToWorldSpace.py",
"tags": ["animation", "bake", "selection", "worldspace.py"],
"title": "# Bake Selected To Worldspace",
"type": "action"
},
{
"sourcetype": "file",
"command": "$OPENPYPE_SCRIPTS\\animation\\timeStepper.py",
"tags": ["animation", "time", "stepper"],
"title": "# Time Stepper",
"type": "action"
},
{
"sourcetype": "file",
"command": "$OPENPYPE_SCRIPTS\\animation\\capture_ui.py",
"tags": [
"animation",
"capture",
"ui",
"screen",
"movie",
"image"
],
"title": "# Capture UI",
"type": "action"
},
{
"sourcetype": "file",
"command": "$OPENPYPE_SCRIPTS\\animation\\simplePlayblastUI.py",
"tags": ["animation", "simple", "playblast", "ui"],
"title": "# Simple Playblast UI",
"type": "action"
},
{
"sourcetype": "file",
"command": "$OPENPYPE_SCRIPTS\\animation\\tweenMachineUI.py",
"tags": ["animation", "tween", "machine"],
"title": "# Tween Machine UI",
"type": "action"
},
{
"sourcetype": "file",
"command": "$OPENPYPE_SCRIPTS\\animation\\selectAllAnimationCurves.py",
"tags": ["animation", "select", "curves"],
"title": "# Select All Animation Curves",
"type": "action"
},
{
"sourcetype": "file",
"command": "$OPENPYPE_SCRIPTS\\animation\\pathAnimation.py",
"tags": ["animation", "path", "along"],
"title": "# Path Animation",
"type": "action"
},
{
"sourcetype": "file",
"command": "$OPENPYPE_SCRIPTS\\animation\\offsetSelectedObjectsUI.py",
"tags": ["animation", "offsetSelectedObjectsUI.py"],
"title": "# Offset Selected Objects UI",
"type": "action"
},
{
"sourcetype": "file",
"command": "$OPENPYPE_SCRIPTS\\animation\\key_amplifier_ui.py",
"tags": ["animation", "key", "amplifier"],
"title": "# Key Amplifier UI",
"type": "action"
},
{
"sourcetype": "file",
"command": "$OPENPYPE_SCRIPTS\\animation\\anim_scene_optimizer.py",
"tags": ["animation", "anim_scene_optimizer.py"],
"title": "# Anim_Scene_Optimizer",
"type": "action"
},
{
"sourcetype": "file",
"command": "$OPENPYPE_SCRIPTS\\animation\\zvParentMaster.py",
"tags": ["animation", "zvParentMaster.py"],
"title": "# ZV Parent Master",
"type": "action"
},
{
"sourcetype": "file",
"command": "$OPENPYPE_SCRIPTS\\animation\\animLibrary.py",
"tags": ["animation", "studiolibrary.py"],
"title": "Anim Library",
"type": "action"
}
]
},
{
"type": "menu",
"title": "# Layout",
"items": [
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\layout\\alignDistributeUI.py",
"sourcetype": "file",
"tags": ["layout", "align", "Distribute", "UI"],
"title": "# Align Distribute UI",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\layout\\alignSimpleUI.py",
"sourcetype": "file",
"tags": ["layout", "align", "UI", "Simple"],
"title": "# Align Simple UI",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\layout\\center_locator.py",
"sourcetype": "file",
"tags": ["layout", "center", "locator"],
"title": "# Center Locator",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\layout\\average_locator.py",
"sourcetype": "file",
"tags": ["layout", "average", "locator"],
"title": "# Average Locator",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\layout\\selectWithinProximityUI.py",
"sourcetype": "file",
"tags": ["layout", "select", "proximity", "ui"],
"title": "# Select Within Proximity UI",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\layout\\dupCurveUI.py",
"sourcetype": "file",
"tags": ["layout", "Duplicate", "Curve", "UI"],
"title": "# Duplicate Curve UI",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\layout\\randomDeselectUI.py",
"sourcetype": "file",
"tags": ["layout", "random", "Deselect", "UI"],
"title": "# Random Deselect UI",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\layout\\multiReferencerUI.py",
"sourcetype": "file",
"tags": ["layout", "multi", "reference"],
"title": "# Multi Referencer UI",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\layout\\duplicateOffsetUI.py",
"sourcetype": "file",
"tags": ["layout", "duplicate", "offset", "UI"],
"title": "# Duplicate Offset UI",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\layout\\spPaint3d.py",
"sourcetype": "file",
"tags": ["layout", "spPaint3d", "paint", "tool"],
"title": "# SP Paint 3d",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\layout\\randomizeUI.py",
"sourcetype": "file",
"tags": ["layout", "randomize", "UI"],
"title": "# Randomize UI",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\layout\\distributeWithinObjectUI.py",
"sourcetype": "file",
"tags": ["layout", "distribute", "ObjectUI", "within"],
"title": "# Distribute Within Object UI",
"tooltip": ""
}
]
},
{
"type": "menu",
"title": "# Particles",
"items": [
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjects.py",
"sourcetype": "file",
"tags": ["particles", "instancerToObjects"],
"title": "# Instancer To Objects",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjectsInstances.py",
"sourcetype": "file",
"tags": ["particles", "instancerToObjectsInstances"],
"title": "# Instancer To Objects Instances",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjectsInstancesWithAnimation.py",
"sourcetype": "file",
"tags": [
"particles",
"instancerToObjectsInstancesWithAnimation"
],
"title": "# Instancer To Objects Instances With Animation",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjectsWithAnimation.py",
"sourcetype": "file",
"tags": ["particles", "instancerToObjectsWithAnimation"],
"title": "# Instancer To Objects With Animation",
"tooltip": ""
}
]
},
{
"type": "menu",
"title": "Cleanup",
"items": [
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\cleanup\\repair_faulty_containers.py",
"sourcetype": "file",
"tags": ["cleanup", "repair", "containers"],
"title": "# Find and Repair Containers",
"tooltip": ""
},
{
"type": "separator"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\cleanup\\removeNamespaces.py",
"sourcetype": "file",
"tags": ["cleanup", "remove", "namespaces"],
"title": "# Remove Namespaces",
"tooltip": "Remove all namespaces"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\cleanup\\remove_user_defined_attributes.py",
"sourcetype": "file",
"tags": ["cleanup", "remove_user_defined_attributes"],
"title": "# Remove User Defined Attributes",
"tooltip": "Remove all user-defined attributes from all nodes"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\cleanup\\removeUnknownNodes.py",
"sourcetype": "file",
"tags": ["cleanup", "removeUnknownNodes"],
"title": "# Remove Unknown Nodes",
"tooltip": "Remove all unknown nodes"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\cleanup\\removeUnloadedReferences.py",
"sourcetype": "file",
"tags": ["cleanup", "removeUnloadedReferences"],
"title": "# Remove Unloaded References",
"tooltip": "Remove all unloaded references"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\cleanup\\removeReferencesFailedEdits.py",
"sourcetype": "file",
"tags": ["cleanup", "removeReferencesFailedEdits"],
"title": "# Remove References Failed Edits",
"tooltip": "Remove failed edits for all references"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\cleanup\\remove_unused_looks.py",
"sourcetype": "file",
"tags": ["cleanup", "removeUnusedLooks"],
"title": "# Remove Unused Looks",
"tooltip": "Remove all loaded yet unused Avalon look containers"
},
{
"type": "separator"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\cleanup\\uniqifyNodeNames.py",
"sourcetype": "file",
"tags": ["cleanup", "uniqifyNodeNames"],
"title": "# Uniqify Node Names",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\cleanup\\autoRenameFileNodes.py",
"sourcetype": "file",
"tags": ["cleanup", "auto", "rename", "filenodes"],
"title": "# Auto Rename File Nodes",
"tooltip": ""
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\cleanup\\update_asset_id.py",
"sourcetype": "file",
"tags": ["cleanup", "update", "database", "asset", "id"],
"title": "# Update Asset ID",
"tooltip": "Will replace the Colorbleed ID with a new one (asset ID : Unique number)"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\cleanup\\ccRenameReplace.py",
"sourcetype": "file",
"tags": ["cleanup", "rename", "ui"],
"title": "Renamer",
"tooltip": "Rename UI"
},
{
"type": "action",
"command": "$OPENPYPE_SCRIPTS\\cleanup\\renameShapesToTransform.py",
"sourcetype": "file",
"tags": ["cleanup", "renameShapesToTransform"],
"title": "# Rename Shapes To Transform",
"tooltip": ""
}
]
}
]

File diff suppressed because it is too large Load diff

View file

@ -184,76 +184,6 @@ def uninstall():
menu.uninstall()
def lock():
"""Lock scene
Add an invisible node to your Maya scene with the name of the
current file, indicating that this file is "locked" and cannot
be modified any further.
"""
if not cmds.objExists("lock"):
with lib.maintained_selection():
cmds.createNode("objectSet", name="lock")
cmds.addAttr("lock", ln="basename", dataType="string")
# Permanently hide from outliner
cmds.setAttr("lock.verticesOnlySet", True)
fname = cmds.file(query=True, sceneName=True)
basename = os.path.basename(fname)
cmds.setAttr("lock.basename", basename, type="string")
def unlock():
"""Permanently unlock a locked scene
Doesn't throw an error if scene is already unlocked.
"""
try:
cmds.delete("lock")
except ValueError:
pass
def is_locked():
"""Query whether current scene is locked"""
fname = cmds.file(query=True, sceneName=True)
basename = os.path.basename(fname)
if self._ignore_lock:
return False
try:
return cmds.getAttr("lock.basename") == basename
except ValueError:
return False
@contextlib.contextmanager
def lock_ignored():
"""Context manager for temporarily ignoring the lock of a scene
The purpose of this function is to enable locking a scene and
saving it with the lock still in place.
Example:
>>> with lock_ignored():
... pass # Do things without lock
"""
self._ignore_lock = True
try:
yield
finally:
self._ignore_lock = False
def parse_container(container):
"""Return the container node's full container data.

View file

@ -43,6 +43,7 @@ DEFAULT_OPENPYPE_MODULES = (
"standalonepublish_action",
"job_queue",
"timers_manager",
"sync_server",
)

View file

@ -20,18 +20,23 @@ class CollectSlackFamilies(pyblish.api.InstancePlugin):
def process(self, instance):
task_name = io.Session.get("AVALON_TASK")
family = self.main_family_from_instance(instance)
key_values = {
"families": family,
"tasks": task_name,
"hosts": instance.data["anatomyData"]["app"],
"subsets": instance.data["subset"]
}
profile = filter_profiles(self.profiles, key_values,
logger=self.log)
if not profile:
self.log.info("No profile found, notification won't be send")
return
# make slack publishable
if profile:
self.log.info("Found profile: {}".format(profile))
if instance.data.get('families'):
instance.data['families'].append('slack')
else:

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 980 B

After

Width:  |  Height:  |  Size: 980 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1,011 B

After

Width:  |  Height:  |  Size: 1,011 B

Before After
Before After

View file

@ -478,6 +478,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
if index_frame_start is not None:
dst_padding_exp = "%0{}d".format(frame_start_padding)
dst_padding = dst_padding_exp % (index_frame_start + frame_number) # noqa: E501
elif repre.get("udim"):
dst_padding = int(i)
dst = "{0}{1}{2}".format(
dst_head,

View file

@ -2,7 +2,8 @@
"create": {
"CreateShotClip": {
"hierarchy": "{folder}/{sequence}",
"clipRename": true,
"useShotName": true,
"clipRename": false,
"clipName": "{sequence}{shot}",
"segmentIndex": true,
"countFrom": 10,

View file

@ -10,6 +10,7 @@
"hosts": [],
"task_types": [],
"tasks": [],
"subsets": [],
"channel_messages": []
}
]

View file

@ -135,7 +135,7 @@
"OPENPYPE_WIRETAP_TOOLS": "/opt/Autodesk/wiretap/tools/2021"
}
},
"2021.1": {
"2021_1": {
"use_python_2": true,
"executables": {
"windows": [],
@ -159,7 +159,7 @@
},
"__dynamic_keys_labels__": {
"2021": "2021",
"2021.1": "2021.1"
"2021_1": "2021.1"
}
}
},

View file

@ -28,6 +28,11 @@
"key": "hierarchy",
"label": "Shot parent hierarchy"
},
{
"type": "boolean",
"key": "useShotName",
"label": "Use Shot Name"
},
{
"type": "boolean",
"key": "clipRename",

View file

@ -69,6 +69,12 @@
"type": "list",
"object_type": "text"
},
{
"key": "subsets",
"label": "Subset names",
"type": "list",
"object_type": "text"
},
{
"type": "separator"
},

View file

@ -15,7 +15,7 @@ provides a bridge between the file-based project inventory and configuration.
"""
import os
from Qt import QtGui, QtCore
from Qt import QtGui
from avalon.vendor import qtawesome
from openpype.api import resources
@ -23,77 +23,6 @@ ICON_CACHE = {}
NOT_FOUND = type("NotFound", (object, ), {})
class ProjectHandler(QtCore.QObject):
"""Handler of project model and current project in Launcher tool.
Helps to organize two separate widgets handling current project selection.
It is easier to trigger project change callbacks from one place than from
multiple different places without proper handling or sequence changes.
Args:
dbcon(AvalonMongoDB): Mongo connection with Session.
model(ProjectModel): Object of projects model which is shared across
all widgets using projects. Arg dbcon should be used as source for
the model.
"""
# Project list will be refreshed each 10000 msecs
# - this is not part of helper implementation but should be used by widgets
# that may require reshing of projects
refresh_interval = 10000
# Signal emitted when project has changed
project_changed = QtCore.Signal(str)
projects_refreshed = QtCore.Signal()
timer_timeout = QtCore.Signal()
def __init__(self, dbcon, model):
super(ProjectHandler, self).__init__()
self._active = False
# Store project model for usage
self.model = model
# Store dbcon
self.dbcon = dbcon
self.current_project = dbcon.Session.get("AVALON_PROJECT")
refresh_timer = QtCore.QTimer()
refresh_timer.setInterval(self.refresh_interval)
refresh_timer.timeout.connect(self._on_timeout)
self.refresh_timer = refresh_timer
def _on_timeout(self):
if self._active:
self.timer_timeout.emit()
self.refresh_model()
def set_active(self, active):
self._active = active
def start_timer(self, trigger=False):
self.refresh_timer.start()
if trigger:
self._on_timeout()
def stop_timer(self):
self.refresh_timer.stop()
def set_project(self, project_name):
# Change current project of this handler
self.current_project = project_name
# Change session project to take effect for other widgets using the
# dbcon object.
self.dbcon.Session["AVALON_PROJECT"] = project_name
# Trigger change signal when everything is updated to new project
self.project_changed.emit(project_name)
def refresh_model(self):
self.model.refresh()
self.projects_refreshed.emit()
def get_action_icon(action):
icon_name = action.icon
if not icon_name:

View file

@ -1,8 +1,30 @@
import re
import uuid
import copy
import logging
import collections
import time
import appdirs
from Qt import QtCore, QtGui
from avalon.vendor import qtawesome
from avalon import api
from openpype.lib import JSONSettingRegistry
from openpype.lib.applications import (
CUSTOM_LAUNCH_APP_GROUPS,
ApplicationManager
)
from openpype.tools.utils.lib import DynamicQThread
from openpype.tools.utils.assets_widget import (
AssetModel,
ASSET_NAME_ROLE
)
from openpype.tools.utils.tasks_widget import (
TasksModel,
TasksProxyModel,
TASK_TYPE_ROLE,
TASK_ASSIGNEE_ROLE
)
from . import lib
from .constants import (
@ -13,17 +35,13 @@ from .constants import (
FORCE_NOT_OPEN_WORKFILE_ROLE
)
from .actions import ApplicationAction
from Qt import QtCore, QtGui
from avalon.vendor import qtawesome
from avalon import api
from openpype.lib import JSONSettingRegistry
from openpype.lib.applications import (
CUSTOM_LAUNCH_APP_GROUPS,
ApplicationManager
)
log = logging.getLogger(__name__)
# Must be different than roles in default asset model
ASSET_TASK_TYPES_ROLE = QtCore.Qt.UserRole + 10
ASSET_ASSIGNEE_ROLE = QtCore.Qt.UserRole + 11
class ActionModel(QtGui.QStandardItemModel):
def __init__(self, dbcon, parent=None):
@ -330,21 +348,483 @@ class ActionModel(QtGui.QStandardItemModel):
return compare_data
class LauncherModel(QtCore.QObject):
# Refresh interval of projects
refresh_interval = 10000
# Signals
# Current project has changed
project_changed = QtCore.Signal(str)
# Filters has changed (any)
filters_changed = QtCore.Signal()
# Projects were refreshed
projects_refreshed = QtCore.Signal()
# Signals ONLY for assets model!
# - other objects should listen to asset model signals
# Asset refresh started
assets_refresh_started = QtCore.Signal()
# Assets refresh finished
assets_refreshed = QtCore.Signal()
# Refresh timer timeout
# - give ability to tell parent window that this timer still runs
timer_timeout = QtCore.Signal()
# Duplication from AssetsModel with "data.tasks"
_asset_projection = {
"name": 1,
"parent": 1,
"data.visualParent": 1,
"data.label": 1,
"data.icon": 1,
"data.color": 1,
"data.tasks": 1
}
def __init__(self, dbcon):
super(LauncherModel, self).__init__()
# Refresh timer
# - should affect only projects
refresh_timer = QtCore.QTimer()
refresh_timer.setInterval(self.refresh_interval)
refresh_timer.timeout.connect(self._on_timeout)
self._refresh_timer = refresh_timer
# Launcher is active
self._active = False
# Global data
self._dbcon = dbcon
# Available project names
self._project_names = set()
# Context data
self._asset_docs = []
self._asset_docs_by_id = {}
self._asset_filter_data_by_id = {}
self._assignees = set()
self._task_types = set()
# Filters
self._asset_name_filter = ""
self._assignee_filters = set()
self._task_type_filters = set()
# Last project for which were assets queried
self._last_project_name = None
# Asset refresh thread is running
self._refreshing_assets = False
# Asset refresh thread
self._asset_refresh_thread = None
def _on_timeout(self):
"""Refresh timer timeout."""
if self._active:
self.timer_timeout.emit()
self.refresh_projects()
def set_active(self, active):
"""Window change active state."""
self._active = active
def start_refresh_timer(self, trigger=False):
"""Start refresh timer."""
self._refresh_timer.start()
if trigger:
self._on_timeout()
def stop_refresh_timer(self):
"""Stop refresh timer."""
self._refresh_timer.stop()
@property
def project_name(self):
"""Current project name."""
return self._dbcon.Session.get("AVALON_PROJECT")
@property
def refreshing_assets(self):
"""Refreshing thread is running."""
return self._refreshing_assets
@property
def asset_docs(self):
"""Access to asset docs."""
return self._asset_docs
@property
def project_names(self):
"""Available project names."""
return self._project_names
@property
def asset_filter_data_by_id(self):
"""Prepared filter data by asset id."""
return self._asset_filter_data_by_id
@property
def assignees(self):
"""All assignees for all assets in current project."""
return self._assignees
@property
def task_types(self):
"""All task types for all assets in current project.
TODO: This could be maybe taken from project document where are all
task types...
"""
return self._task_types
@property
def task_type_filters(self):
"""Currently set task type filters."""
return self._task_type_filters
@property
def assignee_filters(self):
"""Currently set assignee filters."""
return self._assignee_filters
@property
def asset_name_filter(self):
"""Asset name filter (can be used as regex filter)."""
return self._asset_name_filter
def get_asset_doc(self, asset_id):
"""Get single asset document by id."""
return self._asset_docs_by_id.get(asset_id)
def set_project_name(self, project_name):
"""Change project name and refresh asset documents."""
if project_name == self.project_name:
return
self._dbcon.Session["AVALON_PROJECT"] = project_name
self.project_changed.emit(project_name)
self.refresh_assets(force=True)
def refresh(self):
"""Trigger refresh of whole model."""
self.refresh_projects()
self.refresh_assets(force=False)
def refresh_projects(self):
"""Refresh projects."""
current_project = self.project_name
project_names = set()
for project_doc in self._dbcon.projects(only_active=True):
project_names.add(project_doc["name"])
self._project_names = project_names
self.projects_refreshed.emit()
if (
current_project is not None
and current_project not in project_names
):
self.set_project_name(None)
def _set_asset_docs(self, asset_docs=None):
"""Set asset documents and all related data.
Method extract and prepare data needed for assets and tasks widget and
prepare filtering data.
"""
if asset_docs is None:
asset_docs = []
all_task_types = set()
all_assignees = set()
asset_docs_by_id = {}
asset_filter_data_by_id = {}
for asset_doc in asset_docs:
task_types = set()
assignees = set()
asset_id = asset_doc["_id"]
asset_docs_by_id[asset_id] = asset_doc
asset_tasks = asset_doc.get("data", {}).get("tasks")
asset_filter_data_by_id[asset_id] = {
"assignees": assignees,
"task_types": task_types
}
if not asset_tasks:
continue
for task_data in asset_tasks.values():
task_assignees = set()
_task_assignees = task_data.get("assignees")
if _task_assignees:
for assignee in _task_assignees:
task_assignees.add(assignee["username"])
task_type = task_data.get("type")
if task_assignees:
assignees |= set(task_assignees)
if task_type:
task_types.add(task_type)
all_task_types |= task_types
all_assignees |= assignees
self._asset_docs_by_id = asset_docs_by_id
self._asset_docs = asset_docs
self._asset_filter_data_by_id = asset_filter_data_by_id
self._assignees = all_assignees
self._task_types = all_task_types
self.assets_refreshed.emit()
def set_task_type_filter(self, task_types):
"""Change task type filter.
Args:
task_types (set): Set of task types that should be visible.
Pass empty set to turn filter off.
"""
self._task_type_filters = task_types
self.filters_changed.emit()
def set_assignee_filter(self, assignees):
"""Change assignees filter.
Args:
assignees (set): Set of assignees that should be visible.
Pass empty set to turn filter off.
"""
self._assignee_filters = assignees
self.filters_changed.emit()
def set_asset_name_filter(self, text_filter):
"""Change asset name filter.
Args:
text_filter (str): Asset name filter. Pass empty string to
turn filter off.
"""
self._asset_name_filter = text_filter
self.filters_changed.emit()
def refresh_assets(self, force=True):
"""Refresh assets."""
self.assets_refresh_started.emit()
if self.project_name is None:
self._set_asset_docs()
return
if (
not force
and self._last_project_name == self.project_name
):
return
self._stop_fetch_thread()
self._refreshing_assets = True
self._last_project_name = self.project_name
self._asset_refresh_thread = DynamicQThread(self._refresh_assets)
self._asset_refresh_thread.start()
def _stop_fetch_thread(self):
self._refreshing_assets = False
if self._asset_refresh_thread is not None:
while self._asset_refresh_thread.isRunning():
# TODO this is blocking UI should be done in a different way
time.sleep(0.01)
self._asset_refresh_thread = None
def _refresh_assets(self):
asset_docs = list(self._dbcon.find(
{"type": "asset"},
self._asset_projection
))
if not self._refreshing_assets:
return
self._refreshing_assets = False
self._set_asset_docs(asset_docs)
class LauncherTasksProxyModel(TasksProxyModel):
"""Tasks proxy model with more filtering.
TODO:
This can be (with few modifications) used in default tasks widget too.
"""
def __init__(self, launcher_model, *args, **kwargs):
self._launcher_model = launcher_model
super(LauncherTasksProxyModel, self).__init__(*args, **kwargs)
launcher_model.filters_changed.connect(self._on_filter_change)
self._task_types_filter = set()
self._assignee_filter = set()
def _on_filter_change(self):
self._task_types_filter = self._launcher_model.task_type_filters
self._assignee_filter = self._launcher_model.assignee_filters
self.invalidateFilter()
def filterAcceptsRow(self, row, parent):
if not self._task_types_filter and not self._assignee_filter:
return True
model = self.sourceModel()
source_index = model.index(row, self.filterKeyColumn(), parent)
if not source_index.isValid():
return False
# Check current index itself
if self._task_types_filter:
task_type = model.data(source_index, TASK_TYPE_ROLE)
if task_type not in self._task_types_filter:
return False
if self._assignee_filter:
assignee = model.data(source_index, TASK_ASSIGNEE_ROLE)
if not self._assignee_filter.intersection(assignee):
return False
return True
class LauncherTaskModel(TasksModel):
def __init__(self, launcher_model, *args, **kwargs):
self._launcher_model = launcher_model
super(LauncherTaskModel, self).__init__(*args, **kwargs)
def set_asset_id(self, asset_id):
asset_doc = None
if self._context_is_valid():
asset_doc = self._launcher_model.get_asset_doc(asset_id)
self._set_asset(asset_doc)
class AssetRecursiveSortFilterModel(QtCore.QSortFilterProxyModel):
def __init__(self, launcher_model, *args, **kwargs):
self._launcher_model = launcher_model
super(AssetRecursiveSortFilterModel, self).__init__(*args, **kwargs)
launcher_model.filters_changed.connect(self._on_filter_change)
self._name_filter = ""
self._task_types_filter = set()
self._assignee_filter = set()
def _on_filter_change(self):
self._name_filter = self._launcher_model.asset_name_filter
self._task_types_filter = self._launcher_model.task_type_filters
self._assignee_filter = self._launcher_model.assignee_filters
self.invalidateFilter()
"""Filters to the regex if any of the children matches allow parent"""
def filterAcceptsRow(self, row, parent):
if (
not self._name_filter
and not self._task_types_filter
and not self._assignee_filter
):
return True
model = self.sourceModel()
source_index = model.index(row, self.filterKeyColumn(), parent)
if not source_index.isValid():
return False
# Check current index itself
valid = True
if self._name_filter:
name = model.data(source_index, ASSET_NAME_ROLE)
if (
name is None
or not re.search(self._name_filter, name, re.IGNORECASE)
):
valid = False
if valid and self._task_types_filter:
task_types = model.data(source_index, ASSET_TASK_TYPES_ROLE)
if not self._task_types_filter.intersection(task_types):
valid = False
if valid and self._assignee_filter:
assignee = model.data(source_index, ASSET_ASSIGNEE_ROLE)
if not self._assignee_filter.intersection(assignee):
valid = False
if valid:
return True
# Check children
rows = model.rowCount(source_index)
for child_row in range(rows):
if self.filterAcceptsRow(child_row, source_index):
return True
return False
class LauncherAssetsModel(AssetModel):
def __init__(self, launcher_model, dbcon, parent=None):
self._launcher_model = launcher_model
# Make sure that variable is available (even if is in AssetModel)
self._last_project_name = None
super(LauncherAssetsModel, self).__init__(dbcon, parent)
launcher_model.project_changed.connect(self._on_project_change)
launcher_model.assets_refresh_started.connect(
self._on_launcher_refresh_start
)
launcher_model.assets_refreshed.connect(self._on_launcher_refresh)
def _on_launcher_refresh_start(self):
self._refreshing = True
project_name = self._launcher_model.project_name
if self._last_project_name != project_name:
self._clear_items()
self._last_project_name = project_name
def _on_launcher_refresh(self):
self._fill_assets(self._launcher_model.asset_docs)
self._refreshing = False
self.refreshed.emit(bool(self._items_by_asset_id))
def _fill_assets(self, *args, **kwargs):
super(LauncherAssetsModel, self)._fill_assets(*args, **kwargs)
asset_filter_data_by_id = self._launcher_model.asset_filter_data_by_id
for asset_id, item in self._items_by_asset_id.items():
filter_data = asset_filter_data_by_id.get(asset_id)
assignees = filter_data["assignees"]
task_types = filter_data["task_types"]
item.setData(assignees, ASSET_ASSIGNEE_ROLE)
item.setData(task_types, ASSET_TASK_TYPES_ROLE)
def _on_project_change(self):
self._clear_items()
def refresh(self, *args, **kwargs):
raise ValueError("This is a bug!")
def stop_refresh(self, *args, **kwargs):
raise ValueError("This is a bug!")
class ProjectModel(QtGui.QStandardItemModel):
"""List of projects"""
def __init__(self, dbcon, parent=None):
def __init__(self, launcher_model, parent=None):
super(ProjectModel, self).__init__(parent=parent)
self.dbcon = dbcon
self._launcher_model = launcher_model
self.project_icon = qtawesome.icon("fa.map", color="white")
self._project_names = set()
def refresh(self):
project_names = set()
for project_doc in self.get_projects():
project_names.add(project_doc["name"])
launcher_model.projects_refreshed.connect(self._on_refresh)
def _on_refresh(self):
project_names = set(self._launcher_model.project_names)
origin_project_names = set(self._project_names)
self._project_names = project_names
@ -387,7 +867,3 @@ class ProjectModel(QtGui.QStandardItemModel):
items.append(item)
self.invisibleRootItem().insertRows(row, items)
def get_projects(self):
return sorted(self.dbcon.projects(only_active=True),
key=lambda x: x["name"])

View file

@ -4,11 +4,21 @@ import collections
from Qt import QtWidgets, QtCore, QtGui
from avalon.vendor import qtawesome
from openpype.tools.flickcharm import FlickCharm
from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
from openpype.tools.utils.tasks_widget import TasksWidget
from .delegates import ActionDelegate
from . import lib
from .models import (
ActionModel,
ProjectModel,
LauncherAssetsModel,
AssetRecursiveSortFilterModel,
LauncherTaskModel,
LauncherTasksProxyModel
)
from .actions import ApplicationAction
from .models import ActionModel
from openpype.tools.flickcharm import FlickCharm
from .constants import (
ACTION_ROLE,
GROUP_ROLE,
@ -22,15 +32,15 @@ from .constants import (
class ProjectBar(QtWidgets.QWidget):
def __init__(self, project_handler, parent=None):
def __init__(self, launcher_model, parent=None):
super(ProjectBar, self).__init__(parent)
project_combobox = QtWidgets.QComboBox(self)
# Change delegate so stylysheets are applied
project_delegate = QtWidgets.QStyledItemDelegate(project_combobox)
project_combobox.setItemDelegate(project_delegate)
project_combobox.setModel(project_handler.model)
model = ProjectModel(launcher_model)
project_combobox.setModel(model)
project_combobox.setRootModelIndex(QtCore.QModelIndex())
layout = QtWidgets.QHBoxLayout(self)
@ -42,16 +52,17 @@ class ProjectBar(QtWidgets.QWidget):
QtWidgets.QSizePolicy.Maximum
)
self.project_handler = project_handler
self._launcher_model = launcher_model
self.project_delegate = project_delegate
self.project_combobox = project_combobox
self._model = model
# Signals
self.project_combobox.currentIndexChanged.connect(self.on_index_change)
project_handler.project_changed.connect(self._on_project_change)
launcher_model.project_changed.connect(self._on_project_change)
# Set current project by default if it's set.
project_name = project_handler.current_project
project_name = launcher_model.project_name
if project_name:
self.set_project(project_name)
@ -67,7 +78,7 @@ class ProjectBar(QtWidgets.QWidget):
index = self.project_combobox.findText(project_name)
if index < 0:
# Try refresh combobox model
self.project_handler.refresh_model()
self._launcher_model.refresh_projects()
index = self.project_combobox.findText(project_name)
if index >= 0:
@ -78,7 +89,70 @@ class ProjectBar(QtWidgets.QWidget):
return
project_name = self.get_current_project()
self.project_handler.set_project(project_name)
self._launcher_model.set_project_name(project_name)
class LauncherTaskWidget(TasksWidget):
def __init__(self, launcher_model, *args, **kwargs):
self._launcher_model = launcher_model
super(LauncherTaskWidget, self).__init__(*args, **kwargs)
def _create_source_model(self):
return LauncherTaskModel(self._launcher_model, self._dbcon)
def _create_proxy_model(self, source_model):
proxy = LauncherTasksProxyModel(self._launcher_model)
proxy.setSourceModel(source_model)
return proxy
class LauncherAssetsWidget(SingleSelectAssetsWidget):
def __init__(self, launcher_model, *args, **kwargs):
self._launcher_model = launcher_model
super(LauncherAssetsWidget, self).__init__(*args, **kwargs)
launcher_model.assets_refresh_started.connect(self._on_refresh_start)
self.set_current_asset_btn_visibility(False)
def _on_refresh_start(self):
self._set_loading_state(loading=True, empty=True)
self.refresh_triggered.emit()
@property
def refreshing(self):
return self._model.refreshing
def refresh(self):
self._launcher_model.refresh_assets(force=True)
def stop_refresh(self):
raise ValueError("bug stop_refresh called")
def _refresh_model(self, clear=False):
raise ValueError("bug _refresh_model called")
def _create_source_model(self):
model = LauncherAssetsModel(self._launcher_model, self.dbcon)
model.refreshed.connect(self._on_model_refresh)
return model
def _create_proxy_model(self, source_model):
proxy = AssetRecursiveSortFilterModel(self._launcher_model)
proxy.setSourceModel(source_model)
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
return proxy
def _on_model_refresh(self, has_item):
self._proxy.sort(0)
self._set_loading_state(loading=False, empty=not has_item)
self.refreshed.emit()
def _on_filter_text_change(self, new_text):
self._launcher_model.set_asset_name_filter(new_text)
class ActionBar(QtWidgets.QWidget):
@ -86,10 +160,10 @@ class ActionBar(QtWidgets.QWidget):
action_clicked = QtCore.Signal(object)
def __init__(self, project_handler, dbcon, parent=None):
def __init__(self, launcher_model, dbcon, parent=None):
super(ActionBar, self).__init__(parent)
self.project_handler = project_handler
self._launcher_model = launcher_model
self.dbcon = dbcon
view = QtWidgets.QListView(self)
@ -136,7 +210,7 @@ class ActionBar(QtWidgets.QWidget):
self.set_row_height(1)
project_handler.projects_refreshed.connect(self._on_projects_refresh)
launcher_model.projects_refreshed.connect(self._on_projects_refresh)
view.clicked.connect(self.on_clicked)
view.customContextMenuRequested.connect(self.on_context_menu)
@ -182,8 +256,8 @@ class ActionBar(QtWidgets.QWidget):
self.update()
def _start_animation(self, index):
# Offset refresh timeout
self.project_handler.start_timer()
# Offset refresh timout
self._launcher_model.start_refresh_timer()
action_id = index.data(ACTION_ID_ROLE)
item = self.model.items_by_id.get(action_id)
if item:
@ -250,8 +324,8 @@ class ActionBar(QtWidgets.QWidget):
self.action_clicked.emit(action)
return
# Offset refresh timeout
self.project_handler.start_timer()
# Offset refresh timout
self._launcher_model.start_refresh_timer()
actions = index.data(ACTION_ROLE)

View file

@ -8,17 +8,19 @@ from avalon.api import AvalonMongoDB
from openpype import style
from openpype.api import resources
from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
from openpype.tools.utils.tasks_widget import TasksWidget
from avalon.vendor import qtawesome
from .models import ProjectModel
from .lib import get_action_label, ProjectHandler
from .models import (
LauncherModel,
ProjectModel
)
from .lib import get_action_label
from .widgets import (
ProjectBar,
ActionBar,
ActionHistory,
SlidePageWidget
SlidePageWidget,
LauncherAssetsWidget,
LauncherTaskWidget
)
from openpype.tools.flickcharm import FlickCharm
@ -89,15 +91,15 @@ class ProjectIconView(QtWidgets.QListView):
class ProjectsPanel(QtWidgets.QWidget):
"""Projects Page"""
def __init__(self, project_handler, parent=None):
def __init__(self, launcher_model, parent=None):
super(ProjectsPanel, self).__init__(parent=parent)
view = ProjectIconView(parent=self)
view.setSelectionMode(QtWidgets.QListView.NoSelection)
flick = FlickCharm(parent=self)
flick.activateOn(view)
view.setModel(project_handler.model)
model = ProjectModel(launcher_model)
view.setModel(model)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
@ -105,13 +107,14 @@ class ProjectsPanel(QtWidgets.QWidget):
view.clicked.connect(self.on_clicked)
self._model = model
self.view = view
self.project_handler = project_handler
self._launcher_model = launcher_model
def on_clicked(self, index):
if index.isValid():
project_name = index.data(QtCore.Qt.DisplayRole)
self.project_handler.set_project(project_name)
self._launcher_model.set_project_name(project_name)
class AssetsPanel(QtWidgets.QWidget):
@ -119,7 +122,7 @@ class AssetsPanel(QtWidgets.QWidget):
back_clicked = QtCore.Signal()
session_changed = QtCore.Signal()
def __init__(self, project_handler, dbcon, parent=None):
def __init__(self, launcher_model, dbcon, parent=None):
super(AssetsPanel, self).__init__(parent=parent)
self.dbcon = dbcon
@ -129,7 +132,7 @@ class AssetsPanel(QtWidgets.QWidget):
btn_back = QtWidgets.QPushButton(self)
btn_back.setIcon(btn_back_icon)
project_bar = ProjectBar(project_handler, self)
project_bar = ProjectBar(launcher_model, self)
project_bar_layout = QtWidgets.QHBoxLayout()
project_bar_layout.setContentsMargins(0, 0, 0, 0)
@ -138,12 +141,14 @@ class AssetsPanel(QtWidgets.QWidget):
project_bar_layout.addWidget(project_bar)
# Assets widget
assets_widget = SingleSelectAssetsWidget(dbcon=self.dbcon, parent=self)
assets_widget = LauncherAssetsWidget(
launcher_model, dbcon=self.dbcon, parent=self
)
# Make assets view flickable
assets_widget.activate_flick_charm()
# Tasks widget
tasks_widget = TasksWidget(self.dbcon, self)
tasks_widget = LauncherTaskWidget(launcher_model, self.dbcon, self)
# Body
body = QtWidgets.QSplitter(self)
@ -165,19 +170,20 @@ class AssetsPanel(QtWidgets.QWidget):
layout.addWidget(body)
# signals
project_handler.project_changed.connect(self._on_project_changed)
launcher_model.project_changed.connect(self._on_project_changed)
assets_widget.selection_changed.connect(self._on_asset_changed)
assets_widget.refreshed.connect(self._on_asset_changed)
tasks_widget.task_changed.connect(self._on_task_change)
btn_back.clicked.connect(self.back_clicked)
self.project_handler = project_handler
self.project_bar = project_bar
self.assets_widget = assets_widget
self._tasks_widget = tasks_widget
self._btn_back = btn_back
self._launcher_model = launcher_model
def select_asset(self, asset_name):
self.assets_widget.select_asset_by_name(asset_name)
@ -196,8 +202,6 @@ class AssetsPanel(QtWidgets.QWidget):
def _on_project_changed(self):
self.session_changed.emit()
self.assets_widget.refresh()
def _on_asset_changed(self):
"""Callback on asset selection changed
@ -250,18 +254,17 @@ class LauncherWindow(QtWidgets.QDialog):
| QtCore.Qt.WindowCloseButtonHint
)
project_model = ProjectModel(self.dbcon)
project_handler = ProjectHandler(self.dbcon, project_model)
launcher_model = LauncherModel(self.dbcon)
project_panel = ProjectsPanel(project_handler)
asset_panel = AssetsPanel(project_handler, self.dbcon)
project_panel = ProjectsPanel(launcher_model)
asset_panel = AssetsPanel(launcher_model, self.dbcon)
page_slider = SlidePageWidget()
page_slider.addWidget(project_panel)
page_slider.addWidget(asset_panel)
# actions
actions_bar = ActionBar(project_handler, self.dbcon, self)
actions_bar = ActionBar(launcher_model, self.dbcon, self)
# statusbar
message_label = QtWidgets.QLabel(self)
@ -303,8 +306,8 @@ class LauncherWindow(QtWidgets.QDialog):
# signals
actions_bar.action_clicked.connect(self.on_action_clicked)
action_history.trigger_history.connect(self.on_history_action)
project_handler.project_changed.connect(self.on_project_change)
project_handler.timer_timeout.connect(self._on_refresh_timeout)
launcher_model.project_changed.connect(self.on_project_change)
launcher_model.timer_timeout.connect(self._on_refresh_timeout)
asset_panel.back_clicked.connect(self.on_back_clicked)
asset_panel.session_changed.connect(self.on_session_changed)
@ -314,7 +317,7 @@ class LauncherWindow(QtWidgets.QDialog):
self._message_timer = message_timer
self.project_handler = project_handler
self._launcher_model = launcher_model
self._message_label = message_label
self.project_panel = project_panel
@ -324,19 +327,19 @@ class LauncherWindow(QtWidgets.QDialog):
self.page_slider = page_slider
def showEvent(self, event):
self.project_handler.set_active(True)
self.project_handler.start_timer(True)
self._launcher_model.set_active(True)
self._launcher_model.start_refresh_timer(True)
super(LauncherWindow, self).showEvent(event)
def _on_refresh_timeout(self):
# Stop timer if widget is not visible
if not self.isVisible():
self.project_handler.stop_timer()
self._launcher_model.stop_refresh_timer()
def changeEvent(self, event):
if event.type() == QtCore.QEvent.ActivationChange:
self.project_handler.set_active(self.isActiveWindow())
self._launcher_model.set_active(self.isActiveWindow())
super(LauncherWindow, self).changeEvent(event)
def set_page(self, page):
@ -371,7 +374,7 @@ class LauncherWindow(QtWidgets.QDialog):
self.discover_actions()
def on_back_clicked(self):
self.project_handler.set_project(None)
self._launcher_model.set_project_name(None)
self.set_page(0)
self.discover_actions()

View file

@ -152,6 +152,8 @@ class Controller(QtCore.QObject):
self.instance_toggled.connect(self._on_instance_toggled)
self._main_thread_processor = MainThreadProcess()
self._current_state = ""
def reset_variables(self):
self.log.debug("Resetting pyblish context variables")
@ -159,6 +161,7 @@ class Controller(QtCore.QObject):
self.is_running = False
self.stopped = False
self.errored = False
self._current_state = ""
# Active producer of pairs
self.pair_generator = None
@ -167,7 +170,6 @@ class Controller(QtCore.QObject):
# Orders which changes GUI
# - passing collectors order disables plugin/instance toggle
self.collectors_order = None
self.collect_state = 0
# - passing validators order disables validate button and gives ability
@ -176,11 +178,8 @@ class Controller(QtCore.QObject):
self.validated = False
# Get collectors and validators order
self.order_groups.reset()
plugin_groups = self.order_groups.groups()
plugin_groups_keys = list(plugin_groups.keys())
self.collectors_order = plugin_groups_keys[0]
self.validators_order = self.order_groups.validation_order()
plugin_groups_keys = list(self.order_groups.groups.keys())
self.validators_order = self.order_groups.validation_order
next_group_order = None
if len(plugin_groups_keys) > 1:
next_group_order = plugin_groups_keys[1]
@ -191,13 +190,18 @@ class Controller(QtCore.QObject):
"stop_on_validation": False,
# Used?
"last_plugin_order": None,
"current_group_order": self.collectors_order,
"current_group_order": plugin_groups_keys[0],
"next_group_order": next_group_order,
"nextOrder": None,
"ordersWithError": set()
}
self._set_state_by_order()
self.log.debug("Reset of pyblish context variables done")
@property
def current_state(self):
return self._current_state
def presets_by_hosts(self):
# Get global filters as base
presets = get_project_settings(os.environ['AVALON_PROJECT']) or {}
@ -293,6 +297,9 @@ class Controller(QtCore.QObject):
def on_published(self):
if self.is_running:
self.is_running = False
self._current_state = (
"Published" if not self.errored else "Published, with errors"
)
self.was_finished.emit()
self._main_thread_processor.stop()
@ -355,7 +362,7 @@ class Controller(QtCore.QObject):
new_current_group_order = self.processing["next_group_order"]
if new_current_group_order is not None:
current_next_order_found = False
for order in self.order_groups.groups().keys():
for order in self.order_groups.groups.keys():
if current_next_order_found:
new_next_group_order = order
break
@ -370,6 +377,10 @@ class Controller(QtCore.QObject):
if self.collect_state == 0:
self.collect_state = 1
self._current_state = (
"Ready" if not self.errored else
"Collected, with errors"
)
self.switch_toggleability.emit(True)
self.passed_group.emit(current_group_order)
yield IterationBreak("Collected")
@ -377,6 +388,11 @@ class Controller(QtCore.QObject):
else:
self.passed_group.emit(current_group_order)
if self.errored:
self._current_state = (
"Stopped, due to errors" if not
self.processing["stop_on_validation"] else
"Validated, with errors"
)
yield IterationBreak("Last group errored")
if self.collect_state == 1:
@ -386,17 +402,23 @@ class Controller(QtCore.QObject):
if not self.validated and plugin.order > self.validators_order:
self.validated = True
if self.processing["stop_on_validation"]:
self._current_state = (
"Validated" if not self.errored else
"Validated, with errors"
)
yield IterationBreak("Validated")
# Stop if was stopped
if self.stopped:
self.stopped = False
self._current_state = "Paused"
yield IterationBreak("Stopped")
# check test if will stop
self.processing["nextOrder"] = plugin.order
message = self.test(**self.processing)
if message:
self._current_state = "Paused"
yield IterationBreak("Stopped due to \"{}\"".format(message))
self.processing["last_plugin_order"] = plugin.order
@ -426,6 +448,7 @@ class Controller(QtCore.QObject):
# Stop if was stopped
if self.stopped:
self.stopped = False
self._current_state = "Paused"
yield IterationBreak("Stopped")
yield (plugin, instance)
@ -536,20 +559,27 @@ class Controller(QtCore.QObject):
MainThreadItem(on_next)
)
def _set_state_by_order(self):
order = self.processing["current_group_order"]
self._current_state = self.order_groups.groups[order]["state"]
def collect(self):
""" Iterate and process Collect plugins
- load_plugins method is launched again when finished
"""
self._set_state_by_order()
self._main_thread_processor.process(self._start_collect)
self._main_thread_processor.start()
def validate(self):
""" Process plugins to validations_order value."""
self._set_state_by_order()
self._main_thread_processor.process(self._start_validate)
self._main_thread_processor.start()
def publish(self):
""" Iterate and process all remaining plugins."""
self._set_state_by_order()
self._main_thread_processor.process(self._start_publish)
self._main_thread_processor.start()

View file

@ -428,12 +428,12 @@ class PluginModel(QtGui.QStandardItemModel):
self.clear()
def append(self, plugin):
plugin_groups = self.controller.order_groups.groups()
plugin_groups = self.controller.order_groups.groups
label = None
order = None
for _order, _label in reversed(plugin_groups.items()):
for _order, item in reversed(plugin_groups.items()):
if _order is None or plugin.order < _order:
label = _label
label = item["label"]
order = _order
else:
break

View file

@ -95,224 +95,44 @@ def collect_families_from_instances(instances, only_active=False):
class OrderGroups:
# Validator order can be set with environment "PYBLISH_VALIDATION_ORDER"
# - this variable sets when validation button will hide and proecssing
# of validation will end with ability to continue in process
default_validation_order = pyblish.api.ValidatorOrder + 0.5
# Group range can be set with environment "PYBLISH_GROUP_RANGE"
default_group_range = 1
# Group string can be set with environment "PYBLISH_GROUP_SETTING"
default_groups = {
pyblish.api.CollectorOrder + 0.5: "Collect",
pyblish.api.ValidatorOrder + 0.5: "Validate",
pyblish.api.ExtractorOrder + 0.5: "Extract",
pyblish.api.IntegratorOrder + 0.5: "Integrate",
None: "Other"
}
# *** This example should have same result as is `default_groups` if
# `group_range` is set to "1"
__groups_str_example__ = (
# half of `group_range` is added to 0 because number means it is Order
"0=Collect"
# if `<` is before than it means group range is not used
# but is expected that number is already max
",<1.5=Validate"
# "Extractor" will be used in range `<1.5; 2.5)`
",<2.5=Extract"
",<3.5=Integrate"
# "Other" if number is not set than all remaining plugins are in
# - in this case Other's range is <3.5; infinity)
",Other"
)
_groups = None
_validation_order = None
_group_range = None
def __init__(
self, group_str=None, group_range=None, validation_order=None
):
super(OrderGroups, self).__init__()
# Override class methods with object methods
self.groups = self._object_groups
self.validation_order = self._object_validation_order
self.group_range = self._object_group_range
self.reset = self._object_reset
# set
if group_range is not None:
self._group_range = self.parse_group_range(
group_range
)
if group_str is not None:
self._groups = self.parse_group_str(
group_str
)
if validation_order is not None:
self._validation_order = self.parse_validation_order(
validation_order
)
@staticmethod
def _groups_method(obj):
if obj._groups is None:
obj._groups = obj.parse_group_str(
group_range=obj.group_range()
)
return obj._groups
@staticmethod
def _reset_method(obj):
obj._groups = None
obj._validation_order = None
obj._group_range = None
@classmethod
def reset(cls):
return cls._reset_method(cls)
def _object_reset(self):
return self._reset_method(self)
@classmethod
def groups(cls):
return cls._groups_method(cls)
def _object_groups(self):
return self._groups_method(self)
@staticmethod
def _validation_order_method(obj):
if obj._validation_order is None:
obj._validation_order = obj.parse_validation_order(
group_range=obj.group_range()
)
return obj._validation_order
@classmethod
def validation_order(cls):
return cls._validation_order_method(cls)
def _object_validation_order(self):
return self._validation_order_method(self)
@staticmethod
def _group_range_method(obj):
if obj._group_range is None:
obj._group_range = obj.parse_group_range()
return obj._group_range
@classmethod
def group_range(cls):
return cls._group_range_method(cls)
def _object_group_range(self):
return self._group_range_method(self)
@staticmethod
def sort_groups(_groups_dict):
sorted_dict = collections.OrderedDict()
# make sure won't affect any dictionary as pointer
groups_dict = copy.deepcopy(_groups_dict)
last_order = None
if None in groups_dict:
last_order = groups_dict.pop(None)
for key in sorted(groups_dict):
sorted_dict[key] = groups_dict[key]
if last_order is not None:
sorted_dict[None] = last_order
return sorted_dict
@staticmethod
def parse_group_str(groups_str=None, group_range=None):
if groups_str is None:
groups_str = os.environ.get("PYBLISH_GROUP_SETTING")
if groups_str is None:
return OrderGroups.sort_groups(OrderGroups.default_groups)
items = groups_str.split(",")
groups = {}
for item in items:
if "=" not in item:
order = None
label = item
else:
order, label = item.split("=")
order = order.strip()
if not order:
order = None
elif order.startswith("<"):
order = float(order.replace("<", ""))
else:
if group_range is None:
group_range = OrderGroups.default_group_range
print(
"Using default Plugin group range \"{}\".".format(
OrderGroups.default_group_range
)
)
order = float(order) + float(group_range) / 2
if order in groups:
print((
"Order \"{}\" is registered more than once."
" Using first found."
).format(str(order)))
continue
groups[order] = label
return OrderGroups.sort_groups(groups)
@staticmethod
def parse_validation_order(validation_order_value=None, group_range=None):
if validation_order_value is None:
validation_order_value = os.environ.get("PYBLISH_VALIDATION_ORDER")
if validation_order_value is None:
return OrderGroups.default_validation_order
if group_range is None:
group_range = OrderGroups.default_group_range
group_range_half = float(group_range) / 2
if isinstance(validation_order_value, numbers.Integral):
return validation_order_value + group_range_half
if validation_order_value.startswith("<"):
validation_order_value = float(
validation_order_value.replace("<", "")
)
else:
validation_order_value = (
float(validation_order_value)
+ group_range_half
)
return validation_order_value
@staticmethod
def parse_group_range(group_range=None):
if group_range is None:
group_range = os.environ.get("PYBLISH_GROUP_RANGE")
if group_range is None:
return OrderGroups.default_group_range
if isinstance(group_range, numbers.Integral):
return group_range
return float(group_range)
validation_order = pyblish.api.ValidatorOrder + 0.5
groups = collections.OrderedDict((
(
pyblish.api.CollectorOrder + 0.5,
{
"label": "Collect",
"state": "Collecting.."
}
),
(
pyblish.api.ValidatorOrder + 0.5,
{
"label": "Validate",
"state": "Validating.."
}
),
(
pyblish.api.ExtractorOrder + 0.5,
{
"label": "Extract",
"state": "Extracting.."
}
),
(
pyblish.api.IntegratorOrder + 0.5,
{
"label": "Integrate",
"state": "Integrating.."
}
),
(
None,
{
"label": "Other",
"state": "Finishing.."
}
)
))
def env_variable_to_bool(env_key, default=False):

View file

@ -296,25 +296,6 @@ class Window(QtWidgets.QDialog):
self.main_layout.setSpacing(0)
self.main_layout.addWidget(main_widget)
# Display info
info_effect = QtWidgets.QGraphicsOpacityEffect(footer_info)
footer_info.setGraphicsEffect(info_effect)
on = QtCore.QPropertyAnimation(info_effect, b"opacity")
on.setDuration(0)
on.setStartValue(0)
on.setEndValue(1)
fade = QtCore.QPropertyAnimation(info_effect, b"opacity")
fade.setDuration(500)
fade.setStartValue(1.0)
fade.setEndValue(0.0)
animation_info_msg = QtCore.QSequentialAnimationGroup()
animation_info_msg.addAnimation(on)
animation_info_msg.addPause(2000)
animation_info_msg.addAnimation(fade)
"""Setup
Widgets are referred to in CSS via their object-name. We
@ -448,6 +429,8 @@ class Window(QtWidgets.QDialog):
self.footer_button_validate = footer_button_validate
self.footer_button_play = footer_button_play
self.footer_info = footer_info
self.overview_instance_view = overview_instance_view
self.overview_plugin_view = overview_plugin_view
self.plugin_model = plugin_model
@ -457,8 +440,6 @@ class Window(QtWidgets.QDialog):
self.presets_button = presets_button
self.animation_info_msg = animation_info_msg
self.terminal_model = terminal_model
self.terminal_proxy = terminal_proxy
self.terminal_view = terminal_view
@ -984,6 +965,8 @@ class Window(QtWidgets.QDialog):
self.footer_button_stop.setEnabled(True)
self.footer_button_play.setEnabled(False)
self._update_state()
def on_passed_group(self, order):
for group_item in self.instance_model.group_items.values():
group_index = self.instance_sort_proxy.mapFromSource(
@ -1015,6 +998,8 @@ class Window(QtWidgets.QDialog):
self.overview_plugin_view.setAnimated(False)
self.overview_plugin_view.collapse(group_index)
self._update_state()
def on_was_stopped(self):
self.overview_plugin_view.setAnimated(settings.Animated)
errored = self.controller.errored
@ -1039,6 +1024,9 @@ class Window(QtWidgets.QDialog):
and not self.controller.stopped
)
self.button_suspend_logs.setEnabled(suspend_log_bool)
self._update_state()
if not self.isVisible():
self.setVisible(True)
@ -1084,6 +1072,7 @@ class Window(QtWidgets.QDialog):
)
self.update_compatibility()
self._update_state()
def on_was_processed(self, result):
existing_ids = set(self.instance_model.instance_items.keys())
@ -1165,6 +1154,8 @@ class Window(QtWidgets.QDialog):
self.controller.validate()
self._update_state()
def publish(self):
self.info(self.tr("Preparing publish.."))
self.footer_button_stop.setEnabled(True)
@ -1176,6 +1167,8 @@ class Window(QtWidgets.QDialog):
self.controller.publish()
self._update_state()
def act(self, plugin_item, action):
self.info("%s %s.." % (self.tr("Preparing"), action))
@ -1290,6 +1283,9 @@ class Window(QtWidgets.QDialog):
#
# -------------------------------------------------------------------------
def _update_state(self):
self.footer_info.setText(self.controller.current_state)
def info(self, message):
"""Print user-facing information
@ -1297,19 +1293,12 @@ class Window(QtWidgets.QDialog):
message (str): Text message for the user
"""
info = self.findChild(QtWidgets.QLabel, "FooterInfo")
info.setText(message)
# Include message in terminal
self.terminal_model.append([{
"label": message,
"type": "info"
}])
self.animation_info_msg.stop()
self.animation_info_msg.start()
if settings.PrintInfo:
# Print message to console
util.u_print(message)

View file

@ -91,6 +91,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
state_changed = QtCore.Signal()
saved = QtCore.Signal(QtWidgets.QWidget)
restart_required_trigger = QtCore.Signal()
reset_started = QtCore.Signal()
reset_finished = QtCore.Signal()
full_path_requested = QtCore.Signal(str, str)
require_restart_label_text = (
@ -379,7 +381,12 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
"""Change path of widget based on category full path."""
pass
def change_path(self, path):
"""Change path and go to widget."""
self.breadcrumbs_bar.change_path(path)
def set_path(self, path):
"""Called from clicked widget."""
self.breadcrumbs_bar.set_path(path)
def _add_developer_ui(self, footer_layout, footer_widget):
@ -492,6 +499,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
self._update_labels_visibility()
def reset(self):
self.reset_started.emit()
self.set_state(CategoryState.Working)
self._on_reset_start()
@ -596,6 +604,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
self._on_reset_crash()
else:
self._on_reset_success()
self.reset_finished.emit()
def _on_source_version_change(self, version):
if self._updating_root:

View file

@ -0,0 +1,186 @@
import re
import collections
from Qt import QtCore, QtWidgets, QtGui
ENTITY_LABEL_ROLE = QtCore.Qt.UserRole + 1
ENTITY_PATH_ROLE = QtCore.Qt.UserRole + 2
def get_entity_children(entity):
# TODO find better way how to go through all children
if hasattr(entity, "values"):
return entity.values()
return []
class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
"""Filters recursively to regex in all columns"""
def __init__(self):
super(RecursiveSortFilterProxyModel, self).__init__()
# Note: Recursive filtering was introduced in Qt 5.10.
self.setRecursiveFilteringEnabled(True)
def filterAcceptsRow(self, row, parent):
if not parent.isValid():
return False
regex = self.filterRegExp()
if not regex.isEmpty() and regex.isValid():
pattern = regex.pattern()
compiled_regex = re.compile(pattern)
source_model = self.sourceModel()
# Check current index itself in all columns
source_index = source_model.index(row, 0, parent)
if source_index.isValid():
for role in (ENTITY_PATH_ROLE, ENTITY_LABEL_ROLE):
value = source_model.data(source_index, role)
if value and compiled_regex.search(value):
return True
return False
return super(
RecursiveSortFilterProxyModel, self
).filterAcceptsRow(row, parent)
class SearchEntitiesDialog(QtWidgets.QDialog):
path_clicked = QtCore.Signal(str)
def __init__(self, parent):
super(SearchEntitiesDialog, self).__init__(parent=parent)
self.setWindowTitle("Search Settings")
filter_edit = QtWidgets.QLineEdit(self)
filter_edit.setPlaceholderText("Search...")
model = EntityTreeModel()
proxy = RecursiveSortFilterProxyModel()
proxy.setSourceModel(model)
proxy.setDynamicSortFilter(True)
view = QtWidgets.QTreeView(self)
view.setAllColumnsShowFocus(True)
view.setSortingEnabled(True)
view.setModel(proxy)
model.setColumnCount(3)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(filter_edit)
layout.addWidget(view)
filter_changed_timer = QtCore.QTimer()
filter_changed_timer.setInterval(200)
view.selectionModel().selectionChanged.connect(
self._on_selection_change
)
filter_changed_timer.timeout.connect(self._on_filter_timer)
filter_edit.textChanged.connect(self._on_filter_changed)
self._filter_edit = filter_edit
self._model = model
self._proxy = proxy
self._view = view
self._filter_changed_timer = filter_changed_timer
self._first_show = True
def set_root_entity(self, entity):
self._model.set_root_entity(entity)
self._view.resizeColumnToContents(0)
def showEvent(self, event):
super(SearchEntitiesDialog, self).showEvent(event)
if self._first_show:
self._first_show = False
self.resize(700, 500)
def _on_filter_changed(self, txt):
self._filter_changed_timer.start()
def _on_filter_timer(self):
text = self._filter_edit.text()
self._proxy.setFilterRegExp(text)
# WARNING This expanding and resizing is relatively slow.
self._view.expandAll()
self._view.resizeColumnToContents(0)
def _on_selection_change(self):
current = self._view.currentIndex()
path = current.data(ENTITY_PATH_ROLE)
self.path_clicked.emit(path)
class EntityTreeModel(QtGui.QStandardItemModel):
def __init__(self, *args, **kwargs):
super(EntityTreeModel, self).__init__(*args, **kwargs)
self.setColumnCount(3)
def data(self, index, role=None):
if role is None:
role = QtCore.Qt.DisplayRole
col = index.column()
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
if col == 0:
pass
elif col == 1:
role = ENTITY_LABEL_ROLE
elif col == 2:
role = ENTITY_PATH_ROLE
if col > 0:
index = self.index(index.row(), 0, index.parent())
return super(EntityTreeModel, self).data(index, role)
def flags(self, index):
if index.column() > 0:
index = self.index(index.row(), 0, index.parent())
return super(EntityTreeModel, self).flags(index)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if section == 0:
return "Key"
elif section == 1:
return "Label"
elif section == 2:
return "Path"
return ""
return super(EntityTreeModel, self).headerData(
section, orientation, role
)
def set_root_entity(self, root_entity):
parent = self.invisibleRootItem()
parent.removeRows(0, parent.rowCount())
if not root_entity:
return
# We don't want to see the root entity so we directly add its children
fill_queue = collections.deque()
fill_queue.append((root_entity, parent))
cols = self.columnCount()
while fill_queue:
parent_entity, parent_item = fill_queue.popleft()
child_items = []
for child in get_entity_children(parent_entity):
label = child.label
path = child.path
key = path.split("/")[-1]
item = QtGui.QStandardItem(key)
item.setEditable(False)
item.setData(label, ENTITY_LABEL_ROLE)
item.setData(path, ENTITY_PATH_ROLE)
item.setColumnCount(cols)
child_items.append(item)
fill_queue.append((child, item))
if child_items:
parent_item.appendRows(child_items)

View file

@ -9,6 +9,7 @@ from .widgets import (
RestartDialog,
SettingsTabWidget
)
from .search_dialog import SearchEntitiesDialog
from openpype import style
from openpype.lib import is_admin_password_required
@ -58,15 +59,22 @@ class MainWidget(QtWidgets.QWidget):
self.setLayout(layout)
search_dialog = SearchEntitiesDialog(self)
self._shadow_widget = ShadowWidget("Working...", self)
self._shadow_widget.setVisible(False)
header_tab_widget.currentChanged.connect(self._on_tab_changed)
search_dialog.path_clicked.connect(self._on_search_path_clicked)
for tab_widget in tab_widgets:
tab_widget.saved.connect(self._on_tab_save)
tab_widget.state_changed.connect(self._on_state_change)
tab_widget.restart_required_trigger.connect(
self._on_restart_required
)
tab_widget.reset_started.connect(self._on_reset_started)
tab_widget.reset_started.connect(self._on_reset_finished)
tab_widget.full_path_requested.connect(self._on_full_path_request)
header_tab_widget.context_menu_requested.connect(
@ -75,6 +83,7 @@ class MainWidget(QtWidgets.QWidget):
self._header_tab_widget = header_tab_widget
self.tab_widgets = tab_widgets
self._search_dialog = search_dialog
def _on_tab_save(self, source_widget):
for tab_widget in self.tab_widgets:
@ -170,6 +179,21 @@ class MainWidget(QtWidgets.QWidget):
for tab_widget in self.tab_widgets:
tab_widget.reset()
def _update_search_dialog(self, clear=False):
if self._search_dialog.isVisible():
entity = None
if not clear:
widget = self._header_tab_widget.currentWidget()
entity = widget.entity
self._search_dialog.set_root_entity(entity)
def _on_tab_changed(self):
self._update_search_dialog()
def _on_search_path_clicked(self, path):
widget = self._header_tab_widget.currentWidget()
widget.change_path(path)
def _on_restart_required(self):
# Don't show dialog if there are not registered slots for
# `trigger_restart` signal.
@ -184,3 +208,26 @@ class MainWidget(QtWidgets.QWidget):
result = dialog.exec_()
if result == 1:
self.trigger_restart.emit()
def _on_reset_started(self):
widget = self.sender()
current_widget = self._header_tab_widget.currentWidget()
if current_widget is widget:
self._update_search_dialog(True)
def _on_reset_finished(self):
widget = self.sender()
current_widget = self._header_tab_widget.currentWidget()
if current_widget is widget:
self._update_search_dialog()
def keyPressEvent(self, event):
if event.matches(QtGui.QKeySequence.Find):
# todo: search in all widgets (or in active)?
widget = self._header_tab_widget.currentWidget()
self._search_dialog.show()
self._search_dialog.set_root_entity(widget.entity)
event.accept()
return
return super(MainWidget, self).keyPressEvent(event)

View file

@ -354,7 +354,6 @@ class AssetModel(QtGui.QStandardItemModel):
Args:
force (bool): Stop currently running refresh start new refresh.
clear (bool): Clear model before refresh thread starts.
"""
# Skip fetch if there is already other thread fetching documents
if self._refreshing:

View file

@ -9,6 +9,7 @@ from .views import DeselectableTreeView
TASK_NAME_ROLE = QtCore.Qt.UserRole + 1
TASK_TYPE_ROLE = QtCore.Qt.UserRole + 2
TASK_ORDER_ROLE = QtCore.Qt.UserRole + 3
TASK_ASSIGNEE_ROLE = QtCore.Qt.UserRole + 4
class TasksModel(QtGui.QStandardItemModel):
@ -144,11 +145,19 @@ class TasksModel(QtGui.QStandardItemModel):
task_type_icon = task_type_info.get("icon")
icon = self._get_icon(task_icon, task_type_icon)
task_assignees = set()
assignees_data = task_info.get("assignees") or []
for assignee in assignees_data:
username = assignee.get("username")
if username:
task_assignees.add(username)
label = "{} ({})".format(task_name, task_type or "type N/A")
item = QtGui.QStandardItem(label)
item.setData(task_name, TASK_NAME_ROLE)
item.setData(task_type, TASK_TYPE_ROLE)
item.setData(task_order, TASK_ORDER_ROLE)
item.setData(task_assignees, TASK_ASSIGNEE_ROLE)
item.setData(icon, QtCore.Qt.DecorationRole)
item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
items.append(item)

View file

@ -726,9 +726,9 @@ class FilesWidget(QtWidgets.QWidget):
self.file_opened.emit()
def save_changes_prompt(self):
self._messagebox = messagebox = QtWidgets.QMessageBox()
messagebox.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self._messagebox = messagebox = QtWidgets.QMessageBox(parent=self)
messagebox.setWindowFlags(messagebox.windowFlags() |
QtCore.Qt.FramelessWindowHint)
messagebox.setIcon(messagebox.Warning)
messagebox.setWindowTitle("Unsaved Changes!")
messagebox.setText(
@ -739,10 +739,6 @@ class FilesWidget(QtWidgets.QWidget):
messagebox.Yes | messagebox.No | messagebox.Cancel
)
# Parenting the QMessageBox to the Widget seems to crash
# so we skip parenting and explicitly apply the stylesheet.
messagebox.setStyle(self.style())
result = messagebox.exec_()
if result == messagebox.Yes:
return True

View file

@ -9,7 +9,7 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
:::warning
Before you are able to start with OpenPype tools in DaVinci Resolve, installation of its own Python 3.6 interpreter and PySide 2 has to be done. Go to [Installation of python and pyside](#installation-of-python-and-pyside) link for more information
Before you are able to start with OpenPype tools in DaVinci Resolve, installation of its own Python 3.6 interpreter and PySide 2 has to be done. Go to [Installation of python and pyside](admin_hosts_resolve.md#installation-of-python-and-pyside) link for more information
:::

View file

@ -47,7 +47,7 @@ It is possible to create multiple tokens and configure different scopes for them
### Profiles
Profiles are used to select when to trigger notification. One or multiple profiles
could be configured, `Families`, `Task names` (regex available), `Host names` combination is needed.
could be configured, `Families`, `Task names` (regex available), `Host names`, `Subset names` (regex available) combination is needed.
Eg. If I want to be notified when render is published from Maya, setting is: