diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index 653f97b3dd..8edccd48d4 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -17,11 +17,12 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "nuke", "nukex", "hiero", + "houdini", "nukestudio", "blender", "photoshop", "tvpaint", - "afftereffects" + "aftereffects" ] def execute(self): diff --git a/openpype/hooks/pre_copy_template_workfile.py b/openpype/hooks/pre_copy_template_workfile.py index 5c56d721e8..dffac22ee2 100644 --- a/openpype/hooks/pre_copy_template_workfile.py +++ b/openpype/hooks/pre_copy_template_workfile.py @@ -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) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index bbb7c38119..74d9e7607a 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -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, diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index db1793cba8..ec49db1601 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -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( diff --git a/openpype/hosts/flame/plugins/create/create_shot_clip.py b/openpype/hosts/flame/plugins/create/create_shot_clip.py index f055c77a89..11c00dab42 100644 --- a/openpype/hosts/flame/plugins/create/create_shot_clip.py +++ b/openpype/hosts/flame/plugins/create/create_shot_clip.py @@ -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": { diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 72f1c8e71f..5a087ea276 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -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) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index c3dbdc5ef5..1c08e72d65 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -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.""" diff --git a/openpype/hosts/houdini/plugins/publish/save_scene.py b/openpype/hosts/houdini/plugins/publish/save_scene.py index 1b12efa603..fe5962fbd3 100644 --- a/openpype/hosts/houdini/plugins/publish/save_scene.py +++ b/openpype/hosts/houdini/plugins/publish/save_scene.py @@ -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() diff --git a/openpype/hosts/houdini/plugins/publish/save_scene_deadline.py b/openpype/hosts/houdini/plugins/publish/save_scene_deadline.py deleted file mode 100644 index a0efd0610c..0000000000 --- a/openpype/hosts/houdini/plugins/publish/save_scene_deadline.py +++ /dev/null @@ -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..") diff --git a/openpype/hosts/houdini/plugins/publish/validate_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_output_node.py deleted file mode 100644 index 0b60ab5c48..0000000000 --- a/openpype/hosts/houdini/plugins/publish/validate_output_node.py +++ /dev/null @@ -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()] diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index b8c7f93d76..abfa3f136e 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -66,6 +66,14 @@ host_tools.show_workfiles(parent) ]]> + + + + + diff --git a/openpype/hosts/houdini/startup/scripts/123.py b/openpype/hosts/houdini/startup/python2.7libs/pythonrc.py similarity index 100% rename from openpype/hosts/houdini/startup/scripts/123.py rename to openpype/hosts/houdini/startup/python2.7libs/pythonrc.py diff --git a/openpype/hosts/houdini/startup/scripts/houdinicore.py b/openpype/hosts/houdini/startup/python3.7libs/pythonrc.py similarity index 100% rename from openpype/hosts/houdini/startup/scripts/houdinicore.py rename to openpype/hosts/houdini/startup/python3.7libs/pythonrc.py diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 9ea798e927..5d76bf0f04 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -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", diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 09d1a68223..0b05182bb1 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -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 = [] diff --git a/openpype/hosts/maya/api/menu.json b/openpype/hosts/maya/api/menu.json deleted file mode 100644 index a2efd5233c..0000000000 --- a/openpype/hosts/maya/api/menu.json +++ /dev/null @@ -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": "" - } - ] - } -] diff --git a/openpype/hosts/maya/api/menu_backup.json b/openpype/hosts/maya/api/menu_backup.json deleted file mode 100644 index e2a558aedc..0000000000 --- a/openpype/hosts/maya/api/menu_backup.json +++ /dev/null @@ -1,1567 +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\\show_current_scene_in_explorer.py", - "sourcetype": "file", - "title": "Explore current scene..", - "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": "separator" - }, - { - "type": "menu", - "title": "Modeling", - "items": [ - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\modeling\\duplicate_normalized.py", - "sourcetype": "file", - "tags": ["modeling", "duplicate", "normalized"], - "title": "Duplicate Normalized", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\modeling\\transferUVs.py", - "sourcetype": "file", - "tags": ["modeling", "transfer", "uv"], - "title": "Transfer UVs", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\modeling\\mirrorSymmetry.py", - "sourcetype": "file", - "tags": ["modeling", "mirror", "symmetry"], - "title": "Mirror Symmetry", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\modeling\\selectOutlineUI.py", - "sourcetype": "file", - "tags": ["modeling", "select", "outline", "ui"], - "title": "Select Outline UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\modeling\\polyDeleteOtherUVSets.py", - "sourcetype": "file", - "tags": ["modeling", "polygon", "uvset", "delete"], - "title": "Polygon Delete Other UV Sets", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\modeling\\polyCombineQuick.py", - "sourcetype": "file", - "tags": ["modeling", "combine", "polygon", "quick"], - "title": "Polygon Combine Quick", - "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\\polyRelaxVerts.py", - "sourcetype": "file", - "tags": ["modeling", "relax", "verts"], - "title": "Polygon Relax Vertices", - "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\\addCurveBetween.py", - "sourcetype": "file", - "tags": ["rigging", "addCurveBetween", "file"], - "title": "Add Curve Between" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\averageSkinWeights.py", - "sourcetype": "file", - "tags": ["rigging", "average", "skin weights", "file"], - "title": "Average Skin Weights" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\cbSmoothSkinWeightUI.py", - "sourcetype": "file", - "tags": ["rigging", "cbSmoothSkinWeightUI", "file"], - "title": "CB Smooth Skin Weight UI" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\channelBoxManagerUI.py", - "sourcetype": "file", - "tags": ["rigging", "channelBoxManagerUI", "file"], - "title": "Channel Box Manager UI" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\characterAutorigger.py", - "sourcetype": "file", - "tags": ["rigging", "characterAutorigger", "file"], - "title": "Character Auto Rigger" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\connectUI.py", - "sourcetype": "file", - "tags": ["rigging", "connectUI", "file"], - "title": "Connect UI" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\copySkinWeightsLocal.py", - "sourcetype": "file", - "tags": ["rigging", "copySkinWeightsLocal", "file"], - "title": "Copy Skin Weights Local" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\createCenterLocator.py", - "sourcetype": "file", - "tags": ["rigging", "createCenterLocator", "file"], - "title": "Create Center Locator" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\freezeTransformToGroup.py", - "sourcetype": "file", - "tags": ["rigging", "freezeTransformToGroup", "file"], - "title": "Freeze Transform To Group" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\groupSelected.py", - "sourcetype": "file", - "tags": ["rigging", "groupSelected", "file"], - "title": "Group Selected" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\ikHandlePoleVectorLocator.py", - "sourcetype": "file", - "tags": ["rigging", "ikHandlePoleVectorLocator", "file"], - "title": "IK Handle Pole Vector Locator" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\jointOrientUI.py", - "sourcetype": "file", - "tags": ["rigging", "jointOrientUI", "file"], - "title": "Joint Orient UI" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\jointsOnCurve.py", - "sourcetype": "file", - "tags": ["rigging", "jointsOnCurve", "file"], - "title": "Joints On Curve" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\resetBindSelectedSkinJoints.py", - "sourcetype": "file", - "tags": ["rigging", "resetBindSelectedSkinJoints", "file"], - "title": "Reset Bind Selected Skin Joints" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\selectSkinclusterJointsFromSelectedComponents.py", - "sourcetype": "file", - "tags": [ - "rigging", - "selectSkinclusterJointsFromSelectedComponents", - "file" - ], - "title": "Select Skincluster Joints From Selected Components" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\selectSkinclusterJointsFromSelectedMesh.py", - "sourcetype": "file", - "tags": [ - "rigging", - "selectSkinclusterJointsFromSelectedMesh", - "file" - ], - "title": "Select Skincluster Joints From Selected Mesh" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\setJointLabels.py", - "sourcetype": "file", - "tags": ["rigging", "setJointLabels", "file"], - "title": "Set Joint Labels" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\setJointOrientationFromCurrentRotation.py", - "sourcetype": "file", - "tags": [ - "rigging", - "setJointOrientationFromCurrentRotation", - "file" - ], - "title": "Set Joint Orientation From Current Rotation" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\setSelectedJointsOrientationZero.py", - "sourcetype": "file", - "tags": ["rigging", "setSelectedJointsOrientationZero", "file"], - "title": "Set Selected Joints Orientation Zero" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\mirrorCurveShape.py", - "sourcetype": "file", - "tags": ["rigging", "mirrorCurveShape", "file"], - "title": "Mirror Curve Shape" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\setRotationOrderUI.py", - "sourcetype": "file", - "tags": ["rigging", "setRotationOrderUI", "file"], - "title": "Set Rotation Order UI" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\paintItNowUI.py", - "sourcetype": "file", - "tags": ["rigging", "paintItNowUI", "file"], - "title": "Paint It Now UI" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\parentScaleConstraint.py", - "sourcetype": "file", - "tags": ["rigging", "parentScaleConstraint", "file"], - "title": "Parent Scale Constraint" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\quickSetWeightsUI.py", - "sourcetype": "file", - "tags": ["rigging", "quickSetWeightsUI", "file"], - "title": "Quick Set Weights UI" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\rapidRig.py", - "sourcetype": "file", - "tags": ["rigging", "rapidRig", "file"], - "title": "Rapid Rig" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\regenerate_blendshape_targets.py", - "sourcetype": "file", - "tags": ["rigging", "regenerate_blendshape_targets", "file"], - "title": "Regenerate Blendshape Targets" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\removeRotationAxis.py", - "sourcetype": "file", - "tags": ["rigging", "removeRotationAxis", "file"], - "title": "Remove Rotation Axis" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\resetBindSelectedMeshes.py", - "sourcetype": "file", - "tags": ["rigging", "resetBindSelectedMeshes", "file"], - "title": "Reset Bind Selected Meshes" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\simpleControllerOnSelection.py", - "sourcetype": "file", - "tags": ["rigging", "simpleControllerOnSelection", "file"], - "title": "Simple Controller On Selection" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\simpleControllerOnSelectionHierarchy.py", - "sourcetype": "file", - "tags": [ - "rigging", - "simpleControllerOnSelectionHierarchy", - "file" - ], - "title": "Simple Controller On Selection Hierarchy" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\superRelativeCluster.py", - "sourcetype": "file", - "tags": ["rigging", "superRelativeCluster", "file"], - "title": "Super Relative Cluster" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\tfSmoothSkinWeight.py", - "sourcetype": "file", - "tags": ["rigging", "tfSmoothSkinWeight", "file"], - "title": "TF Smooth Skin Weight" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\toggleIntermediates.py", - "sourcetype": "file", - "tags": ["rigging", "toggleIntermediates", "file"], - "title": "Toggle Intermediates" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\toggleSegmentScaleCompensate.py", - "sourcetype": "file", - "tags": ["rigging", "toggleSegmentScaleCompensate", "file"], - "title": "Toggle Segment Scale Compensate" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\rigging\\toggleSkinclusterDeformNormals.py", - "sourcetype": "file", - "tags": ["rigging", "toggleSkinclusterDeformNormals", "file"], - "title": "Toggle Skincluster Deform Normals" - } - ] - }, - { - "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\\poseLibrary.py", - "tags": ["animation", "poseLibrary.py"], - "title": "Pose 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\\objectsToParticlesAndInstancerCleanSource.py", - "sourcetype": "file", - "tags": [ - "particles", - "objects", - "Particles", - "Instancer", - "Clean", - "Source" - ], - "title": "Objects To Particles & Instancer - Clean Source", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\particles\\particleComponentsToLocators.py", - "sourcetype": "file", - "tags": ["particles", "components", "locators"], - "title": "Particle Components To Locators", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\particles\\objectsToParticlesAndInstancer.py", - "sourcetype": "file", - "tags": ["particles", "objects", "particles", "instancer"], - "title": "Objects To Particles And Instancer", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\particles\\spawnParticlesOnMesh.py", - "sourcetype": "file", - "tags": ["particles", "spawn", "on", "mesh"], - "title": "Spawn Particles On Mesh", - "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\\objectsToParticles.py", - "sourcetype": "file", - "tags": ["particles", "objectsToParticles"], - "title": "Objects To Particles", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\particles\\add_particle_cacheFile_attrs.py", - "sourcetype": "file", - "tags": ["particles", "add_particle_cacheFile_attrs"], - "title": "Add Particle CacheFile Attributes", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\particles\\mergeParticleSystems.py", - "sourcetype": "file", - "tags": ["particles", "mergeParticleSystems"], - "title": "Merge Particle Systems", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\particles\\particlesToLocators.py", - "sourcetype": "file", - "tags": ["particles", "particlesToLocators"], - "title": "Particles To Locators", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjectsWithAnimation.py", - "sourcetype": "file", - "tags": ["particles", "instancerToObjectsWithAnimation"], - "title": "Instancer To Objects With Animation", - "tooltip": "" - }, - { - "type": "separator" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\particles\\mayaReplicateHoudiniTool.py", - "sourcetype": "file", - "tags": [ - "particles", - "houdini", - "houdiniTool", - "houdiniEngine" - ], - "title": "Replicate Houdini Tool", - "tooltip": "" - }, - { - "type": "separator" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\particles\\clearInitialState.py", - "sourcetype": "file", - "tags": ["particles", "clearInitialState"], - "title": "Clear Initial State", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\particles\\killSelectedParticles.py", - "sourcetype": "file", - "tags": ["particles", "killSelectedParticles"], - "title": "Kill Selected Particles", - "tooltip": "" - } - ] - }, - { - "type": "menu", - "title": "Yeti", - "items": [ - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\yeti\\yeti_rig_manager.py", - "sourcetype": "file", - "tags": ["yeti", "rig", "fur", "manager"], - "title": "Open Yeti Rig Manager", - "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": "action", - "command": "$OPENPYPE_SCRIPTS\\cleanup\\selectByType.py", - "sourcetype": "file", - "tags": ["cleanup", "selectByType"], - "title": "Select By Type", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\cleanup\\selectIntermediateObjects.py", - "sourcetype": "file", - "tags": ["cleanup", "selectIntermediateObjects"], - "title": "Select Intermediate Objects", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\cleanup\\selectNonUniqueNames.py", - "sourcetype": "file", - "tags": ["cleanup", "select", "non unique", "names"], - "title": "Select Non Unique Names", - "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": "action", - "command": "$OPENPYPE_SCRIPTS\\cleanup\\deleteGhostIntermediateObjects.py", - "sourcetype": "file", - "tags": ["cleanup", "deleteGhostIntermediateObjects"], - "title": "Delete Ghost Intermediate Objects", - "tooltip": "" - }, - { - "type": "separator" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\cleanup\\resetViewportCache.py", - "sourcetype": "file", - "tags": ["cleanup", "reset", "viewport", "cache"], - "title": "Reset Viewport Cache", - "tooltip": "" - }, - { - "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\\colorbleedRename.py", - "sourcetype": "file", - "tags": ["cleanup", "rename", "ui"], - "title": "Colorbleed Renamer", - "tooltip": "Colorbleed Rename UI" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\cleanup\\renameShapesToTransform.py", - "sourcetype": "file", - "tags": ["cleanup", "renameShapesToTransform"], - "title": "Rename Shapes To Transform", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\cleanup\\reorderUI.py", - "sourcetype": "file", - "tags": ["cleanup", "reorderUI"], - "title": "Reorder UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\cleanup\\pastedCleaner.py", - "sourcetype": "file", - "tags": ["cleanup", "pastedCleaner"], - "title": "Pasted Cleaner", - "tooltip": "" - } - ] - }, - { - "type": "menu", - "title": "Others", - "items": [ - { - "type": "menu", - "sourcetype": "file", - "title": "Yeti", - "items": [ - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\yeti\\cache_selected_yeti_nodes.py", - "sourcetype": "file", - "tags": ["others", "yeti", "cache", "selected"], - "title": "Cache Selected Yeti Nodes", - "tooltip": "" - } - ] - }, - { - "type": "menu", - "title": "Hair", - "tooltip": "", - "items": [ - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\hair\\recolorHairCurrentCurve", - "sourcetype": "file", - "tags": ["others", "selectSoftSelection"], - "title": "Select Soft Selection", - "tooltip": "" - } - ] - }, - { - "type": "menu", - "command": "$OPENPYPE_SCRIPTS\\others\\display", - "sourcetype": "file", - "tags": ["others", "display"], - "title": "Display", - "items": [ - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\display\\wireframeSelectedObjects.py", - "sourcetype": "file", - "tags": ["others", "wireframe", "selected", "objects"], - "title": "Wireframe Selected Objects", - "tooltip": "" - } - ] - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\archiveSceneUI.py", - "sourcetype": "file", - "tags": ["others", "archiveSceneUI"], - "title": "Archive Scene UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\getSimilarMeshes.py", - "sourcetype": "file", - "tags": ["others", "getSimilarMeshes"], - "title": "Get Similar Meshes", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\createBoundingBoxEachSelected.py", - "sourcetype": "file", - "tags": ["others", "createBoundingBoxEachSelected"], - "title": "Create BoundingBox Each Selected", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\curveFromPositionEveryFrame.py", - "sourcetype": "file", - "tags": ["others", "curveFromPositionEveryFrame"], - "title": "Curve From Position", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\instanceLeafSmartTransform.py", - "sourcetype": "file", - "tags": ["others", "instance", "leaf", "smart", "transform"], - "title": "Instance Leaf Smart Transform", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\instanceSmartTransform.py", - "sourcetype": "file", - "tags": ["others", "instance", "smart", "transform"], - "title": "Instance Smart Transform", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\randomizeUVShellsSelectedObjects.py", - "sourcetype": "file", - "tags": ["others", "randomizeUVShellsSelectedObjects"], - "title": "Randomize UV Shells", - "tooltip": "Select objects before running action" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\centerPivotGroup.py", - "sourcetype": "file", - "tags": ["others", "centerPivotGroup"], - "title": "Center Pivot Group", - "tooltip": "" - }, - { - "type": "separator" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\locatorsOnSelectedFaces.py", - "sourcetype": "file", - "tags": ["others", "locatorsOnSelectedFaces"], - "title": "Locators On Selected Faces", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\locatorsOnEdgeSelectionPrompt.py", - "sourcetype": "file", - "tags": ["others", "locatorsOnEdgeSelectionPrompt"], - "title": "Locators On Edge Selection Prompt", - "tooltip": "" - }, - { - "type": "separator" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\copyDeformers.py", - "sourcetype": "file", - "tags": ["others", "copyDeformers"], - "title": "Copy Deformers", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\selectInReferenceEditor.py", - "sourcetype": "file", - "tags": ["others", "selectInReferenceEditor"], - "title": "Select In Reference Editor", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\selectConstrainingObject.py", - "sourcetype": "file", - "tags": ["others", "selectConstrainingObject"], - "title": "Select Constraining Object", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\deformerSetRelationsUI.py", - "sourcetype": "file", - "tags": ["others", "deformerSetRelationsUI"], - "title": "Deformer Set Relations UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\recreateBaseNodesForAllLatticeNodes.py", - "sourcetype": "file", - "tags": ["others", "recreate", "base", "nodes", "lattice"], - "title": "Recreate Base Nodes For Lattice Nodes", - "tooltip": "" - } - ] - } -] diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 14e8f4eb45..1b3bb9feb3 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -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. diff --git a/openpype/modules/base.py b/openpype/modules/base.py index d566692439..437f5efdbc 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -43,6 +43,7 @@ DEFAULT_OPENPYPE_MODULES = ( "standalonepublish_action", "job_queue", "timers_manager", + "sync_server", ) diff --git a/openpype/modules/slack/plugins/publish/collect_slack_family.py b/openpype/modules/slack/plugins/publish/collect_slack_family.py index 2110c0703b..6c965b04cd 100644 --- a/openpype/modules/slack/plugins/publish/collect_slack_family.py +++ b/openpype/modules/slack/plugins/publish/collect_slack_family.py @@ -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: diff --git a/openpype/modules/default_modules/sync_server/README.md b/openpype/modules/sync_server/README.md similarity index 100% rename from openpype/modules/default_modules/sync_server/README.md rename to openpype/modules/sync_server/README.md diff --git a/openpype/modules/default_modules/sync_server/__init__.py b/openpype/modules/sync_server/__init__.py similarity index 100% rename from openpype/modules/default_modules/sync_server/__init__.py rename to openpype/modules/sync_server/__init__.py diff --git a/openpype/modules/default_modules/sync_server/providers/__init__.py b/openpype/modules/sync_server/providers/__init__.py similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/__init__.py rename to openpype/modules/sync_server/providers/__init__.py diff --git a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py b/openpype/modules/sync_server/providers/abstract_provider.py similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/abstract_provider.py rename to openpype/modules/sync_server/providers/abstract_provider.py diff --git a/openpype/modules/default_modules/sync_server/providers/dropbox.py b/openpype/modules/sync_server/providers/dropbox.py similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/dropbox.py rename to openpype/modules/sync_server/providers/dropbox.py diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/sync_server/providers/gdrive.py similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/gdrive.py rename to openpype/modules/sync_server/providers/gdrive.py diff --git a/openpype/modules/default_modules/sync_server/providers/lib.py b/openpype/modules/sync_server/providers/lib.py similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/lib.py rename to openpype/modules/sync_server/providers/lib.py diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/sync_server/providers/local_drive.py similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/local_drive.py rename to openpype/modules/sync_server/providers/local_drive.py diff --git a/openpype/modules/default_modules/sync_server/providers/resources/dropbox.png b/openpype/modules/sync_server/providers/resources/dropbox.png similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/resources/dropbox.png rename to openpype/modules/sync_server/providers/resources/dropbox.png diff --git a/openpype/modules/default_modules/sync_server/providers/resources/folder.png b/openpype/modules/sync_server/providers/resources/folder.png similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/resources/folder.png rename to openpype/modules/sync_server/providers/resources/folder.png diff --git a/openpype/modules/default_modules/sync_server/providers/resources/gdrive.png b/openpype/modules/sync_server/providers/resources/gdrive.png similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/resources/gdrive.png rename to openpype/modules/sync_server/providers/resources/gdrive.png diff --git a/openpype/modules/default_modules/sync_server/providers/resources/local_drive.png b/openpype/modules/sync_server/providers/resources/local_drive.png similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/resources/local_drive.png rename to openpype/modules/sync_server/providers/resources/local_drive.png diff --git a/openpype/modules/default_modules/sync_server/providers/resources/sftp.png b/openpype/modules/sync_server/providers/resources/sftp.png similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/resources/sftp.png rename to openpype/modules/sync_server/providers/resources/sftp.png diff --git a/openpype/modules/default_modules/sync_server/providers/resources/studio.png b/openpype/modules/sync_server/providers/resources/studio.png similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/resources/studio.png rename to openpype/modules/sync_server/providers/resources/studio.png diff --git a/openpype/modules/default_modules/sync_server/providers/sftp.py b/openpype/modules/sync_server/providers/sftp.py similarity index 100% rename from openpype/modules/default_modules/sync_server/providers/sftp.py rename to openpype/modules/sync_server/providers/sftp.py diff --git a/openpype/modules/default_modules/sync_server/resources/paused.png b/openpype/modules/sync_server/resources/paused.png similarity index 100% rename from openpype/modules/default_modules/sync_server/resources/paused.png rename to openpype/modules/sync_server/resources/paused.png diff --git a/openpype/modules/default_modules/sync_server/resources/refresh.png b/openpype/modules/sync_server/resources/refresh.png similarity index 100% rename from openpype/modules/default_modules/sync_server/resources/refresh.png rename to openpype/modules/sync_server/resources/refresh.png diff --git a/openpype/modules/default_modules/sync_server/resources/synced.png b/openpype/modules/sync_server/resources/synced.png similarity index 100% rename from openpype/modules/default_modules/sync_server/resources/synced.png rename to openpype/modules/sync_server/resources/synced.png diff --git a/openpype/modules/default_modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py similarity index 100% rename from openpype/modules/default_modules/sync_server/sync_server.py rename to openpype/modules/sync_server/sync_server.py diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py similarity index 100% rename from openpype/modules/default_modules/sync_server/sync_server_module.py rename to openpype/modules/sync_server/sync_server_module.py diff --git a/openpype/modules/default_modules/sync_server/tray/app.py b/openpype/modules/sync_server/tray/app.py similarity index 100% rename from openpype/modules/default_modules/sync_server/tray/app.py rename to openpype/modules/sync_server/tray/app.py diff --git a/openpype/modules/default_modules/sync_server/tray/delegates.py b/openpype/modules/sync_server/tray/delegates.py similarity index 100% rename from openpype/modules/default_modules/sync_server/tray/delegates.py rename to openpype/modules/sync_server/tray/delegates.py diff --git a/openpype/modules/default_modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py similarity index 100% rename from openpype/modules/default_modules/sync_server/tray/lib.py rename to openpype/modules/sync_server/tray/lib.py diff --git a/openpype/modules/default_modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py similarity index 100% rename from openpype/modules/default_modules/sync_server/tray/models.py rename to openpype/modules/sync_server/tray/models.py diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py similarity index 100% rename from openpype/modules/default_modules/sync_server/tray/widgets.py rename to openpype/modules/sync_server/tray/widgets.py diff --git a/openpype/modules/default_modules/sync_server/utils.py b/openpype/modules/sync_server/utils.py similarity index 100% rename from openpype/modules/default_modules/sync_server/utils.py rename to openpype/modules/sync_server/utils.py diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 3d48fb92ee..486718d8c4 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -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, diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index b601f9bcba..6fb6f55528 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -2,7 +2,8 @@ "create": { "CreateShotClip": { "hierarchy": "{folder}/{sequence}", - "clipRename": true, + "useShotName": true, + "clipRename": false, "clipName": "{sequence}{shot}", "segmentIndex": true, "countFrom": 10, diff --git a/openpype/settings/defaults/project_settings/slack.json b/openpype/settings/defaults/project_settings/slack.json index 2d10bd173d..d77b8c2208 100644 --- a/openpype/settings/defaults/project_settings/slack.json +++ b/openpype/settings/defaults/project_settings/slack.json @@ -10,6 +10,7 @@ "hosts": [], "task_types": [], "tasks": [], + "subsets": [], "channel_messages": [] } ] diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 912e2d9924..2f99200a88 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -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" } } }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index 9ef05fa832..dc88d11f61 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -28,6 +28,11 @@ "key": "hierarchy", "label": "Shot parent hierarchy" }, + { + "type": "boolean", + "key": "useShotName", + "label": "Use Shot Name" + }, { "type": "boolean", "key": "clipRename", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json index 4e82c991e7..14814d8b01 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json @@ -69,6 +69,12 @@ "type": "list", "object_type": "text" }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, { "type": "separator" }, diff --git a/openpype/tools/launcher/lib.py b/openpype/tools/launcher/lib.py index 4d678b96ae..b4e6a0c3e9 100644 --- a/openpype/tools/launcher/lib.py +++ b/openpype/tools/launcher/lib.py @@ -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: diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index ecee8b1575..effa283318 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -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"]) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 5dad41c349..30e6531843 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -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) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index a8f65894f2..b5b6368865 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -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() diff --git a/openpype/tools/pyblish_pype/control.py b/openpype/tools/pyblish_pype/control.py index 9774c8020b..455a338499 100644 --- a/openpype/tools/pyblish_pype/control.py +++ b/openpype/tools/pyblish_pype/control.py @@ -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() diff --git a/openpype/tools/pyblish_pype/model.py b/openpype/tools/pyblish_pype/model.py index bb1aff2a9a..0faadb5940 100644 --- a/openpype/tools/pyblish_pype/model.py +++ b/openpype/tools/pyblish_pype/model.py @@ -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 diff --git a/openpype/tools/pyblish_pype/util.py b/openpype/tools/pyblish_pype/util.py index d3d76b187c..9f3697be16 100644 --- a/openpype/tools/pyblish_pype/util.py +++ b/openpype/tools/pyblish_pype/util.py @@ -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): diff --git a/openpype/tools/pyblish_pype/window.py b/openpype/tools/pyblish_pype/window.py index 9c4d4b10f4..38907c1a52 100644 --- a/openpype/tools/pyblish_pype/window.py +++ b/openpype/tools/pyblish_pype/window.py @@ -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) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 18a9764b34..14e25a54d8 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -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: diff --git a/openpype/tools/settings/settings/search_dialog.py b/openpype/tools/settings/settings/search_dialog.py new file mode 100644 index 0000000000..3f987c0010 --- /dev/null +++ b/openpype/tools/settings/settings/search_dialog.py @@ -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) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 8a01bf1bce..22778e4a5b 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -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) diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index be9f442d10..17164d9e0f 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -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: diff --git a/openpype/tools/utils/tasks_widget.py b/openpype/tools/utils/tasks_widget.py index 6c7787d06a..2a8a45626c 100644 --- a/openpype/tools/utils/tasks_widget.py +++ b/openpype/tools/utils/tasks_widget.py @@ -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) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 4b5bf07b47..3a772a038c 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -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 diff --git a/website/docs/artist_hosts_resolve.md b/website/docs/artist_hosts_resolve.md index afd050648f..7c462484f5 100644 --- a/website/docs/artist_hosts_resolve.md +++ b/website/docs/artist_hosts_resolve.md @@ -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 ::: diff --git a/website/docs/module_slack.md b/website/docs/module_slack.md index 67b200259d..3a2842da63 100644 --- a/website/docs/module_slack.md +++ b/website/docs/module_slack.md @@ -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: