[Automated] Merged develop into main
|
|
@ -17,11 +17,12 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
|||
"nuke",
|
||||
"nukex",
|
||||
"hiero",
|
||||
"houdini",
|
||||
"nukestudio",
|
||||
"blender",
|
||||
"photoshop",
|
||||
"tvpaint",
|
||||
"afftereffects"
|
||||
"aftereffects"
|
||||
]
|
||||
|
||||
def execute(self):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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..")
|
||||
|
|
@ -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()]
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ DEFAULT_OPENPYPE_MODULES = (
|
|||
"standalonepublish_action",
|
||||
"job_queue",
|
||||
"timers_manager",
|
||||
"sync_server",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1,011 B After Width: | Height: | Size: 1,011 B |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 980 B After Width: | Height: | Size: 980 B |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1,011 B After Width: | Height: | Size: 1,011 B |
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
"create": {
|
||||
"CreateShotClip": {
|
||||
"hierarchy": "{folder}/{sequence}",
|
||||
"clipRename": true,
|
||||
"useShotName": true,
|
||||
"clipRename": false,
|
||||
"clipName": "{sequence}{shot}",
|
||||
"segmentIndex": true,
|
||||
"countFrom": 10,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"hosts": [],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"subsets": [],
|
||||
"channel_messages": []
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@
|
|||
"key": "hierarchy",
|
||||
"label": "Shot parent hierarchy"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "useShotName",
|
||||
"label": "Use Shot Name"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "clipRename",
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@
|
|||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"key": "subsets",
|
||||
"label": "Subset names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
186
openpype/tools/settings/settings/search_dialog.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
:::
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||