mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
ac798932ce
98 changed files with 6268 additions and 335 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,7 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.16.5
|
||||
- 3.16.5-nightly.5
|
||||
- 3.16.5-nightly.4
|
||||
- 3.16.5-nightly.3
|
||||
|
|
@ -134,7 +135,6 @@ body:
|
|||
- 3.14.9-nightly.3
|
||||
- 3.14.9-nightly.2
|
||||
- 3.14.9-nightly.1
|
||||
- 3.14.8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -663,10 +663,13 @@ def convert_v4_representation_to_v3(representation):
|
|||
if isinstance(context, six.string_types):
|
||||
context = json.loads(context)
|
||||
|
||||
if "folder" in context:
|
||||
_c_folder = context.pop("folder")
|
||||
if "asset" not in context and "folder" in context:
|
||||
_c_folder = context["folder"]
|
||||
context["asset"] = _c_folder["name"]
|
||||
|
||||
elif "asset" in context and "folder" not in context:
|
||||
context["folder"] = {"name": context["asset"]}
|
||||
|
||||
if "product" in context:
|
||||
_c_product = context.pop("product")
|
||||
context["family"] = _c_product["type"]
|
||||
|
|
@ -959,9 +962,11 @@ def convert_create_representation_to_v4(representation, con):
|
|||
converted_representation["files"] = new_files
|
||||
|
||||
context = representation["context"]
|
||||
context["folder"] = {
|
||||
"name": context.pop("asset", None)
|
||||
}
|
||||
if "folder" not in context:
|
||||
context["folder"] = {
|
||||
"name": context.get("asset")
|
||||
}
|
||||
|
||||
context["product"] = {
|
||||
"type": context.pop("family", None),
|
||||
"name": context.pop("subset", None),
|
||||
|
|
@ -1285,7 +1290,7 @@ def convert_update_representation_to_v4(
|
|||
|
||||
if "context" in update_data:
|
||||
context = update_data["context"]
|
||||
if "asset" in context:
|
||||
if "folder" not in context and "asset" in context:
|
||||
context["folder"] = {"name": context.pop("asset")}
|
||||
|
||||
if "family" in context or "subset" in context:
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class BlendLoader(plugin.AssetLoader):
|
|||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
libpath = self.fname
|
||||
libpath = self.filepath_from_context(context)
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class AbcCameraLoader(plugin.AssetLoader):
|
|||
asset_group = bpy.data.objects.new(group_name, object_data=None)
|
||||
avalon_container.objects.link(asset_group)
|
||||
|
||||
objects = self._process(libpath, asset_group, group_name)
|
||||
self._process(libpath, asset_group, group_name)
|
||||
|
||||
objects = []
|
||||
nodes = list(asset_group.children)
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ class FbxCameraLoader(plugin.AssetLoader):
|
|||
asset_group = bpy.data.objects.new(group_name, object_data=None)
|
||||
avalon_container.objects.link(asset_group)
|
||||
|
||||
objects = self._process(libpath, asset_group, group_name)
|
||||
self._process(libpath, asset_group, group_name)
|
||||
|
||||
objects = []
|
||||
nodes = list(asset_group.children)
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ class ExtractABC(publish.Extractor):
|
|||
filename = f"{instance.name}.abc"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
context = bpy.context
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ class ExtractAnimationABC(publish.Extractor):
|
|||
filename = f"{instance.name}.abc"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
context = bpy.context
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
|
|
|
|||
|
|
@ -21,16 +21,11 @@ class ExtractCameraABC(publish.Extractor):
|
|||
filename = f"{instance.name}.abc"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
context = bpy.context
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
plugin.deselect_all()
|
||||
|
||||
selected = []
|
||||
active = None
|
||||
|
||||
asset_group = None
|
||||
for obj in instance:
|
||||
if obj.get(AVALON_PROPERTY):
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ class LoadClip(opfapi.ClipLoader):
|
|||
self.fpd = fproject.current_workspace.desktop
|
||||
|
||||
# load clip to timeline and get main variables
|
||||
namespace = namespace
|
||||
version = context['version']
|
||||
version_data = version.get("data", {})
|
||||
version_name = version.get("name", None)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ class LoadClipBatch(opfapi.ClipLoader):
|
|||
self.batch = options.get("batch") or flame.batch
|
||||
|
||||
# load clip to timeline and get main variables
|
||||
namespace = namespace
|
||||
version = context['version']
|
||||
version_data = version.get("data", {})
|
||||
version_name = version.get("name", None)
|
||||
|
|
|
|||
|
|
@ -325,7 +325,6 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
|
|||
def _create_shot_instance(self, context, clip_name, **data):
|
||||
master_layer = data.get("heroTrack")
|
||||
hierarchy_data = data.get("hierarchyData")
|
||||
asset = data.get("asset")
|
||||
|
||||
if not master_layer:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ class TemplateLoader(load.LoaderPlugin):
|
|||
node = harmony.find_node_by_name(node_name, "GROUP")
|
||||
self_name = self.__class__.__name__
|
||||
|
||||
update_and_replace = False
|
||||
if is_representation_from_latest(representation):
|
||||
self._set_green(node)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -317,20 +317,6 @@ class Spacer(QtWidgets.QWidget):
|
|||
self.setLayout(layout)
|
||||
|
||||
|
||||
def get_reference_node_parents(ref):
|
||||
"""Return all parent reference nodes of reference node
|
||||
|
||||
Args:
|
||||
ref (str): reference node.
|
||||
|
||||
Returns:
|
||||
list: The upstream parent reference nodes.
|
||||
|
||||
"""
|
||||
parents = []
|
||||
return parents
|
||||
|
||||
|
||||
class SequenceLoader(LoaderPlugin):
|
||||
"""A basic SequenceLoader for Resolve
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ class CollectClipEffects(pyblish.api.InstancePlugin):
|
|||
if review and review_track_index == _track_index:
|
||||
continue
|
||||
for sitem in sub_track_items:
|
||||
effect = None
|
||||
# make sure this subtrack item is relative of track item
|
||||
if ((track_item not in sitem.linkedItems())
|
||||
and (len(sitem.linkedItems()) > 0)):
|
||||
|
|
@ -53,7 +52,6 @@ class CollectClipEffects(pyblish.api.InstancePlugin):
|
|||
continue
|
||||
|
||||
effect = self.add_effect(_track_index, sitem)
|
||||
|
||||
if effect:
|
||||
effects.update(effect)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import attr
|
||||
import hou
|
||||
from openpype.hosts.houdini.api.lib import get_color_management_preferences
|
||||
|
||||
from openpype.pipeline.colorspace import get_display_view_colorspace_name
|
||||
|
||||
@attr.s
|
||||
class LayerMetadata(object):
|
||||
|
|
@ -54,3 +54,16 @@ class ARenderProduct(object):
|
|||
)
|
||||
]
|
||||
return colorspace_data
|
||||
|
||||
|
||||
def get_default_display_view_colorspace():
|
||||
"""Returns the colorspace attribute of the default (display, view) pair.
|
||||
|
||||
It's used for 'ociocolorspace' parm in OpenGL Node."""
|
||||
|
||||
prefs = get_color_management_preferences()
|
||||
return get_display_view_colorspace_name(
|
||||
config_path=prefs["config"],
|
||||
display=prefs["display"],
|
||||
view=prefs["view"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
from openpype.hosts.houdini.api import plugin
|
||||
from openpype.lib import EnumDef, BoolDef, NumberDef
|
||||
|
||||
import os
|
||||
import hou
|
||||
|
||||
|
||||
class CreateReview(plugin.HoudiniCreator):
|
||||
"""Review with OpenGL ROP"""
|
||||
|
|
@ -13,7 +16,6 @@ class CreateReview(plugin.HoudiniCreator):
|
|||
icon = "video-camera"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
import hou
|
||||
|
||||
instance_data.pop("active", None)
|
||||
instance_data.update({"node_type": "opengl"})
|
||||
|
|
@ -82,6 +84,11 @@ class CreateReview(plugin.HoudiniCreator):
|
|||
|
||||
instance_node.setParms(parms)
|
||||
|
||||
# Set OCIO Colorspace to the default output colorspace
|
||||
# if there's OCIO
|
||||
if os.getenv("OCIO"):
|
||||
self.set_colorcorrect_to_default_view_space(instance_node)
|
||||
|
||||
to_lock = ["id", "family"]
|
||||
|
||||
self.lock_parameters(instance_node, to_lock)
|
||||
|
|
@ -123,3 +130,23 @@ class CreateReview(plugin.HoudiniCreator):
|
|||
minimum=0.0001,
|
||||
decimals=3)
|
||||
]
|
||||
|
||||
def set_colorcorrect_to_default_view_space(self,
|
||||
instance_node):
|
||||
"""Set ociocolorspace to the default output space."""
|
||||
from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa
|
||||
|
||||
# set Color Correction parameter to OpenColorIO
|
||||
instance_node.setParms({"colorcorrect": 2})
|
||||
|
||||
# Get default view space for ociocolorspace parm.
|
||||
default_view_space = get_default_display_view_colorspace()
|
||||
instance_node.setParms(
|
||||
{"ociocolorspace": default_view_space}
|
||||
)
|
||||
|
||||
self.log.debug(
|
||||
"'OCIO Colorspace' parm on '{}' has been set to "
|
||||
"the default view color space '{}'"
|
||||
.format(instance_node, default_view_space)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ class BgeoLoader(load.LoaderPlugin):
|
|||
|
||||
# Create a new geo node
|
||||
container = obj.createNode("geo", node_name=node_name)
|
||||
is_sequence = bool(context["representation"]["context"].get("frame"))
|
||||
|
||||
# Remove the file node, it only loads static meshes
|
||||
# Houdini 17 has removed the file node from the geo node
|
||||
|
|
|
|||
|
|
@ -80,14 +80,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin):
|
|||
def get_beauty_render_product(self, prefix, suffix="<reName>"):
|
||||
"""Return the beauty output filename if render element enabled
|
||||
"""
|
||||
# Remove aov suffix from the product: `prefix.aov_suffix` -> `prefix`
|
||||
aov_parm = ".{}".format(suffix)
|
||||
beauty_product = None
|
||||
if aov_parm in prefix:
|
||||
beauty_product = prefix.replace(aov_parm, "")
|
||||
else:
|
||||
beauty_product = prefix
|
||||
|
||||
return beauty_product
|
||||
return prefix.replace(aov_parm, "")
|
||||
|
||||
def get_render_element_name(self, node, prefix, suffix="<reName>"):
|
||||
"""Return the output filename using the AOV prefix and suffix
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from openpype.pipeline import (
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
from openpype.pipeline.publish import RepairAction
|
||||
from openpype.hosts.houdini.api.action import SelectROPAction
|
||||
|
||||
import os
|
||||
import hou
|
||||
|
||||
|
||||
class SetDefaultViewSpaceAction(RepairAction):
|
||||
label = "Set default view colorspace"
|
||||
icon = "mdi.monitor"
|
||||
|
||||
|
||||
class ValidateReviewColorspace(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validate Review Colorspace parameters.
|
||||
|
||||
It checks if 'OCIO Colorspace' parameter was set to valid value.
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder + 0.1
|
||||
families = ["review"]
|
||||
hosts = ["houdini"]
|
||||
label = "Validate Review Colorspace"
|
||||
actions = [SetDefaultViewSpaceAction, SelectROPAction]
|
||||
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
if os.getenv("OCIO") is None:
|
||||
self.log.debug(
|
||||
"Using Houdini's Default Color Management, "
|
||||
" skipping check.."
|
||||
)
|
||||
return
|
||||
|
||||
rop_node = hou.node(instance.data["instance_node"])
|
||||
if rop_node.evalParm("colorcorrect") != 2:
|
||||
# any colorspace settings other than default requires
|
||||
# 'Color Correct' parm to be set to 'OpenColorIO'
|
||||
raise PublishValidationError(
|
||||
"'Color Correction' parm on '{}' ROP must be set to"
|
||||
" 'OpenColorIO'".format(rop_node.path())
|
||||
)
|
||||
|
||||
if rop_node.evalParm("ociocolorspace") not in \
|
||||
hou.Color.ocio_spaces():
|
||||
|
||||
raise PublishValidationError(
|
||||
"Invalid value: Colorspace name doesn't exist.\n"
|
||||
"Check 'OCIO Colorspace' parameter on '{}' ROP"
|
||||
.format(rop_node.path())
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
"""Set Default View Space Action.
|
||||
|
||||
It is a helper action more than a repair action,
|
||||
used to set colorspace on opengl node to the default view.
|
||||
"""
|
||||
from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa
|
||||
|
||||
rop_node = hou.node(instance.data["instance_node"])
|
||||
|
||||
if rop_node.evalParm("colorcorrect") != 2:
|
||||
rop_node.setParms({"colorcorrect": 2})
|
||||
cls.log.debug(
|
||||
"'Color Correction' parm on '{}' has been set to"
|
||||
" 'OpenColorIO'".format(rop_node.path())
|
||||
)
|
||||
|
||||
# Get default view colorspace name
|
||||
default_view_space = get_default_display_view_colorspace()
|
||||
|
||||
rop_node.setParms({"ociocolorspace": default_view_space})
|
||||
cls.log.info(
|
||||
"'OCIO Colorspace' parm on '{}' has been set to "
|
||||
"the default view color space '{}'"
|
||||
.format(rop_node, default_view_space)
|
||||
)
|
||||
|
|
@ -37,13 +37,10 @@ class RenderSettings(object):
|
|||
def set_render_camera(self, selection):
|
||||
for sel in selection:
|
||||
# to avoid Attribute Error from pymxs wrapper
|
||||
found = False
|
||||
if rt.classOf(sel) in rt.Camera.classes:
|
||||
found = True
|
||||
rt.viewport.setCamera(sel)
|
||||
break
|
||||
if not found:
|
||||
raise RuntimeError("Active Camera not found")
|
||||
return
|
||||
raise RuntimeError("Active Camera not found")
|
||||
|
||||
def render_output(self, container):
|
||||
folder = rt.maxFilePath
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ class CreateRender(plugin.MaxCreator):
|
|||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
from pymxs import runtime as rt
|
||||
sel_obj = list(rt.selection)
|
||||
file = rt.maxFileName
|
||||
filename, _ = os.path.splitext(file)
|
||||
instance_data["AssetName"] = filename
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ class CollectRender(pyblish.api.InstancePlugin):
|
|||
asset = get_current_asset_name()
|
||||
|
||||
files_by_aov = RenderProducts().get_beauty(instance.name)
|
||||
folder = folder.replace("\\", "/")
|
||||
aovs = RenderProducts().get_aovs(instance.name)
|
||||
files_by_aov.update(aovs)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin):
|
|||
start = float(instance.data.get("frameStartHandle", 1))
|
||||
end = float(instance.data.get("frameEndHandle", 1))
|
||||
|
||||
container = instance.data["instance_node"]
|
||||
|
||||
self.log.info("Extracting Camera ...")
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
|
|
|||
|
|
@ -19,9 +19,8 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin):
|
|||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
container = instance.data["instance_node"]
|
||||
|
||||
self.log.info("Extracting Camera ...")
|
||||
self.log.debug("Extracting Camera ...")
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{name}.fbx".format(**instance.data)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,9 @@ class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin):
|
|||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
container = instance.data["instance_node"]
|
||||
|
||||
# publish the raw scene for camera
|
||||
self.log.info("Extracting Raw Max Scene ...")
|
||||
self.log.debug("Extracting Raw Max Scene ...")
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{name}.max".format(**instance.data)
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin):
|
|||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
container = instance.data["instance_node"]
|
||||
|
||||
self.log.info("Extracting Geometry ...")
|
||||
self.log.debug("Extracting Geometry ...")
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{name}.abc".format(**instance.data)
|
||||
|
|
|
|||
|
|
@ -20,10 +20,7 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin):
|
|||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
container = instance.data["instance_node"]
|
||||
|
||||
|
||||
self.log.info("Extracting Geometry ...")
|
||||
self.log.debug("Extracting Geometry ...")
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{name}.fbx".format(**instance.data)
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin):
|
|||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
container = instance.data["instance_node"]
|
||||
|
||||
self.log.info("Extracting Geometry ...")
|
||||
self.log.debug("Extracting Geometry ...")
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{name}.obj".format(**instance.data)
|
||||
|
|
|
|||
|
|
@ -54,8 +54,6 @@ class ExtractAlembic(publish.Extractor):
|
|||
start = float(instance.data.get("frameStartHandle", 1))
|
||||
end = float(instance.data.get("frameEndHandle", 1))
|
||||
|
||||
container = instance.data["instance_node"]
|
||||
|
||||
self.log.debug("Extracting pointcache ...")
|
||||
|
||||
parent_dir = self.staging_dir(instance)
|
||||
|
|
|
|||
|
|
@ -16,11 +16,10 @@ class ExtractRedshiftProxy(publish.Extractor):
|
|||
families = ["redshiftproxy"]
|
||||
|
||||
def process(self, instance):
|
||||
container = instance.data["instance_node"]
|
||||
start = int(instance.context.data.get("frameStart"))
|
||||
end = int(instance.context.data.get("frameEnd"))
|
||||
|
||||
self.log.info("Extracting Redshift Proxy...")
|
||||
self.log.debug("Extracting Redshift Proxy...")
|
||||
stagingdir = self.staging_dir(instance)
|
||||
rs_filename = "{name}.rs".format(**instance.data)
|
||||
rs_filepath = os.path.join(stagingdir, rs_filename)
|
||||
|
|
|
|||
|
|
@ -6,11 +6,6 @@ from openpype.pipeline import (
|
|||
from pymxs import runtime as rt
|
||||
from openpype.hosts.max.api.lib import reset_scene_resolution
|
||||
|
||||
from openpype.pipeline.context_tools import (
|
||||
get_current_project_asset,
|
||||
get_current_project
|
||||
)
|
||||
|
||||
|
||||
class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
|
|
@ -43,22 +38,16 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
|||
"on asset or shot.")
|
||||
|
||||
def get_db_resolution(self, instance):
|
||||
data = ["data.resolutionWidth", "data.resolutionHeight"]
|
||||
project_resolution = get_current_project(fields=data)
|
||||
project_resolution_data = project_resolution["data"]
|
||||
asset_resolution = get_current_project_asset(fields=data)
|
||||
asset_resolution_data = asset_resolution["data"]
|
||||
# Set project resolution
|
||||
project_width = int(
|
||||
project_resolution_data.get("resolutionWidth", 1920))
|
||||
project_height = int(
|
||||
project_resolution_data.get("resolutionHeight", 1080))
|
||||
width = int(
|
||||
asset_resolution_data.get("resolutionWidth", project_width))
|
||||
height = int(
|
||||
asset_resolution_data.get("resolutionHeight", project_height))
|
||||
asset_doc = instance.data["assetEntity"]
|
||||
project_doc = instance.context.data["projectEntity"]
|
||||
for data in [asset_doc["data"], project_doc["data"]]:
|
||||
if "resolutionWidth" in data and "resolutionHeight" in data:
|
||||
width = data["resolutionWidth"]
|
||||
height = data["resolutionHeight"]
|
||||
return int(width), int(height)
|
||||
|
||||
return width, height
|
||||
# Defaults if not found in asset document or project document
|
||||
return 1920, 1080
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
|
|
|
|||
|
|
@ -177,12 +177,7 @@ class RenderSettings(object):
|
|||
# list all the aovs
|
||||
all_rs_aovs = cmds.ls(type='RedshiftAOV')
|
||||
for rs_aov in redshift_aovs:
|
||||
rs_layername = rs_aov
|
||||
if " " in rs_aov:
|
||||
rs_renderlayer = rs_aov.replace(" ", "")
|
||||
rs_layername = "rsAov_{}".format(rs_renderlayer)
|
||||
else:
|
||||
rs_layername = "rsAov_{}".format(rs_aov)
|
||||
rs_layername = "rsAov_{}".format(rs_aov.replace(" ", ""))
|
||||
if rs_layername in all_rs_aovs:
|
||||
continue
|
||||
cmds.rsCreateAov(type=rs_aov)
|
||||
|
|
@ -317,7 +312,7 @@ class RenderSettings(object):
|
|||
separators = [cmds.menuItem(i, query=True, label=True) for i in items] # noqa: E501
|
||||
try:
|
||||
sep_idx = separators.index(aov_separator)
|
||||
except ValueError as e:
|
||||
except ValueError:
|
||||
six.reraise(
|
||||
CreatorError,
|
||||
CreatorError(
|
||||
|
|
|
|||
|
|
@ -683,7 +683,6 @@ class ReferenceLoader(Loader):
|
|||
loaded_containers.append(container)
|
||||
self._organize_containers(nodes, container)
|
||||
c += 1
|
||||
namespace = None
|
||||
|
||||
return loaded_containers
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ class ImportReference(InventoryAction):
|
|||
color = "#d8d8d8"
|
||||
|
||||
def process(self, containers):
|
||||
references = cmds.ls(type="reference")
|
||||
for container in containers:
|
||||
if container["loader"] != "ReferenceLoader":
|
||||
print("Not a reference, skipping")
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ class MultiverseUsdLoader(load.LoaderPlugin):
|
|||
import multiverse
|
||||
|
||||
# Create the shape
|
||||
shape = None
|
||||
transform = None
|
||||
with maintained_selection():
|
||||
cmds.namespace(addNamespace=namespace)
|
||||
with namespaced(namespace, new=False):
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
cmds.setAttr("{}.selectHandleZ".format(group_name), cz)
|
||||
|
||||
if family == "rig":
|
||||
self._post_process_rig(name, namespace, context, options)
|
||||
self._post_process_rig(namespace, context, options)
|
||||
else:
|
||||
if "translate" in options:
|
||||
if not attach_to_root and new_nodes:
|
||||
|
|
@ -229,7 +229,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
members = get_container_members(container)
|
||||
self._lock_camera_transforms(members)
|
||||
|
||||
def _post_process_rig(self, name, namespace, context, options):
|
||||
def _post_process_rig(self, namespace, context, options):
|
||||
|
||||
nodes = self[:]
|
||||
create_rig_animation_instance(
|
||||
|
|
|
|||
|
|
@ -53,8 +53,6 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
)
|
||||
|
||||
# Reference xgen. Xgen does not like being referenced in under a group.
|
||||
new_nodes = []
|
||||
|
||||
with maintained_selection():
|
||||
nodes = cmds.file(
|
||||
maya_filepath,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,16 @@ from openpype.hosts.maya.api import lib
|
|||
from openpype.hosts.maya.api.pipeline import containerise
|
||||
|
||||
|
||||
# Do not reset these values on update but only apply on first load
|
||||
# to preserve any potential local overrides
|
||||
SKIP_UPDATE_ATTRS = {
|
||||
"displayOutput",
|
||||
"viewportDensity",
|
||||
"viewportWidth",
|
||||
"viewportLength",
|
||||
}
|
||||
|
||||
|
||||
def set_attribute(node, attr, value):
|
||||
"""Wrapper of set attribute which ignores None values"""
|
||||
if value is None:
|
||||
|
|
@ -205,6 +215,8 @@ class YetiCacheLoader(load.LoaderPlugin):
|
|||
yeti_node = yeti_nodes[0]
|
||||
|
||||
for attr, value in node_settings["attrs"].items():
|
||||
if attr in SKIP_UPDATE_ATTRS:
|
||||
continue
|
||||
set_attribute(attr, value, yeti_node)
|
||||
|
||||
cmds.setAttr("{}.representation".format(container_node),
|
||||
|
|
@ -311,7 +323,6 @@ class YetiCacheLoader(load.LoaderPlugin):
|
|||
# Update attributes with defaults
|
||||
attributes = node_settings["attrs"]
|
||||
attributes.update({
|
||||
"viewportDensity": 0.1,
|
||||
"verbosity": 2,
|
||||
"fileMode": 1,
|
||||
|
||||
|
|
@ -321,6 +332,9 @@ class YetiCacheLoader(load.LoaderPlugin):
|
|||
"visibleInRefractions": True
|
||||
})
|
||||
|
||||
if "viewportDensity" not in attributes:
|
||||
attributes["viewportDensity"] = 0.1
|
||||
|
||||
# Apply attributes to pgYetiMaya node
|
||||
for attr, value in attributes.items():
|
||||
set_attribute(attr, value, yeti_node)
|
||||
|
|
|
|||
|
|
@ -281,7 +281,6 @@ class CollectMultiverseLookData(pyblish.api.InstancePlugin):
|
|||
long=True)
|
||||
nodes.update(nodes_of_interest)
|
||||
|
||||
files = []
|
||||
sets = {}
|
||||
instance.data["resources"] = []
|
||||
publishMipMap = instance.data["publishMipMap"]
|
||||
|
|
|
|||
39
openpype/hosts/maya/plugins/publish/collect_rig_sets.py
Normal file
39
openpype/hosts/maya/plugins/publish/collect_rig_sets.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import pyblish.api
|
||||
from maya import cmds
|
||||
|
||||
|
||||
class CollectRigSets(pyblish.api.InstancePlugin):
|
||||
"""Ensure rig contains pipeline-critical content
|
||||
|
||||
Every rig must contain at least two object sets:
|
||||
"controls_SET" - Set of all animatable controls
|
||||
"out_SET" - Set of all cacheable meshes
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.05
|
||||
label = "Collect Rig Sets"
|
||||
hosts = ["maya"]
|
||||
families = ["rig"]
|
||||
|
||||
accepted_output = ["mesh", "transform"]
|
||||
accepted_controllers = ["transform"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# Find required sets by suffix
|
||||
searching = {"controls_SET", "out_SET"}
|
||||
found = {}
|
||||
for node in cmds.ls(instance, exactType="objectSet"):
|
||||
for suffix in searching:
|
||||
if node.endswith(suffix):
|
||||
found[suffix] = node
|
||||
searching.remove(suffix)
|
||||
break
|
||||
if not searching:
|
||||
break
|
||||
|
||||
self.log.debug("Found sets: {}".format(found))
|
||||
rig_sets = instance.data.setdefault("rig_sets", {})
|
||||
for name, objset in found.items():
|
||||
rig_sets[name] = objset
|
||||
|
|
@ -4,12 +4,23 @@ import pyblish.api
|
|||
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
SETTINGS = {"renderDensity",
|
||||
"renderWidth",
|
||||
"renderLength",
|
||||
"increaseRenderBounds",
|
||||
"imageSearchPath",
|
||||
"cbId"}
|
||||
|
||||
SETTINGS = {
|
||||
# Preview
|
||||
"displayOutput",
|
||||
"colorR", "colorG", "colorB",
|
||||
"viewportDensity",
|
||||
"viewportWidth",
|
||||
"viewportLength",
|
||||
# Render attributes
|
||||
"renderDensity",
|
||||
"renderWidth",
|
||||
"renderLength",
|
||||
"increaseRenderBounds",
|
||||
"imageSearchPath",
|
||||
# Pipeline specific
|
||||
"cbId"
|
||||
}
|
||||
|
||||
|
||||
class CollectYetiCache(pyblish.api.InstancePlugin):
|
||||
|
|
@ -39,10 +50,6 @@ class CollectYetiCache(pyblish.api.InstancePlugin):
|
|||
# Get yeti nodes and their transforms
|
||||
yeti_shapes = cmds.ls(instance, type="pgYetiMaya")
|
||||
for shape in yeti_shapes:
|
||||
shape_data = {"transform": None,
|
||||
"name": shape,
|
||||
"cbId": lib.get_id(shape),
|
||||
"attrs": None}
|
||||
|
||||
# Get specific node attributes
|
||||
attr_data = {}
|
||||
|
|
@ -58,9 +65,12 @@ class CollectYetiCache(pyblish.api.InstancePlugin):
|
|||
parent = cmds.listRelatives(shape, parent=True)[0]
|
||||
transform_data = {"name": parent, "cbId": lib.get_id(parent)}
|
||||
|
||||
# Store collected data
|
||||
shape_data["attrs"] = attr_data
|
||||
shape_data["transform"] = transform_data
|
||||
shape_data = {
|
||||
"transform": transform_data,
|
||||
"name": shape,
|
||||
"cbId": lib.get_id(shape),
|
||||
"attrs": attr_data,
|
||||
}
|
||||
|
||||
settings["nodes"].append(shape_data)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ class ExtractImportReference(publish.Extractor,
|
|||
tmp_format = "_tmp"
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, project_setting, system_settings):
|
||||
cls.active = project_setting["deadline"]["publish"]["MayaSubmitDeadline"]["import_reference"] # noqa
|
||||
def apply_settings(cls, project_settings):
|
||||
cls.active = project_settings["deadline"]["publish"]["MayaSubmitDeadline"]["import_reference"] # noqa
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin):
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, project_settings, system_settings):
|
||||
def apply_settings(cls, project_settings):
|
||||
"""Apply project settings to creator"""
|
||||
settings = (
|
||||
project_settings["maya"]["publish"]["ValidateMayaUnits"]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ import pyblish.api
|
|||
from maya import cmds
|
||||
|
||||
from openpype.pipeline.publish import (
|
||||
PublishValidationError, ValidateContentsOrder)
|
||||
PublishValidationError,
|
||||
ValidateContentsOrder
|
||||
)
|
||||
|
||||
|
||||
class ValidateRigContents(pyblish.api.InstancePlugin):
|
||||
|
|
@ -24,31 +26,45 @@ class ValidateRigContents(pyblish.api.InstancePlugin):
|
|||
|
||||
def process(self, instance):
|
||||
|
||||
objectsets = ("controls_SET", "out_SET")
|
||||
missing = [obj for obj in objectsets if obj not in instance]
|
||||
assert not missing, ("%s is missing %s" % (instance, missing))
|
||||
# Find required sets by suffix
|
||||
required = ["controls_SET", "out_SET"]
|
||||
missing = [
|
||||
key for key in required if key not in instance.data["rig_sets"]
|
||||
]
|
||||
if missing:
|
||||
raise PublishValidationError(
|
||||
"%s is missing sets: %s" % (instance, ", ".join(missing))
|
||||
)
|
||||
|
||||
controls_set = instance.data["rig_sets"]["controls_SET"]
|
||||
out_set = instance.data["rig_sets"]["out_SET"]
|
||||
|
||||
# Ensure there are at least some transforms or dag nodes
|
||||
# in the rig instance
|
||||
set_members = instance.data['setMembers']
|
||||
if not cmds.ls(set_members, type="dagNode", long=True):
|
||||
raise PublishValidationError(
|
||||
("No dag nodes in the pointcache instance. "
|
||||
"(Empty instance?)"))
|
||||
"No dag nodes in the pointcache instance. "
|
||||
"(Empty instance?)"
|
||||
)
|
||||
|
||||
# Ensure contents in sets and retrieve long path for all objects
|
||||
output_content = cmds.sets("out_SET", query=True) or []
|
||||
assert output_content, "Must have members in rig out_SET"
|
||||
output_content = cmds.sets(out_set, query=True) or []
|
||||
if not output_content:
|
||||
raise PublishValidationError("Must have members in rig out_SET")
|
||||
output_content = cmds.ls(output_content, long=True)
|
||||
|
||||
controls_content = cmds.sets("controls_SET", query=True) or []
|
||||
assert controls_content, "Must have members in rig controls_SET"
|
||||
controls_content = cmds.sets(controls_set, query=True) or []
|
||||
if not controls_content:
|
||||
raise PublishValidationError(
|
||||
"Must have members in rig controls_SET"
|
||||
)
|
||||
controls_content = cmds.ls(controls_content, long=True)
|
||||
|
||||
# Validate members are inside the hierarchy from root node
|
||||
root_node = cmds.ls(set_members, assemblies=True)
|
||||
hierarchy = cmds.listRelatives(root_node, allDescendents=True,
|
||||
fullPath=True)
|
||||
root_nodes = cmds.ls(set_members, assemblies=True, long=True)
|
||||
hierarchy = cmds.listRelatives(root_nodes, allDescendents=True,
|
||||
fullPath=True) + root_nodes
|
||||
hierarchy = set(hierarchy)
|
||||
|
||||
invalid_hierarchy = []
|
||||
|
|
|
|||
|
|
@ -52,22 +52,30 @@ class ValidateRigControllers(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise PublishValidationError('{} failed, see log '
|
||||
'information'.format(self.label))
|
||||
raise PublishValidationError(
|
||||
'{} failed, see log information'.format(self.label)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
||||
controllers_sets = [i for i in instance if i == "controls_SET"]
|
||||
controls = cmds.sets(controllers_sets, query=True)
|
||||
assert controls, "Must have 'controls_SET' in rig instance"
|
||||
controls_set = instance.data["rig_sets"].get("controls_SET")
|
||||
if not controls_set:
|
||||
cls.log.error(
|
||||
"Must have 'controls_SET' in rig instance"
|
||||
)
|
||||
return [instance.data["instance_node"]]
|
||||
|
||||
controls = cmds.sets(controls_set, query=True)
|
||||
|
||||
# Ensure all controls are within the top group
|
||||
lookup = set(instance[:])
|
||||
assert all(control in lookup for control in cmds.ls(controls,
|
||||
long=True)), (
|
||||
"All controls must be inside the rig's group."
|
||||
)
|
||||
if not all(control in lookup for control in cmds.ls(controls,
|
||||
long=True)):
|
||||
cls.log.error(
|
||||
"All controls must be inside the rig's group."
|
||||
)
|
||||
return [controls_set]
|
||||
|
||||
# Validate all controls
|
||||
has_connections = list()
|
||||
|
|
@ -181,9 +189,17 @@ class ValidateRigControllers(pyblish.api.InstancePlugin):
|
|||
@classmethod
|
||||
def repair(cls, instance):
|
||||
|
||||
controls_set = instance.data["rig_sets"].get("controls_SET")
|
||||
if not controls_set:
|
||||
cls.log.error(
|
||||
"Unable to repair because no 'controls_SET' found in rig "
|
||||
"instance: {}".format(instance)
|
||||
)
|
||||
return
|
||||
|
||||
# Use a single undo chunk
|
||||
with undo_chunk():
|
||||
controls = cmds.sets("controls_SET", query=True)
|
||||
controls = cmds.sets(controls_set, query=True)
|
||||
for control in controls:
|
||||
|
||||
# Lock visibility
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@ class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin):
|
|||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
||||
controllers_sets = [i for i in instance if i == "controls_SET"]
|
||||
if not controllers_sets:
|
||||
controls_set = instance.data["rig_sets"].get("controls_SET")
|
||||
if not controls_set:
|
||||
return []
|
||||
|
||||
controls = cmds.sets(controllers_sets, query=True) or []
|
||||
controls = cmds.sets(controls_set, query=True) or []
|
||||
if not controls:
|
||||
return []
|
||||
|
||||
|
|
|
|||
|
|
@ -38,16 +38,19 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin):
|
|||
# if a deformer has been created on the shape
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise PublishValidationError("Nodes found with mismatching "
|
||||
"IDs: {0}".format(invalid))
|
||||
raise PublishValidationError(
|
||||
"Nodes found with mismatching IDs: {0}".format(invalid)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
"""Get all nodes which do not match the criteria"""
|
||||
|
||||
invalid = []
|
||||
out_set = instance.data["rig_sets"].get("out_SET")
|
||||
if not out_set:
|
||||
return []
|
||||
|
||||
out_set = next(x for x in instance if x.endswith("out_SET"))
|
||||
invalid = []
|
||||
members = cmds.sets(out_set, query=True)
|
||||
shapes = cmds.ls(members,
|
||||
dag=True,
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin):
|
|||
invalid = {}
|
||||
|
||||
if compute:
|
||||
out_set = next(x for x in instance if "out_SET" in x)
|
||||
out_set = instance.data["rig_sets"].get("out_SET")
|
||||
if not out_set:
|
||||
instance.data["mismatched_output_ids"] = invalid
|
||||
return invalid
|
||||
|
||||
instance_nodes = cmds.sets(out_set, query=True, nodesOnly=True)
|
||||
instance_nodes = cmds.ls(instance_nodes, long=True)
|
||||
|
|
|
|||
|
|
@ -138,8 +138,13 @@ def create_items_from_nodes(nodes):
|
|||
asset_doc = asset_docs_by_id.get(asset_id)
|
||||
# Skip if asset id is not found
|
||||
if not asset_doc:
|
||||
log.warning("Id not found in the database, skipping '%s'." % _id)
|
||||
log.warning("Nodes: %s" % id_nodes)
|
||||
log.warning(
|
||||
"Id found on {num} nodes for which no asset is found database,"
|
||||
" skipping '{asset_id}'".format(
|
||||
num=len(nodes),
|
||||
asset_id=asset_id
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# Collect available look subsets for this asset
|
||||
|
|
|
|||
|
|
@ -90,15 +90,13 @@ class AssetOutliner(QtWidgets.QWidget):
|
|||
def get_all_assets(self):
|
||||
"""Add all items from the current scene"""
|
||||
|
||||
items = []
|
||||
with preserve_expanded_rows(self.view):
|
||||
with preserve_selection(self.view):
|
||||
self.clear()
|
||||
nodes = commands.get_all_asset_nodes()
|
||||
items = commands.create_items_from_nodes(nodes)
|
||||
self.add_items(items)
|
||||
|
||||
return len(items) > 0
|
||||
return len(items) > 0
|
||||
|
||||
def get_selected_assets(self):
|
||||
"""Add all selected items from the current scene"""
|
||||
|
|
|
|||
|
|
@ -112,8 +112,6 @@ class AlembicCameraLoader(load.LoaderPlugin):
|
|||
version_doc = get_version_by_id(project_name, representation["parent"])
|
||||
|
||||
object_name = container['objectName']
|
||||
# get corresponding node
|
||||
camera_node = nuke.toNode(object_name)
|
||||
|
||||
# get main variables
|
||||
version_data = version_doc.get("data", {})
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ class ExtractReviewDataLut(publish.Extractor):
|
|||
hosts = ["nuke"]
|
||||
|
||||
def process(self, instance):
|
||||
families = instance.data["families"]
|
||||
self.log.info("Creating staging dir...")
|
||||
if "representations" in instance.data:
|
||||
staging_dir = instance.data[
|
||||
|
|
|
|||
|
|
@ -91,8 +91,6 @@ class ExtractThumbnail(publish.Extractor):
|
|||
|
||||
if collection:
|
||||
# get path
|
||||
fname = os.path.basename(collection.format(
|
||||
"{head}{padding}{tail}"))
|
||||
fhead = collection.format("{head}")
|
||||
|
||||
thumb_fname = list(collection)[mid_frame]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ class CollectAutoImage(pyblish.api.ContextPlugin):
|
|||
targets = ["automated"]
|
||||
|
||||
def process(self, context):
|
||||
family = "image"
|
||||
for instance in context:
|
||||
creator_identifier = instance.data.get("creator_identifier")
|
||||
if creator_identifier and creator_identifier == "auto_image":
|
||||
|
|
|
|||
|
|
@ -413,8 +413,6 @@ class ClipLoader:
|
|||
if self.with_handles:
|
||||
source_in -= handle_start
|
||||
source_out += handle_end
|
||||
handle_start = 0
|
||||
handle_end = 0
|
||||
|
||||
# make track item from source in bin as item
|
||||
timeline_item = lib.create_timeline_item(
|
||||
|
|
@ -433,14 +431,6 @@ class ClipLoader:
|
|||
self.data["path"], self.active_bin)
|
||||
_clip_property = media_pool_item.GetClipProperty
|
||||
|
||||
# get handles
|
||||
handle_start = self.data["versionData"].get("handleStart")
|
||||
handle_end = self.data["versionData"].get("handleEnd")
|
||||
if handle_start is None:
|
||||
handle_start = int(self.data["assetData"]["handleStart"])
|
||||
if handle_end is None:
|
||||
handle_end = int(self.data["assetData"]["handleEnd"])
|
||||
|
||||
source_in = int(_clip_property("Start"))
|
||||
source_out = int(_clip_property("End"))
|
||||
|
||||
|
|
|
|||
|
|
@ -49,8 +49,6 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
|
|||
else:
|
||||
first_filename = files
|
||||
|
||||
staging_dir = None
|
||||
|
||||
# Convert to jpeg if not yet
|
||||
full_input_path = os.path.join(
|
||||
thumbnail_repre["stagingDir"], first_filename
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@ import pyblish.api
|
|||
from openpype.pipeline import OptionalPyblishPluginMixin
|
||||
|
||||
|
||||
class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Collect Frame Range data From Asset Entity
|
||||
class CollectMissingFrameDataFromAssetEntity(
|
||||
pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin
|
||||
):
|
||||
"""Collect Missing Frame Range data From Asset Entity
|
||||
|
||||
Frame range data will only be collected if the keys
|
||||
are not yet collected for the instance.
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.491
|
||||
label = "Collect Frame Data From Asset Entity"
|
||||
label = "Collect Missing Frame Data From Asset Entity"
|
||||
families = ["plate", "pointcache",
|
||||
"vdbcache", "online",
|
||||
"render"]
|
||||
|
|
@ -15,7 +15,7 @@ class ValidateFrameRange(OptionalPyblishPluginMixin,
|
|||
|
||||
label = "Validate Frame Range"
|
||||
hosts = ["traypublisher"]
|
||||
families = ["render"]
|
||||
families = ["render", "plate"]
|
||||
order = ValidateContentsOrder
|
||||
|
||||
optional = True
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ class LoadImage(plugin.Loader):
|
|||
george_script = "\n".join(george_script_lines)
|
||||
execute_george_through_file(george_script)
|
||||
|
||||
def _remove_container(self, container, members=None):
|
||||
def _remove_container(self, container):
|
||||
if not container:
|
||||
return
|
||||
representation = container["representation"]
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
"ignoreLayersTransparency", False
|
||||
)
|
||||
|
||||
family_lowered = instance.data["family"].lower()
|
||||
mark_in = instance.context.data["sceneMarkIn"]
|
||||
mark_out = instance.context.data["sceneMarkOut"]
|
||||
|
||||
|
|
@ -76,11 +75,9 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
|
||||
# Frame start/end may be stored as float
|
||||
frame_start = int(instance.data["frameStart"])
|
||||
frame_end = int(instance.data["frameEnd"])
|
||||
|
||||
# Handles are not stored per instance but on Context
|
||||
handle_start = instance.context.data["handleStart"]
|
||||
handle_end = instance.context.data["handleEnd"]
|
||||
|
||||
scene_bg_color = instance.context.data["sceneBgColor"]
|
||||
|
||||
|
|
|
|||
|
|
@ -19,9 +19,8 @@ class ExtractUAsset(publish.Extractor):
|
|||
"umap" if "umap" in instance.data.get("families") else "uasset")
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.debug("Performing extraction..")
|
||||
staging_dir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.{extension}"
|
||||
|
||||
members = instance.data.get("members", [])
|
||||
|
||||
|
|
|
|||
|
|
@ -724,7 +724,7 @@ def get_ffprobe_data(path_to_file, logger=None):
|
|||
"""
|
||||
if not logger:
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(
|
||||
logger.debug(
|
||||
"Getting information about input \"{}\".".format(path_to_file)
|
||||
)
|
||||
ffprobe_args = get_ffmpeg_tool_args("ffprobe")
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
component_list = instance.data.get("ftrackComponentsList")
|
||||
if not component_list:
|
||||
self.log.info(
|
||||
"Instance don't have components to integrate to Ftrack."
|
||||
self.log.debug(
|
||||
"Instance doesn't have components to integrate to Ftrack."
|
||||
" Skipping."
|
||||
)
|
||||
return
|
||||
|
|
@ -37,7 +37,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
task_entity, parent_entity = self.get_instance_entities(
|
||||
instance, context)
|
||||
if parent_entity is None:
|
||||
self.log.info((
|
||||
self.log.debug((
|
||||
"Skipping ftrack integration. Instance \"{}\" does not"
|
||||
" have specified ftrack entities."
|
||||
).format(str(instance)))
|
||||
|
|
@ -323,7 +323,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
"type_id": asset_type_id,
|
||||
"context_id": parent_id
|
||||
}
|
||||
self.log.info("Created new Asset with data: {}.".format(asset_data))
|
||||
self.log.debug("Created new Asset with data: {}.".format(asset_data))
|
||||
session.create("Asset", asset_data)
|
||||
session.commit()
|
||||
return self._query_asset(session, asset_name, asset_type_id, parent_id)
|
||||
|
|
@ -384,7 +384,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
if comment:
|
||||
new_asset_version_data["comment"] = comment
|
||||
|
||||
self.log.info("Created new AssetVersion with data {}".format(
|
||||
self.log.debug("Created new AssetVersion with data {}".format(
|
||||
new_asset_version_data
|
||||
))
|
||||
session.create("AssetVersion", new_asset_version_data)
|
||||
|
|
@ -555,7 +555,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
location=location
|
||||
)
|
||||
data["component"] = component_entity
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
(
|
||||
"Created new Component with path: {0}, data: {1},"
|
||||
" metadata: {2}, location: {3}"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class IntegrateFtrackDescription(pyblish.api.InstancePlugin):
|
|||
|
||||
comment = instance.data["comment"]
|
||||
if not comment:
|
||||
self.log.info("Comment is not set.")
|
||||
self.log.debug("Comment is not set.")
|
||||
else:
|
||||
self.log.debug("Comment is set to `{}`".format(comment))
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin):
|
|||
app_label = context.data["appLabel"]
|
||||
comment = instance.data["comment"]
|
||||
if not comment:
|
||||
self.log.info("Comment is not set.")
|
||||
self.log.debug("Comment is not set.")
|
||||
else:
|
||||
self.log.debug("Comment is set to `{}`".format(comment))
|
||||
|
||||
|
|
@ -127,14 +127,14 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin):
|
|||
|
||||
note_text = StringTemplate.format_template(template, format_data)
|
||||
if not note_text.solved:
|
||||
self.log.warning((
|
||||
self.log.debug((
|
||||
"Note template require more keys then can be provided."
|
||||
"\nTemplate: {}\nMissing values for keys:{}\nData: {}"
|
||||
).format(template, note_text.missing_keys, format_data))
|
||||
continue
|
||||
|
||||
if not note_text:
|
||||
self.log.info((
|
||||
self.log.debug((
|
||||
"Note for AssetVersion {} would be empty. Skipping."
|
||||
"\nTemplate: {}\nData: {}"
|
||||
).format(asset_version["id"], template, format_data))
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin):
|
|||
publish_comment = self.format_publish_comment(instance)
|
||||
|
||||
if not publish_comment:
|
||||
self.log.info("Comment is not set.")
|
||||
self.log.debug("Comment is not set.")
|
||||
else:
|
||||
self.log.debug("Comment is `{}`".format(publish_comment))
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,17 @@ from openpype.lib import (
|
|||
Logger
|
||||
)
|
||||
from openpype.pipeline import Anatomy
|
||||
from openpype.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS
|
||||
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
||||
class CashedData:
|
||||
remapping = None
|
||||
class CachedData:
|
||||
remapping = {}
|
||||
allowed_exts = {
|
||||
ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
|
||||
}
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
@ -546,15 +551,15 @@ def get_remapped_colorspace_to_native(
|
|||
Union[str, None]: native colorspace name defined in remapping or None
|
||||
"""
|
||||
|
||||
CashedData.remapping.setdefault(host_name, {})
|
||||
if CashedData.remapping[host_name].get("to_native") is None:
|
||||
CachedData.remapping.setdefault(host_name, {})
|
||||
if CachedData.remapping[host_name].get("to_native") is None:
|
||||
remapping_rules = imageio_host_settings["remapping"]["rules"]
|
||||
CashedData.remapping[host_name]["to_native"] = {
|
||||
CachedData.remapping[host_name]["to_native"] = {
|
||||
rule["ocio_name"]: rule["host_native_name"]
|
||||
for rule in remapping_rules
|
||||
}
|
||||
|
||||
return CashedData.remapping[host_name]["to_native"].get(
|
||||
return CachedData.remapping[host_name]["to_native"].get(
|
||||
ocio_colorspace_name)
|
||||
|
||||
|
||||
|
|
@ -572,15 +577,15 @@ def get_remapped_colorspace_from_native(
|
|||
Union[str, None]: Ocio colorspace name defined in remapping or None.
|
||||
"""
|
||||
|
||||
CashedData.remapping.setdefault(host_name, {})
|
||||
if CashedData.remapping[host_name].get("from_native") is None:
|
||||
CachedData.remapping.setdefault(host_name, {})
|
||||
if CachedData.remapping[host_name].get("from_native") is None:
|
||||
remapping_rules = imageio_host_settings["remapping"]["rules"]
|
||||
CashedData.remapping[host_name]["from_native"] = {
|
||||
CachedData.remapping[host_name]["from_native"] = {
|
||||
rule["host_native_name"]: rule["ocio_name"]
|
||||
for rule in remapping_rules
|
||||
}
|
||||
|
||||
return CashedData.remapping[host_name]["from_native"].get(
|
||||
return CachedData.remapping[host_name]["from_native"].get(
|
||||
host_native_colorspace_name)
|
||||
|
||||
|
||||
|
|
@ -601,3 +606,173 @@ def _get_imageio_settings(project_settings, host_name):
|
|||
imageio_host = project_settings.get(host_name, {}).get("imageio", {})
|
||||
|
||||
return imageio_global, imageio_host
|
||||
|
||||
|
||||
def get_colorspace_settings_from_publish_context(context_data):
|
||||
"""Returns solved settings for the host context.
|
||||
|
||||
Args:
|
||||
context_data (publish.Context.data): publishing context data
|
||||
|
||||
Returns:
|
||||
tuple | bool: config, file rules or None
|
||||
"""
|
||||
if "imageioSettings" in context_data and context_data["imageioSettings"]:
|
||||
return context_data["imageioSettings"]
|
||||
|
||||
project_name = context_data["projectName"]
|
||||
host_name = context_data["hostName"]
|
||||
anatomy_data = context_data["anatomyData"]
|
||||
project_settings_ = context_data["project_settings"]
|
||||
|
||||
config_data = get_imageio_config(
|
||||
project_name, host_name,
|
||||
project_settings=project_settings_,
|
||||
anatomy_data=anatomy_data
|
||||
)
|
||||
|
||||
# caching invalid state, so it's not recalculated all the time
|
||||
file_rules = None
|
||||
if config_data:
|
||||
file_rules = get_imageio_file_rules(
|
||||
project_name, host_name,
|
||||
project_settings=project_settings_
|
||||
)
|
||||
|
||||
# caching settings for future instance processing
|
||||
context_data["imageioSettings"] = (config_data, file_rules)
|
||||
|
||||
return config_data, file_rules
|
||||
|
||||
|
||||
def set_colorspace_data_to_representation(
|
||||
representation, context_data,
|
||||
colorspace=None,
|
||||
log=None
|
||||
):
|
||||
"""Sets colorspace data to representation.
|
||||
|
||||
Args:
|
||||
representation (dict): publishing representation
|
||||
context_data (publish.Context.data): publishing context data
|
||||
colorspace (str, optional): colorspace name. Defaults to None.
|
||||
log (logging.Logger, optional): logger instance. Defaults to None.
|
||||
|
||||
Example:
|
||||
```
|
||||
{
|
||||
# for other publish plugins and loaders
|
||||
"colorspace": "linear",
|
||||
"config": {
|
||||
# for future references in case need
|
||||
"path": "/abs/path/to/config.ocio",
|
||||
# for other plugins within remote publish cases
|
||||
"template": "{project[root]}/path/to/config.ocio"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
"""
|
||||
log = log or Logger.get_logger(__name__)
|
||||
|
||||
file_ext = representation["ext"]
|
||||
|
||||
# check if `file_ext` in lower case is in CachedData.allowed_exts
|
||||
if file_ext.lstrip(".").lower() not in CachedData.allowed_exts:
|
||||
log.debug(
|
||||
"Extension '{}' is not in allowed extensions.".format(file_ext)
|
||||
)
|
||||
return
|
||||
|
||||
# get colorspace settings
|
||||
config_data, file_rules = get_colorspace_settings_from_publish_context(
|
||||
context_data)
|
||||
|
||||
# in case host color management is not enabled
|
||||
if not config_data:
|
||||
log.warning("Host's colorspace management is disabled.")
|
||||
return
|
||||
|
||||
log.debug("Config data is: `{}`".format(config_data))
|
||||
|
||||
project_name = context_data["projectName"]
|
||||
host_name = context_data["hostName"]
|
||||
project_settings = context_data["project_settings"]
|
||||
|
||||
# get one filename
|
||||
filename = representation["files"]
|
||||
if isinstance(filename, list):
|
||||
filename = filename[0]
|
||||
|
||||
# get matching colorspace from rules
|
||||
colorspace = colorspace or get_imageio_colorspace_from_filepath(
|
||||
filename, host_name, project_name,
|
||||
config_data=config_data,
|
||||
file_rules=file_rules,
|
||||
project_settings=project_settings
|
||||
)
|
||||
|
||||
# infuse data to representation
|
||||
if colorspace:
|
||||
colorspace_data = {
|
||||
"colorspace": colorspace,
|
||||
"config": config_data
|
||||
}
|
||||
|
||||
# update data key
|
||||
representation["colorspaceData"] = colorspace_data
|
||||
|
||||
|
||||
def get_display_view_colorspace_name(config_path, display, view):
|
||||
"""Returns the colorspace attribute of the (display, view) pair.
|
||||
|
||||
Args:
|
||||
config_path (str): path string leading to config.ocio
|
||||
display (str): display name e.g. "ACES"
|
||||
view (str): view name e.g. "sRGB"
|
||||
|
||||
Returns:
|
||||
view color space name (str) e.g. "Output - sRGB"
|
||||
"""
|
||||
|
||||
if not compatibility_check():
|
||||
# python environment is not compatible with PyOpenColorIO
|
||||
# needs to be run in subprocess
|
||||
return get_display_view_colorspace_subprocess(config_path,
|
||||
display, view)
|
||||
|
||||
from openpype.scripts.ocio_wrapper import _get_display_view_colorspace_name # noqa
|
||||
|
||||
return _get_display_view_colorspace_name(config_path, display, view)
|
||||
|
||||
|
||||
def get_display_view_colorspace_subprocess(config_path, display, view):
|
||||
"""Returns the colorspace attribute of the (display, view) pair
|
||||
via subprocess.
|
||||
|
||||
Args:
|
||||
config_path (str): path string leading to config.ocio
|
||||
display (str): display name e.g. "ACES"
|
||||
view (str): view name e.g. "sRGB"
|
||||
|
||||
Returns:
|
||||
view color space name (str) e.g. "Output - sRGB"
|
||||
"""
|
||||
|
||||
with _make_temp_json_file() as tmp_json_path:
|
||||
# Prepare subprocess arguments
|
||||
args = [
|
||||
"run", get_ocio_config_script_path(),
|
||||
"config", "get_display_view_colorspace_name",
|
||||
"--in_path", config_path,
|
||||
"--out_path", tmp_json_path,
|
||||
"--display", display,
|
||||
"--view", view
|
||||
]
|
||||
log.debug("Executing: {}".format(" ".join(args)))
|
||||
|
||||
run_openpype_process(*args, logger=log)
|
||||
|
||||
# return default view colorspace name
|
||||
with open(tmp_json_path, "r") as f:
|
||||
return json.load(f)
|
||||
|
|
|
|||
|
|
@ -234,6 +234,19 @@ class LoaderPlugin(list):
|
|||
"""
|
||||
return cls.options or []
|
||||
|
||||
@property
|
||||
def fname(self):
|
||||
"""Backwards compatibility with deprecation warning"""
|
||||
|
||||
self.log.warning((
|
||||
"DEPRECATION WARNING: Source - Loader plugin {}."
|
||||
" The 'fname' property on the Loader plugin will be removed in"
|
||||
" future versions of OpenPype. Planned version to drop the support"
|
||||
" is 3.16.6 or 3.17.0."
|
||||
).format(self.__class__.__name__))
|
||||
if hasattr(self, "_fname"):
|
||||
return self._fname
|
||||
|
||||
|
||||
class SubsetLoaderPlugin(LoaderPlugin):
|
||||
"""Load subset into host application
|
||||
|
|
|
|||
|
|
@ -318,7 +318,8 @@ def load_with_repre_context(
|
|||
|
||||
# Backwards compatibility: Originally the loader's __init__ required the
|
||||
# representation context to set `fname` attribute to the filename to load
|
||||
loader.fname = get_representation_path_from_context(repre_context)
|
||||
# Deprecated - to be removed in OpenPype 3.16.6 or 3.17.0.
|
||||
loader._fname = get_representation_path_from_context(repre_context)
|
||||
|
||||
return loader.load(repre_context, name, namespace, options)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import inspect
|
||||
from abc import ABCMeta
|
||||
from pprint import pformat
|
||||
import pyblish.api
|
||||
from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin
|
||||
from openpype.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS
|
||||
|
|
@ -14,9 +13,8 @@ from .lib import (
|
|||
)
|
||||
|
||||
from openpype.pipeline.colorspace import (
|
||||
get_imageio_colorspace_from_filepath,
|
||||
get_imageio_config,
|
||||
get_imageio_file_rules
|
||||
get_colorspace_settings_from_publish_context,
|
||||
set_colorspace_data_to_representation
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -306,12 +304,8 @@ class ColormanagedPyblishPluginMixin(object):
|
|||
matching colorspace from rules. Finally, it infuses this
|
||||
data into the representation.
|
||||
"""
|
||||
allowed_ext = set(
|
||||
ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_colorspace_settings(context):
|
||||
def get_colorspace_settings(self, context):
|
||||
"""Returns solved settings for the host context.
|
||||
|
||||
Args:
|
||||
|
|
@ -320,50 +314,18 @@ class ColormanagedPyblishPluginMixin(object):
|
|||
Returns:
|
||||
tuple | bool: config, file rules or None
|
||||
"""
|
||||
if "imageioSettings" in context.data:
|
||||
return context.data["imageioSettings"]
|
||||
|
||||
project_name = context.data["projectName"]
|
||||
host_name = context.data["hostName"]
|
||||
anatomy_data = context.data["anatomyData"]
|
||||
project_settings_ = context.data["project_settings"]
|
||||
|
||||
config_data = get_imageio_config(
|
||||
project_name, host_name,
|
||||
project_settings=project_settings_,
|
||||
anatomy_data=anatomy_data
|
||||
)
|
||||
|
||||
# in case host color management is not enabled
|
||||
if not config_data:
|
||||
return None
|
||||
|
||||
file_rules = get_imageio_file_rules(
|
||||
project_name, host_name,
|
||||
project_settings=project_settings_
|
||||
)
|
||||
|
||||
# caching settings for future instance processing
|
||||
context.data["imageioSettings"] = (config_data, file_rules)
|
||||
|
||||
return config_data, file_rules
|
||||
return get_colorspace_settings_from_publish_context(context.data)
|
||||
|
||||
def set_representation_colorspace(
|
||||
self, representation, context,
|
||||
colorspace=None,
|
||||
colorspace_settings=None
|
||||
):
|
||||
"""Sets colorspace data to representation.
|
||||
|
||||
Args:
|
||||
representation (dict): publishing representation
|
||||
context (publish.Context): publishing context
|
||||
config_data (dict): host resolved config data
|
||||
file_rules (dict): host resolved file rules data
|
||||
colorspace (str, optional): colorspace name. Defaults to None.
|
||||
colorspace_settings (tuple[dict, dict], optional):
|
||||
Settings for config_data and file_rules.
|
||||
Defaults to None.
|
||||
|
||||
Example:
|
||||
```
|
||||
|
|
@ -380,64 +342,10 @@ class ColormanagedPyblishPluginMixin(object):
|
|||
```
|
||||
|
||||
"""
|
||||
ext = representation["ext"]
|
||||
# check extension
|
||||
self.log.debug("__ ext: `{}`".format(ext))
|
||||
|
||||
# check if ext in lower case is in self.allowed_ext
|
||||
if ext.lstrip(".").lower() not in self.allowed_ext:
|
||||
self.log.debug(
|
||||
"Extension '{}' is not in allowed extensions.".format(ext)
|
||||
)
|
||||
return
|
||||
|
||||
if colorspace_settings is None:
|
||||
colorspace_settings = self.get_colorspace_settings(context)
|
||||
|
||||
# in case host color management is not enabled
|
||||
if not colorspace_settings:
|
||||
self.log.warning("Host's colorspace management is disabled.")
|
||||
return
|
||||
|
||||
# unpack colorspace settings
|
||||
config_data, file_rules = colorspace_settings
|
||||
|
||||
if not config_data:
|
||||
# warn in case no colorspace path was defined
|
||||
self.log.warning("No colorspace management was defined")
|
||||
return
|
||||
|
||||
self.log.debug("Config data is: `{}`".format(config_data))
|
||||
|
||||
project_name = context.data["projectName"]
|
||||
host_name = context.data["hostName"]
|
||||
project_settings = context.data["project_settings"]
|
||||
|
||||
# get one filename
|
||||
filename = representation["files"]
|
||||
if isinstance(filename, list):
|
||||
filename = filename[0]
|
||||
|
||||
self.log.debug("__ filename: `{}`".format(filename))
|
||||
|
||||
# get matching colorspace from rules
|
||||
colorspace = colorspace or get_imageio_colorspace_from_filepath(
|
||||
filename, host_name, project_name,
|
||||
config_data=config_data,
|
||||
file_rules=file_rules,
|
||||
project_settings=project_settings
|
||||
# using cached settings if available
|
||||
set_colorspace_data_to_representation(
|
||||
representation, context.data,
|
||||
colorspace,
|
||||
log=self.log
|
||||
)
|
||||
self.log.debug("__ colorspace: `{}`".format(colorspace))
|
||||
|
||||
# infuse data to representation
|
||||
if colorspace:
|
||||
colorspace_data = {
|
||||
"colorspace": colorspace,
|
||||
"config": config_data
|
||||
}
|
||||
|
||||
# update data key
|
||||
representation["colorspaceData"] = colorspace_data
|
||||
|
||||
self.log.debug("__ colorspace_data: `{}`".format(
|
||||
pformat(colorspace_data)))
|
||||
|
|
|
|||
|
|
@ -50,4 +50,7 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin):
|
|||
return {
|
||||
"frameStart": repres_frames[0],
|
||||
"frameEnd": repres_frames[-1],
|
||||
"handleStart": 0,
|
||||
"handleEnd": 0,
|
||||
"fps": instance.context.data["assetEntity"]["data"]["fps"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,5 +174,79 @@ def _get_views_data(config_path):
|
|||
return data
|
||||
|
||||
|
||||
def _get_display_view_colorspace_name(config_path, display, view):
|
||||
"""Returns the colorspace attribute of the (display, view) pair.
|
||||
|
||||
Args:
|
||||
config_path (str): path string leading to config.ocio
|
||||
display (str): display name e.g. "ACES"
|
||||
view (str): view name e.g. "sRGB"
|
||||
|
||||
|
||||
Raises:
|
||||
IOError: Input config does not exist.
|
||||
|
||||
Returns:
|
||||
view color space name (str) e.g. "Output - sRGB"
|
||||
"""
|
||||
|
||||
config_path = Path(config_path)
|
||||
|
||||
if not config_path.is_file():
|
||||
raise IOError("Input path should be `config.ocio` file")
|
||||
|
||||
config = ocio.Config.CreateFromFile(str(config_path))
|
||||
colorspace = config.getDisplayViewColorSpaceName(display, view)
|
||||
|
||||
return colorspace
|
||||
|
||||
|
||||
@config.command(
|
||||
name="get_display_view_colorspace_name",
|
||||
help=(
|
||||
"return default view colorspace name "
|
||||
"for the given display and view "
|
||||
"--path input arg is required"
|
||||
)
|
||||
)
|
||||
@click.option("--in_path", required=True,
|
||||
help="path where to read ocio config file",
|
||||
type=click.Path(exists=True))
|
||||
@click.option("--out_path", required=True,
|
||||
help="path where to write output json file",
|
||||
type=click.Path())
|
||||
@click.option("--display", required=True,
|
||||
help="display name",
|
||||
type=click.STRING)
|
||||
@click.option("--view", required=True,
|
||||
help="view name",
|
||||
type=click.STRING)
|
||||
def get_display_view_colorspace_name(in_path, out_path,
|
||||
display, view):
|
||||
"""Aggregate view colorspace name to file.
|
||||
|
||||
Wrapper command for processes without access to OpenColorIO
|
||||
|
||||
Args:
|
||||
in_path (str): config file path string
|
||||
out_path (str): temp json file path string
|
||||
display (str): display name e.g. "ACES"
|
||||
view (str): view name e.g. "sRGB"
|
||||
|
||||
Example of use:
|
||||
> pyton.exe ./ocio_wrapper.py config \
|
||||
get_display_view_colorspace_name --in_path=<path> \
|
||||
--out_path=<path> --display=<display> --view=<view>
|
||||
"""
|
||||
|
||||
out_data = _get_display_view_colorspace_name(in_path,
|
||||
display,
|
||||
view)
|
||||
|
||||
with open(out_path, "w") as f:
|
||||
json.dump(out_data, f)
|
||||
|
||||
print(f"Display view colorspace saved to '{out_path}'")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -93,6 +93,11 @@
|
|||
"$JOB"
|
||||
]
|
||||
},
|
||||
"ValidateReviewColorspace": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
},
|
||||
"ValidateContainers": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@
|
|||
"type": "schema_template",
|
||||
"name": "template_publish_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "ValidateReviewColorspace",
|
||||
"label": "Validate Review Colorspace"
|
||||
},
|
||||
{
|
||||
"key": "ValidateContainers",
|
||||
"label": "ValidateContainers"
|
||||
|
|
@ -47,4 +51,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1427,6 +1427,10 @@ CreateNextPageOverlay {
|
|||
background: rgba(0, 0, 0, 127);
|
||||
}
|
||||
|
||||
#OverlayFrameLabel {
|
||||
font-size: 15pt;
|
||||
}
|
||||
|
||||
#BreadcrumbsPathInput {
|
||||
padding: 2px;
|
||||
font-size: 9pt;
|
||||
|
|
|
|||
0
openpype/tools/ayon_workfiles/__init__.py
Normal file
0
openpype/tools/ayon_workfiles/__init__.py
Normal file
984
openpype/tools/ayon_workfiles/abstract.py
Normal file
984
openpype/tools/ayon_workfiles/abstract.py
Normal file
|
|
@ -0,0 +1,984 @@
|
|||
import os
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import six
|
||||
from openpype.style import get_default_entity_icon_color
|
||||
|
||||
|
||||
class WorkfileInfo:
|
||||
"""Information about workarea file with possible additional from database.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
filepath (str): Filepath.
|
||||
filesize (int): File size.
|
||||
creation_time (int): Creation time (timestamp).
|
||||
modification_time (int): Modification time (timestamp).
|
||||
note (str): Note.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
folder_id,
|
||||
task_id,
|
||||
filepath,
|
||||
filesize,
|
||||
creation_time,
|
||||
modification_time,
|
||||
note,
|
||||
):
|
||||
self.folder_id = folder_id
|
||||
self.task_id = task_id
|
||||
self.filepath = filepath
|
||||
self.filesize = filesize
|
||||
self.creation_time = creation_time
|
||||
self.modification_time = modification_time
|
||||
self.note = note
|
||||
|
||||
def to_data(self):
|
||||
"""Converts WorkfileInfo item to data.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Folder item data.
|
||||
"""
|
||||
|
||||
return {
|
||||
"folder_id": self.folder_id,
|
||||
"task_id": self.task_id,
|
||||
"filepath": self.filepath,
|
||||
"filesize": self.filesize,
|
||||
"creation_time": self.creation_time,
|
||||
"modification_time": self.modification_time,
|
||||
"note": self.note,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
"""Re-creates WorkfileInfo item from data.
|
||||
|
||||
Args:
|
||||
data (dict[str, Any]): Workfile info item data.
|
||||
|
||||
Returns:
|
||||
WorkfileInfo: Workfile info item.
|
||||
"""
|
||||
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class FolderItem:
|
||||
"""Item representing folder entity on a server.
|
||||
|
||||
Folder can be a child of another folder or a project.
|
||||
|
||||
Args:
|
||||
entity_id (str): Folder id.
|
||||
parent_id (Union[str, None]): Parent folder id. If 'None' then project
|
||||
is parent.
|
||||
name (str): Name of folder.
|
||||
label (str): Folder label.
|
||||
icon_name (str): Name of icon from font awesome.
|
||||
icon_color (str): Hex color string that will be used for icon.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, entity_id, parent_id, name, label, icon_name, icon_color
|
||||
):
|
||||
self.entity_id = entity_id
|
||||
self.parent_id = parent_id
|
||||
self.name = name
|
||||
self.icon_name = icon_name or "fa.folder"
|
||||
self.icon_color = icon_color or get_default_entity_icon_color()
|
||||
self.label = label or name
|
||||
|
||||
def to_data(self):
|
||||
"""Converts folder item to data.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Folder item data.
|
||||
"""
|
||||
|
||||
return {
|
||||
"entity_id": self.entity_id,
|
||||
"parent_id": self.parent_id,
|
||||
"name": self.name,
|
||||
"label": self.label,
|
||||
"icon_name": self.icon_name,
|
||||
"icon_color": self.icon_color,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
"""Re-creates folder item from data.
|
||||
|
||||
Args:
|
||||
data (dict[str, Any]): Folder item data.
|
||||
|
||||
Returns:
|
||||
FolderItem: Folder item.
|
||||
"""
|
||||
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class TaskItem:
|
||||
"""Task item representing task entity on a server.
|
||||
|
||||
Task is child of a folder.
|
||||
|
||||
Task item has label that is used for display in UI. The label is by
|
||||
default using task name and type.
|
||||
|
||||
Args:
|
||||
task_id (str): Task id.
|
||||
name (str): Name of task.
|
||||
task_type (str): Type of task.
|
||||
parent_id (str): Parent folder id.
|
||||
icon_name (str): Name of icon from font awesome.
|
||||
icon_color (str): Hex color string that will be used for icon.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, task_id, name, task_type, parent_id, icon_name, icon_color
|
||||
):
|
||||
self.task_id = task_id
|
||||
self.name = name
|
||||
self.task_type = task_type
|
||||
self.parent_id = parent_id
|
||||
self.icon_name = icon_name or "fa.male"
|
||||
self.icon_color = icon_color or get_default_entity_icon_color()
|
||||
self._label = None
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Alias for task_id.
|
||||
|
||||
Returns:
|
||||
str: Task id.
|
||||
"""
|
||||
|
||||
return self.task_id
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
"""Label of task item for UI.
|
||||
|
||||
Returns:
|
||||
str: Label of task item.
|
||||
"""
|
||||
|
||||
if self._label is None:
|
||||
self._label = "{} ({})".format(self.name, self.task_type)
|
||||
return self._label
|
||||
|
||||
def to_data(self):
|
||||
"""Converts task item to data.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Task item data.
|
||||
"""
|
||||
|
||||
return {
|
||||
"task_id": self.task_id,
|
||||
"name": self.name,
|
||||
"parent_id": self.parent_id,
|
||||
"task_type": self.task_type,
|
||||
"icon_name": self.icon_name,
|
||||
"icon_color": self.icon_color,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
"""Re-create task item from data.
|
||||
|
||||
Args:
|
||||
data (dict[str, Any]): Task item data.
|
||||
|
||||
Returns:
|
||||
TaskItem: Task item.
|
||||
"""
|
||||
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class FileItem:
|
||||
"""File item that represents a file.
|
||||
|
||||
Can be used for both Workarea and Published workfile. Workarea file
|
||||
will always exist on disk which is not the case for Published workfile.
|
||||
|
||||
Args:
|
||||
dirpath (str): Directory path of file.
|
||||
filename (str): Filename.
|
||||
modified (float): Modified timestamp.
|
||||
representation_id (Optional[str]): Representation id of published
|
||||
workfile.
|
||||
filepath (Optional[str]): Prepared filepath.
|
||||
exists (Optional[bool]): If file exists on disk.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dirpath,
|
||||
filename,
|
||||
modified,
|
||||
representation_id=None,
|
||||
filepath=None,
|
||||
exists=None
|
||||
):
|
||||
self.filename = filename
|
||||
self.dirpath = dirpath
|
||||
self.modified = modified
|
||||
self.representation_id = representation_id
|
||||
self._filepath = filepath
|
||||
self._exists = exists
|
||||
|
||||
@property
|
||||
def filepath(self):
|
||||
"""Filepath of file.
|
||||
|
||||
Returns:
|
||||
str: Full path to a file.
|
||||
"""
|
||||
|
||||
if self._filepath is None:
|
||||
self._filepath = os.path.join(self.dirpath, self.filename)
|
||||
return self._filepath
|
||||
|
||||
@property
|
||||
def exists(self):
|
||||
"""File is available.
|
||||
|
||||
Returns:
|
||||
bool: If file exists on disk.
|
||||
"""
|
||||
|
||||
if self._exists is None:
|
||||
self._exists = os.path.exists(self.filepath)
|
||||
return self._exists
|
||||
|
||||
def to_data(self):
|
||||
"""Converts file item to data.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: File item data.
|
||||
"""
|
||||
|
||||
return {
|
||||
"filename": self.filename,
|
||||
"dirpath": self.dirpath,
|
||||
"modified": self.modified,
|
||||
"representation_id": self.representation_id,
|
||||
"filepath": self.filepath,
|
||||
"exists": self.exists,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
"""Re-creates file item from data.
|
||||
|
||||
Args:
|
||||
data (dict[str, Any]): File item data.
|
||||
|
||||
Returns:
|
||||
FileItem: File item.
|
||||
"""
|
||||
|
||||
required_keys = {
|
||||
"filename",
|
||||
"dirpath",
|
||||
"modified",
|
||||
"representation_id"
|
||||
}
|
||||
missing_keys = required_keys - set(data.keys())
|
||||
if missing_keys:
|
||||
raise KeyError("Missing keys: {}".format(missing_keys))
|
||||
|
||||
return cls(**{
|
||||
key: data[key]
|
||||
for key in required_keys
|
||||
})
|
||||
|
||||
|
||||
class WorkareaFilepathResult:
|
||||
"""Result of workarea file formatting.
|
||||
|
||||
Args:
|
||||
root (str): Root path of workarea.
|
||||
filename (str): Filename.
|
||||
exists (bool): True if file exists.
|
||||
filepath (str): Filepath. If not provided it will be constructed
|
||||
from root and filename.
|
||||
"""
|
||||
|
||||
def __init__(self, root, filename, exists, filepath=None):
|
||||
if not filepath and root and filename:
|
||||
filepath = os.path.join(root, filename)
|
||||
self.root = root
|
||||
self.filename = filename
|
||||
self.exists = exists
|
||||
self.filepath = filepath
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class AbstractWorkfilesCommon(object):
|
||||
@abstractmethod
|
||||
def is_host_valid(self):
|
||||
"""Host is valid for workfiles tool work.
|
||||
|
||||
Returns:
|
||||
bool: True if host is valid.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_workfile_extensions(self):
|
||||
"""Get possible workfile extensions.
|
||||
|
||||
Defined by host implementation.
|
||||
|
||||
Returns:
|
||||
Iterable[str]: List of extensions.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_save_enabled(self):
|
||||
"""Is workfile save enabled.
|
||||
|
||||
Returns:
|
||||
bool: True if save is enabled.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_save_enabled(self, enabled):
|
||||
"""Enable or disabled workfile save.
|
||||
|
||||
Args:
|
||||
enabled (bool): Enable save workfile when True.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AbstractWorkfilesBackend(AbstractWorkfilesCommon):
|
||||
# Current context
|
||||
@abstractmethod
|
||||
def get_host_name(self):
|
||||
"""Name of host.
|
||||
|
||||
Returns:
|
||||
str: Name of host.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_project_name(self):
|
||||
"""Project name from current context of host.
|
||||
|
||||
Returns:
|
||||
str: Name of project.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_folder_id(self):
|
||||
"""Folder id from current context of host.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Folder id or None if host does not have
|
||||
any context.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_task_name(self):
|
||||
"""Task name from current context of host.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Task name or None if host does not have
|
||||
any context.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_workfile(self):
|
||||
"""Current workfile from current context of host.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Path to workfile or None if host does
|
||||
not have opened specific file.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def project_anatomy(self):
|
||||
"""Project anatomy for current project.
|
||||
|
||||
Returns:
|
||||
Anatomy: Project anatomy.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def project_settings(self):
|
||||
"""Project settings for current project.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Project settings.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder_entity(self, folder_id):
|
||||
"""Get folder entity by id.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Folder entity data.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_entity(self, task_id):
|
||||
"""Get task entity by id.
|
||||
|
||||
Args:
|
||||
task_id (str): Task id.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Task entity data.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def emit_event(self, topic, data=None, source=None):
|
||||
"""Emit event.
|
||||
|
||||
Args:
|
||||
topic (str): Event topic used for callbacks filtering.
|
||||
data (Optional[dict[str, Any]]): Event data.
|
||||
source (Optional[str]): Event source.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
||||
"""UI controller abstraction that is used for workfiles tool frontend.
|
||||
|
||||
Abstraction to provide data for UI and to handle UI events.
|
||||
|
||||
Provide access to abstract backend data, like folders and tasks. Cares
|
||||
about handling of selection, keep information about current UI selection
|
||||
and have ability to tell what selection should UI show.
|
||||
|
||||
Selection is separated into 2 parts, first is what UI elements tell
|
||||
about selection, and second is what UI should show as selected.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def register_event_callback(self, topic, callback):
|
||||
"""Register event callback.
|
||||
|
||||
Listen for events with given topic.
|
||||
|
||||
Args:
|
||||
topic (str): Name of topic.
|
||||
callback (Callable): Callback that will be called when event
|
||||
is triggered.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
# Host information
|
||||
@abstractmethod
|
||||
def get_workfile_extensions(self):
|
||||
"""Each host can define extensions that can be used for workfile.
|
||||
|
||||
Returns:
|
||||
List[str]: File extensions that can be used as workfile for
|
||||
current host.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
# Selection information
|
||||
@abstractmethod
|
||||
def get_selected_folder_id(self):
|
||||
"""Currently selected folder id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Folder id or None if no folder is selected.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_folder(self, folder_id):
|
||||
"""Change selected folder.
|
||||
|
||||
This deselects currently selected task.
|
||||
|
||||
Args:
|
||||
folder_id (Union[str, None]): Folder id or None if no folder
|
||||
is selected.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_task_id(self):
|
||||
"""Currently selected task id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Task id or None if no folder is selected.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_task_name(self):
|
||||
"""Currently selected task name.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Task name or None if no folder is selected.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_task(self, folder_id, task_id, task_name):
|
||||
"""Change selected task.
|
||||
|
||||
Args:
|
||||
folder_id (Union[str, None]): Folder id or None if no folder
|
||||
is selected.
|
||||
task_id (Union[str, None]): Task id or None if no task
|
||||
is selected.
|
||||
task_name (Union[str, None]): Task name or None if no task
|
||||
is selected.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_workfile_path(self):
|
||||
"""Currently selected workarea workile.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Selected workfile path.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_workfile_path(self, path):
|
||||
"""Change selected workfile path.
|
||||
|
||||
Args:
|
||||
path (Union[str, None]): Selected workfile path.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_representation_id(self):
|
||||
"""Currently selected workfile representation id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Representation id or None if no representation
|
||||
is selected.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_representation_id(self, representation_id):
|
||||
"""Change selected representation.
|
||||
|
||||
Args:
|
||||
representation_id (Union[str, None]): Selected workfile
|
||||
representation id.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def get_selected_context(self):
|
||||
"""Obtain selected context.
|
||||
|
||||
Returns:
|
||||
dict[str, Union[str, None]]: Selected context.
|
||||
"""
|
||||
|
||||
return {
|
||||
"folder_id": self.get_selected_folder_id(),
|
||||
"task_id": self.get_selected_task_id(),
|
||||
"task_name": self.get_selected_task_name(),
|
||||
"workfile_path": self.get_selected_workfile_path(),
|
||||
"representation_id": self.get_selected_representation_id(),
|
||||
}
|
||||
|
||||
# Expected selection
|
||||
# - expected selection is used to restore selection after refresh
|
||||
# or when current context should be used
|
||||
@abstractmethod
|
||||
def set_expected_selection(
|
||||
self,
|
||||
folder_id,
|
||||
task_name,
|
||||
workfile_name=None,
|
||||
representation_id=None
|
||||
):
|
||||
"""Define what should be selected in UI.
|
||||
|
||||
Expected selection provide a way to define/change selection of
|
||||
sequential UI elements. For example, if folder and task should be
|
||||
selected a task element should wait until folder element has selected
|
||||
folder.
|
||||
|
||||
Triggers 'expected_selection.changed' event.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_name (str): Task name.
|
||||
workfile_name (Optional[str]): Workfile name. Used for workarea
|
||||
files UI element.
|
||||
representation_id (Optional[str]): Representation id. Used for
|
||||
published filed UI element.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_expected_selection_data(self):
|
||||
"""Data of expected selection.
|
||||
|
||||
TODOs:
|
||||
Return defined object instead of dict.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Expected selection data.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def expected_folder_selected(self, folder_id):
|
||||
"""Expected folder was selected in UI.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id which was selected.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def expected_task_selected(self, folder_id, task_name):
|
||||
"""Expected task was selected in UI.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id under which task is.
|
||||
task_name (str): Task name which was selected.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def expected_representation_selected(self, representation_id):
|
||||
"""Expected representation was selected in UI.
|
||||
|
||||
Args:
|
||||
representation_id (str): Representation id which was selected.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def expected_workfile_selected(self, workfile_path):
|
||||
"""Expected workfile was selected in UI.
|
||||
|
||||
Args:
|
||||
workfile_path (str): Workfile path which was selected.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def go_to_current_context(self):
|
||||
"""Set expected selection to current context."""
|
||||
|
||||
pass
|
||||
|
||||
# Model functions
|
||||
@abstractmethod
|
||||
def get_folder_items(self, sender):
|
||||
"""Folder items to visualize project hierarchy.
|
||||
|
||||
This function may trigger events 'folders.refresh.started' and
|
||||
'folders.refresh.finished' which will contain 'sender' value in data.
|
||||
That may help to avoid re-refresh of folder items in UI elements.
|
||||
|
||||
Args:
|
||||
sender (str): Who requested folder items.
|
||||
|
||||
Returns:
|
||||
list[FolderItem]: Minimum possible information needed
|
||||
for visualisation of folder hierarchy.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_items(self, folder_id, sender):
|
||||
"""Task items.
|
||||
|
||||
This function may trigger events 'tasks.refresh.started' and
|
||||
'tasks.refresh.finished' which will contain 'sender' value in data.
|
||||
That may help to avoid re-refresh of task items in UI elements.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder ID for which are tasks requested.
|
||||
sender (str): Who requested folder items.
|
||||
|
||||
Returns:
|
||||
list[TaskItem]: Minimum possible information needed
|
||||
for visualisation of tasks.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def has_unsaved_changes(self):
|
||||
"""Has host unsaved change in currently running session.
|
||||
|
||||
Returns:
|
||||
bool: Has unsaved changes.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_workarea_dir_by_context(self, folder_id, task_id):
|
||||
"""Get workarea directory by context.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
|
||||
Returns:
|
||||
str: Workarea directory.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_workarea_file_items(self, folder_id, task_id):
|
||||
"""Get workarea file items.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
|
||||
Returns:
|
||||
list[FileItem]: List of workarea file items.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_workarea_save_as_data(self, folder_id, task_id):
|
||||
"""Prepare data for Save As operation.
|
||||
|
||||
Todos:
|
||||
Return defined object instead of dict.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Data for Save As operation.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def fill_workarea_filepath(
|
||||
self,
|
||||
folder_id,
|
||||
task_id,
|
||||
extension,
|
||||
use_last_version,
|
||||
version,
|
||||
comment,
|
||||
):
|
||||
"""Calculate workfile path for passed context.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
extension (str): File extension.
|
||||
use_last_version (bool): Use last version.
|
||||
version (int): Version used if 'use_last_version' if 'False'.
|
||||
comment (str): User's comment (subversion).
|
||||
|
||||
Returns:
|
||||
WorkareaFilepathResult: Result of the operation.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_published_file_items(self, folder_id, task_id):
|
||||
"""Get published file items.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (Union[str, None]): Task id.
|
||||
|
||||
Returns:
|
||||
list[FileItem]: List of published file items.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_workfile_info(self, folder_id, task_id, filepath):
|
||||
"""Workfile info from database.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
filepath (str): Workfile path.
|
||||
|
||||
Returns:
|
||||
Union[WorkfileInfo, None]: Workfile info or None if was passed
|
||||
invalid context.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_workfile_info(self, folder_id, task_id, filepath, note):
|
||||
"""Save workfile info to database.
|
||||
|
||||
At this moment the only information which can be saved about
|
||||
workfile is 'note'.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
filepath (str): Workfile path.
|
||||
note (str): Note.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
# General commands
|
||||
@abstractmethod
|
||||
def refresh(self):
|
||||
"""Refresh everything, models, ui etc.
|
||||
|
||||
Triggers 'controller.refresh.started' event at the beginning and
|
||||
'controller.refresh.finished' at the end.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
# Controller actions
|
||||
@abstractmethod
|
||||
def open_workfile(self, filepath):
|
||||
"""Open a workfile.
|
||||
|
||||
Args:
|
||||
filepath (str): Workfile path.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_current_workfile(self):
|
||||
"""Save state of current workfile."""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_as_workfile(
|
||||
self,
|
||||
folder_id,
|
||||
task_id,
|
||||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
):
|
||||
"""Save current state of workfile to workarea.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
workdir (str): Workarea directory.
|
||||
filename (str): Workarea filename.
|
||||
template_key (str): Template key used to get the workdir
|
||||
and filename.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def copy_workfile_representation(
|
||||
self,
|
||||
representation_id,
|
||||
representation_filepath,
|
||||
folder_id,
|
||||
task_id,
|
||||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
):
|
||||
"""Action to copy published workfile representation to workarea.
|
||||
|
||||
Triggers 'copy_representation.started' event on start and
|
||||
'copy_representation.finished' event with '{"failed": bool}'.
|
||||
|
||||
Args:
|
||||
representation_id (str): Representation id.
|
||||
representation_filepath (str): Path to representation file.
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
workdir (str): Workarea directory.
|
||||
filename (str): Workarea filename.
|
||||
template_key (str): Template key.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def duplicate_workfile(self, src_filepath, workdir, filename):
|
||||
"""Duplicate workfile.
|
||||
|
||||
Workfiles is not opened when done.
|
||||
|
||||
Args:
|
||||
src_filepath (str): Source workfile path.
|
||||
workdir (str): Destination workdir.
|
||||
filename (str): Destination filename.
|
||||
"""
|
||||
|
||||
pass
|
||||
642
openpype/tools/ayon_workfiles/control.py
Normal file
642
openpype/tools/ayon_workfiles/control.py
Normal file
|
|
@ -0,0 +1,642 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
import ayon_api
|
||||
|
||||
from openpype.client import get_asset_by_id
|
||||
from openpype.host import IWorkfileHost
|
||||
from openpype.lib import Logger, emit_event
|
||||
from openpype.lib.events import QueuedEventSystem
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import Anatomy, registered_host
|
||||
from openpype.pipeline.context_tools import (
|
||||
change_current_context,
|
||||
get_current_host_name,
|
||||
get_global_context,
|
||||
)
|
||||
from openpype.pipeline.workfile import create_workdir_extra_folders
|
||||
|
||||
from .abstract import (
|
||||
AbstractWorkfilesFrontend,
|
||||
AbstractWorkfilesBackend,
|
||||
)
|
||||
from .models import SelectionModel, EntitiesModel, WorkfilesModel
|
||||
|
||||
|
||||
class ExpectedSelection:
|
||||
def __init__(self):
|
||||
self._folder_id = None
|
||||
self._task_name = None
|
||||
self._workfile_name = None
|
||||
self._representation_id = None
|
||||
self._folder_selected = True
|
||||
self._task_selected = True
|
||||
self._workfile_name_selected = True
|
||||
self._representation_id_selected = True
|
||||
|
||||
def set_expected_selection(
|
||||
self,
|
||||
folder_id,
|
||||
task_name,
|
||||
workfile_name=None,
|
||||
representation_id=None
|
||||
):
|
||||
self._folder_id = folder_id
|
||||
self._task_name = task_name
|
||||
self._workfile_name = workfile_name
|
||||
self._representation_id = representation_id
|
||||
self._folder_selected = False
|
||||
self._task_selected = False
|
||||
self._workfile_name_selected = workfile_name is None
|
||||
self._representation_id_selected = representation_id is None
|
||||
|
||||
def get_expected_selection_data(self):
|
||||
return {
|
||||
"folder_id": self._folder_id,
|
||||
"task_name": self._task_name,
|
||||
"workfile_name": self._workfile_name,
|
||||
"representation_id": self._representation_id,
|
||||
"folder_selected": self._folder_selected,
|
||||
"task_selected": self._task_selected,
|
||||
"workfile_name_selected": self._workfile_name_selected,
|
||||
"representation_id_selected": self._representation_id_selected,
|
||||
}
|
||||
|
||||
def is_expected_folder_selected(self, folder_id):
|
||||
return folder_id == self._folder_id and self._folder_selected
|
||||
|
||||
def is_expected_task_selected(self, folder_id, task_name):
|
||||
if not self.is_expected_folder_selected(folder_id):
|
||||
return False
|
||||
return task_name == self._task_name and self._task_selected
|
||||
|
||||
def expected_folder_selected(self, folder_id):
|
||||
if folder_id != self._folder_id:
|
||||
return False
|
||||
self._folder_selected = True
|
||||
return True
|
||||
|
||||
def expected_task_selected(self, folder_id, task_name):
|
||||
if not self.is_expected_folder_selected(folder_id):
|
||||
return False
|
||||
|
||||
if task_name != self._task_name:
|
||||
return False
|
||||
|
||||
self._task_selected = True
|
||||
return True
|
||||
|
||||
def expected_workfile_selected(self, folder_id, task_name, workfile_name):
|
||||
if not self.is_expected_task_selected(folder_id, task_name):
|
||||
return False
|
||||
|
||||
if workfile_name != self._workfile_name:
|
||||
return False
|
||||
self._workfile_name_selected = True
|
||||
return True
|
||||
|
||||
def expected_representation_selected(
|
||||
self, folder_id, task_name, representation_id
|
||||
):
|
||||
if not self.is_expected_task_selected(folder_id, task_name):
|
||||
return False
|
||||
if representation_id != self._representation_id:
|
||||
return False
|
||||
self._representation_id_selected = True
|
||||
return True
|
||||
|
||||
|
||||
class BaseWorkfileController(
|
||||
AbstractWorkfilesFrontend, AbstractWorkfilesBackend
|
||||
):
|
||||
def __init__(self, host=None):
|
||||
if host is None:
|
||||
host = registered_host()
|
||||
|
||||
host_is_valid = False
|
||||
if host is not None:
|
||||
missing_methods = (
|
||||
IWorkfileHost.get_missing_workfile_methods(host)
|
||||
)
|
||||
host_is_valid = len(missing_methods) == 0
|
||||
|
||||
self._host = host
|
||||
self._host_is_valid = host_is_valid
|
||||
|
||||
self._project_anatomy = None
|
||||
self._project_settings = None
|
||||
self._event_system = None
|
||||
self._log = None
|
||||
|
||||
self._current_project_name = None
|
||||
self._current_folder_name = None
|
||||
self._current_folder_id = None
|
||||
self._current_task_name = None
|
||||
self._save_is_enabled = True
|
||||
|
||||
# Expected selected folder and task
|
||||
self._expected_selection = self._create_expected_selection_obj()
|
||||
|
||||
self._selection_model = self._create_selection_model()
|
||||
self._entities_model = self._create_entities_model()
|
||||
self._workfiles_model = self._create_workfiles_model()
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = Logger.get_logger("WorkfilesUI")
|
||||
return self._log
|
||||
|
||||
def is_host_valid(self):
|
||||
return self._host_is_valid
|
||||
|
||||
def _create_expected_selection_obj(self):
|
||||
return ExpectedSelection()
|
||||
|
||||
def _create_selection_model(self):
|
||||
return SelectionModel(self)
|
||||
|
||||
def _create_entities_model(self):
|
||||
return EntitiesModel(self)
|
||||
|
||||
def _create_workfiles_model(self):
|
||||
return WorkfilesModel(self)
|
||||
|
||||
@property
|
||||
def event_system(self):
|
||||
"""Inner event system for workfiles tool controller.
|
||||
|
||||
Is used for communication with UI. Event system is created on demand.
|
||||
|
||||
Returns:
|
||||
QueuedEventSystem: Event system which can trigger callbacks
|
||||
for topics.
|
||||
"""
|
||||
|
||||
if self._event_system is None:
|
||||
self._event_system = QueuedEventSystem()
|
||||
return self._event_system
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Implementation of methods required for backend logic
|
||||
# ----------------------------------------------------
|
||||
@property
|
||||
def project_settings(self):
|
||||
if self._project_settings is None:
|
||||
self._project_settings = get_project_settings(
|
||||
self.get_current_project_name())
|
||||
return self._project_settings
|
||||
|
||||
@property
|
||||
def project_anatomy(self):
|
||||
if self._project_anatomy is None:
|
||||
self._project_anatomy = Anatomy(self.get_current_project_name())
|
||||
return self._project_anatomy
|
||||
|
||||
def get_folder_entity(self, folder_id):
|
||||
return self._entities_model.get_folder_entity(folder_id)
|
||||
|
||||
def get_task_entity(self, task_id):
|
||||
return self._entities_model.get_task_entity(task_id)
|
||||
|
||||
# ---------------------------------
|
||||
# Implementation of abstract methods
|
||||
# ---------------------------------
|
||||
def emit_event(self, topic, data=None, source=None):
|
||||
"""Use implemented event system to trigger event."""
|
||||
|
||||
if data is None:
|
||||
data = {}
|
||||
self.event_system.emit(topic, data, source)
|
||||
|
||||
def register_event_callback(self, topic, callback):
|
||||
self.event_system.add_callback(topic, callback)
|
||||
|
||||
def is_save_enabled(self):
|
||||
"""Is workfile save enabled.
|
||||
|
||||
Returns:
|
||||
bool: True if save is enabled.
|
||||
"""
|
||||
|
||||
return self._save_is_enabled
|
||||
|
||||
def set_save_enabled(self, enabled):
|
||||
"""Enable or disabled workfile save.
|
||||
|
||||
Args:
|
||||
enabled (bool): Enable save workfile when True.
|
||||
"""
|
||||
|
||||
if self._save_is_enabled == enabled:
|
||||
return
|
||||
|
||||
self._save_is_enabled = enabled
|
||||
self._emit_event(
|
||||
"workfile_save_enable.changed",
|
||||
{"enabled": enabled}
|
||||
)
|
||||
|
||||
# Host information
|
||||
def get_workfile_extensions(self):
|
||||
host = self._host
|
||||
if isinstance(host, IWorkfileHost):
|
||||
return host.get_workfile_extensions()
|
||||
return host.file_extensions()
|
||||
|
||||
def has_unsaved_changes(self):
|
||||
host = self._host
|
||||
if isinstance(host, IWorkfileHost):
|
||||
return host.workfile_has_unsaved_changes()
|
||||
return host.has_unsaved_changes()
|
||||
|
||||
# Current context
|
||||
def get_host_name(self):
|
||||
host = self._host
|
||||
if isinstance(host, IWorkfileHost):
|
||||
return host.name
|
||||
return get_current_host_name()
|
||||
|
||||
def _get_host_current_context(self):
|
||||
if hasattr(self._host, "get_current_context"):
|
||||
return self._host.get_current_context()
|
||||
return get_global_context()
|
||||
|
||||
def get_current_project_name(self):
|
||||
return self._current_project_name
|
||||
|
||||
def get_current_folder_id(self):
|
||||
return self._current_folder_id
|
||||
|
||||
def get_current_task_name(self):
|
||||
return self._current_task_name
|
||||
|
||||
def get_current_workfile(self):
|
||||
host = self._host
|
||||
if isinstance(host, IWorkfileHost):
|
||||
return host.get_current_workfile()
|
||||
return host.current_file()
|
||||
|
||||
# Selection information
|
||||
def get_selected_folder_id(self):
|
||||
return self._selection_model.get_selected_folder_id()
|
||||
|
||||
def set_selected_folder(self, folder_id):
|
||||
self._selection_model.set_selected_folder(folder_id)
|
||||
|
||||
def get_selected_task_id(self):
|
||||
return self._selection_model.get_selected_task_id()
|
||||
|
||||
def get_selected_task_name(self):
|
||||
return self._selection_model.get_selected_task_name()
|
||||
|
||||
def set_selected_task(self, folder_id, task_id, task_name):
|
||||
return self._selection_model.set_selected_task(
|
||||
folder_id, task_id, task_name)
|
||||
|
||||
def get_selected_workfile_path(self):
|
||||
return self._selection_model.get_selected_workfile_path()
|
||||
|
||||
def set_selected_workfile_path(self, path):
|
||||
self._selection_model.set_selected_workfile_path(path)
|
||||
|
||||
def get_selected_representation_id(self):
|
||||
return self._selection_model.get_selected_representation_id()
|
||||
|
||||
def set_selected_representation_id(self, representation_id):
|
||||
self._selection_model.set_selected_representation_id(
|
||||
representation_id)
|
||||
|
||||
def set_expected_selection(
|
||||
self,
|
||||
folder_id,
|
||||
task_name,
|
||||
workfile_name=None,
|
||||
representation_id=None
|
||||
):
|
||||
self._expected_selection.set_expected_selection(
|
||||
folder_id, task_name, workfile_name, representation_id
|
||||
)
|
||||
self._trigger_expected_selection_changed()
|
||||
|
||||
def expected_folder_selected(self, folder_id):
|
||||
if self._expected_selection.expected_folder_selected(folder_id):
|
||||
self._trigger_expected_selection_changed()
|
||||
|
||||
def expected_task_selected(self, folder_id, task_name):
|
||||
if self._expected_selection.expected_task_selected(
|
||||
folder_id, task_name
|
||||
):
|
||||
self._trigger_expected_selection_changed()
|
||||
|
||||
def expected_workfile_selected(self, folder_id, task_name, workfile_name):
|
||||
if self._expected_selection.expected_workfile_selected(
|
||||
folder_id, task_name, workfile_name
|
||||
):
|
||||
self._trigger_expected_selection_changed()
|
||||
|
||||
def expected_representation_selected(
|
||||
self, folder_id, task_name, representation_id
|
||||
):
|
||||
if self._expected_selection.expected_representation_selected(
|
||||
folder_id, task_name, representation_id
|
||||
):
|
||||
self._trigger_expected_selection_changed()
|
||||
|
||||
def get_expected_selection_data(self):
|
||||
return self._expected_selection.get_expected_selection_data()
|
||||
|
||||
def go_to_current_context(self):
|
||||
self.set_expected_selection(
|
||||
self._current_folder_id, self._current_task_name
|
||||
)
|
||||
|
||||
# Model functions
|
||||
def get_folder_items(self, sender):
|
||||
return self._entities_model.get_folder_items(sender)
|
||||
|
||||
def get_task_items(self, folder_id, sender):
|
||||
return self._entities_model.get_tasks_items(folder_id, sender)
|
||||
|
||||
def get_workarea_dir_by_context(self, folder_id, task_id):
|
||||
return self._workfiles_model.get_workarea_dir_by_context(
|
||||
folder_id, task_id)
|
||||
|
||||
def get_workarea_file_items(self, folder_id, task_id):
|
||||
return self._workfiles_model.get_workarea_file_items(
|
||||
folder_id, task_id)
|
||||
|
||||
def get_workarea_save_as_data(self, folder_id, task_id):
|
||||
return self._workfiles_model.get_workarea_save_as_data(
|
||||
folder_id, task_id)
|
||||
|
||||
def fill_workarea_filepath(
|
||||
self,
|
||||
folder_id,
|
||||
task_id,
|
||||
extension,
|
||||
use_last_version,
|
||||
version,
|
||||
comment,
|
||||
):
|
||||
return self._workfiles_model.fill_workarea_filepath(
|
||||
folder_id,
|
||||
task_id,
|
||||
extension,
|
||||
use_last_version,
|
||||
version,
|
||||
comment,
|
||||
)
|
||||
|
||||
def get_published_file_items(self, folder_id, task_id):
|
||||
task_name = None
|
||||
if task_id:
|
||||
task = self.get_task_entity(task_id)
|
||||
task_name = task.get("name")
|
||||
|
||||
return self._workfiles_model.get_published_file_items(
|
||||
folder_id, task_name)
|
||||
|
||||
def get_workfile_info(self, folder_id, task_id, filepath):
|
||||
return self._workfiles_model.get_workfile_info(
|
||||
folder_id, task_id, filepath
|
||||
)
|
||||
|
||||
def save_workfile_info(self, folder_id, task_id, filepath, note):
|
||||
self._workfiles_model.save_workfile_info(
|
||||
folder_id, task_id, filepath, note
|
||||
)
|
||||
|
||||
def refresh(self):
|
||||
if not self._host_is_valid:
|
||||
self._emit_event("controller.refresh.started")
|
||||
self._emit_event("controller.refresh.finished")
|
||||
return
|
||||
expected_folder_id = self.get_selected_folder_id()
|
||||
expected_task_name = self.get_selected_task_name()
|
||||
|
||||
self._emit_event("controller.refresh.started")
|
||||
|
||||
context = self._get_host_current_context()
|
||||
|
||||
project_name = context["project_name"]
|
||||
folder_name = context["asset_name"]
|
||||
task_name = context["task_name"]
|
||||
folder_id = None
|
||||
if folder_name:
|
||||
folder = ayon_api.get_folder_by_name(project_name, folder_name)
|
||||
if folder:
|
||||
folder_id = folder["id"]
|
||||
|
||||
self._project_settings = None
|
||||
self._project_anatomy = None
|
||||
|
||||
self._current_project_name = project_name
|
||||
self._current_folder_name = folder_name
|
||||
self._current_folder_id = folder_id
|
||||
self._current_task_name = task_name
|
||||
|
||||
if not expected_folder_id:
|
||||
expected_folder_id = folder_id
|
||||
expected_task_name = task_name
|
||||
|
||||
self._expected_selection.set_expected_selection(
|
||||
expected_folder_id, expected_task_name
|
||||
)
|
||||
|
||||
self._entities_model.refresh()
|
||||
|
||||
self._emit_event("controller.refresh.finished")
|
||||
|
||||
# Controller actions
|
||||
def open_workfile(self, filepath):
|
||||
self._emit_event("open_workfile.started")
|
||||
|
||||
failed = False
|
||||
try:
|
||||
self._host_open_workfile(filepath)
|
||||
|
||||
except Exception:
|
||||
failed = True
|
||||
self.log.warning("Open of workfile failed", exc_info=True)
|
||||
|
||||
self._emit_event(
|
||||
"open_workfile.finished",
|
||||
{"failed": failed},
|
||||
)
|
||||
|
||||
def save_current_workfile(self):
|
||||
current_file = self.get_current_workfile()
|
||||
self._host_save_workfile(current_file)
|
||||
|
||||
def save_as_workfile(
|
||||
self,
|
||||
folder_id,
|
||||
task_id,
|
||||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
):
|
||||
self._emit_event("save_as.started")
|
||||
|
||||
failed = False
|
||||
try:
|
||||
self._save_as_workfile(
|
||||
folder_id,
|
||||
task_id,
|
||||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
)
|
||||
except Exception:
|
||||
failed = True
|
||||
self.log.warning("Save as failed", exc_info=True)
|
||||
|
||||
self._emit_event(
|
||||
"save_as.finished",
|
||||
{"failed": failed},
|
||||
)
|
||||
|
||||
def copy_workfile_representation(
|
||||
self,
|
||||
representation_id,
|
||||
representation_filepath,
|
||||
folder_id,
|
||||
task_id,
|
||||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
):
|
||||
self._emit_event("copy_representation.started")
|
||||
|
||||
failed = False
|
||||
try:
|
||||
self._save_as_workfile(
|
||||
folder_id,
|
||||
task_id,
|
||||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
)
|
||||
except Exception:
|
||||
failed = True
|
||||
self.log.warning(
|
||||
"Copy of workfile representation failed", exc_info=True
|
||||
)
|
||||
|
||||
self._emit_event(
|
||||
"copy_representation.finished",
|
||||
{"failed": failed},
|
||||
)
|
||||
|
||||
def duplicate_workfile(self, src_filepath, workdir, filename):
|
||||
self._emit_event("workfile_duplicate.started")
|
||||
|
||||
failed = False
|
||||
try:
|
||||
dst_filepath = os.path.join(workdir, filename)
|
||||
shutil.copy(src_filepath, dst_filepath)
|
||||
except Exception:
|
||||
failed = True
|
||||
self.log.warning("Duplication of workfile failed", exc_info=True)
|
||||
|
||||
self._emit_event(
|
||||
"workfile_duplicate.finished",
|
||||
{"failed": failed},
|
||||
)
|
||||
|
||||
# Helper host methods that resolve 'IWorkfileHost' interface
|
||||
def _host_open_workfile(self, filepath):
|
||||
host = self._host
|
||||
if isinstance(host, IWorkfileHost):
|
||||
host.open_workfile(filepath)
|
||||
else:
|
||||
host.open_file(filepath)
|
||||
|
||||
def _host_save_workfile(self, filepath):
|
||||
host = self._host
|
||||
if isinstance(host, IWorkfileHost):
|
||||
host.save_workfile(filepath)
|
||||
else:
|
||||
host.save_file(filepath)
|
||||
|
||||
def _emit_event(self, topic, data=None):
|
||||
self.emit_event(topic, data, "controller")
|
||||
|
||||
# Expected selection
|
||||
# - expected selection is used to restore selection after refresh
|
||||
# or when current context should be used
|
||||
def _trigger_expected_selection_changed(self):
|
||||
self._emit_event(
|
||||
"expected_selection_changed",
|
||||
self._expected_selection.get_expected_selection_data(),
|
||||
)
|
||||
|
||||
def _save_as_workfile(
|
||||
self,
|
||||
folder_id,
|
||||
task_id,
|
||||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
src_filepath=None,
|
||||
):
|
||||
# Trigger before save event
|
||||
project_name = self.get_current_project_name()
|
||||
folder = self.get_folder_entity(folder_id)
|
||||
task = self.get_task_entity(task_id)
|
||||
task_name = task["name"]
|
||||
|
||||
# QUESTION should the data be different for 'before' and 'after'?
|
||||
# NOTE keys should be OpenPype compatible
|
||||
event_data = {
|
||||
"project_name": project_name,
|
||||
"folder_id": folder_id,
|
||||
"asset_id": folder_id,
|
||||
"asset_name": folder["name"],
|
||||
"task_id": task_id,
|
||||
"task_name": task_name,
|
||||
"host_name": self.get_host_name(),
|
||||
"filename": filename,
|
||||
"workdir_path": workdir,
|
||||
}
|
||||
emit_event("workfile.save.before", event_data, source="workfiles.tool")
|
||||
|
||||
# Create workfiles root folder
|
||||
if not os.path.exists(workdir):
|
||||
self.log.debug("Initializing work directory: %s", workdir)
|
||||
os.makedirs(workdir)
|
||||
|
||||
# Change context
|
||||
if (
|
||||
folder_id != self.get_current_folder_id()
|
||||
or task_name != self.get_current_task_name()
|
||||
):
|
||||
# Use OpenPype asset-like object
|
||||
asset_doc = get_asset_by_id(project_name, folder["id"])
|
||||
change_current_context(
|
||||
asset_doc,
|
||||
task["name"],
|
||||
template_key=template_key
|
||||
)
|
||||
|
||||
# Save workfile
|
||||
dst_filepath = os.path.join(workdir, filename)
|
||||
if src_filepath:
|
||||
shutil.copyfile(src_filepath, dst_filepath)
|
||||
self._host_open_workfile(dst_filepath)
|
||||
else:
|
||||
self._host_save_workfile(dst_filepath)
|
||||
|
||||
# Create extra folders
|
||||
create_workdir_extra_folders(
|
||||
workdir,
|
||||
self.get_host_name(),
|
||||
task["taskType"],
|
||||
task_name,
|
||||
project_name
|
||||
)
|
||||
|
||||
# Trigger after save events
|
||||
emit_event("workfile.save.after", event_data, source="workfiles.tool")
|
||||
self.refresh()
|
||||
10
openpype/tools/ayon_workfiles/models/__init__.py
Normal file
10
openpype/tools/ayon_workfiles/models/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from .hierarchy import EntitiesModel
|
||||
from .selection import SelectionModel
|
||||
from .workfiles import WorkfilesModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SelectionModel",
|
||||
"EntitiesModel",
|
||||
"WorkfilesModel",
|
||||
)
|
||||
225
openpype/tools/ayon_workfiles/models/hierarchy.py
Normal file
225
openpype/tools/ayon_workfiles/models/hierarchy.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
"""Hierarchy model that handles folders and tasks.
|
||||
|
||||
The model can be extracted for common usage. In that case it will be required
|
||||
to add more handling of project name changes.
|
||||
"""
|
||||
|
||||
import time
|
||||
import collections
|
||||
import contextlib
|
||||
|
||||
import ayon_api
|
||||
|
||||
from openpype.tools.ayon_workfiles.abstract import (
|
||||
FolderItem,
|
||||
TaskItem,
|
||||
)
|
||||
|
||||
|
||||
def _get_task_items_from_tasks(tasks):
|
||||
"""
|
||||
|
||||
Returns:
|
||||
TaskItem: Task item.
|
||||
"""
|
||||
|
||||
output = []
|
||||
for task in tasks:
|
||||
folder_id = task["folderId"]
|
||||
output.append(TaskItem(
|
||||
task["id"],
|
||||
task["name"],
|
||||
task["type"],
|
||||
folder_id,
|
||||
None,
|
||||
None
|
||||
))
|
||||
return output
|
||||
|
||||
|
||||
def _get_folder_item_from_hierarchy_item(item):
|
||||
return FolderItem(
|
||||
item["id"],
|
||||
item["parentId"],
|
||||
item["name"],
|
||||
item["label"],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
class CacheItem:
|
||||
def __init__(self, lifetime=120):
|
||||
self._lifetime = lifetime
|
||||
self._last_update = None
|
||||
self._data = None
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
if self._last_update is None:
|
||||
return False
|
||||
|
||||
return (time.time() - self._last_update) < self._lifetime
|
||||
|
||||
def set_invalid(self, data=None):
|
||||
self._last_update = None
|
||||
self._data = data
|
||||
|
||||
def get_data(self):
|
||||
return self._data
|
||||
|
||||
def update_data(self, data):
|
||||
self._data = data
|
||||
self._last_update = time.time()
|
||||
|
||||
|
||||
class EntitiesModel(object):
|
||||
event_source = "entities.model"
|
||||
|
||||
def __init__(self, controller):
|
||||
folders_cache = CacheItem()
|
||||
folders_cache.set_invalid({})
|
||||
self._folders_cache = folders_cache
|
||||
self._tasks_cache = {}
|
||||
|
||||
self._folders_by_id = {}
|
||||
self._tasks_by_id = {}
|
||||
|
||||
self._folders_refreshing = False
|
||||
self._tasks_refreshing = set()
|
||||
self._controller = controller
|
||||
|
||||
def reset(self):
|
||||
self._folders_cache.set_invalid({})
|
||||
self._tasks_cache = {}
|
||||
|
||||
self._folders_by_id = {}
|
||||
self._tasks_by_id = {}
|
||||
|
||||
def refresh(self):
|
||||
self._refresh_folders_cache()
|
||||
|
||||
def get_folder_items(self, sender):
|
||||
if not self._folders_cache.is_valid:
|
||||
self._refresh_folders_cache(sender)
|
||||
return self._folders_cache.get_data()
|
||||
|
||||
def get_tasks_items(self, folder_id, sender):
|
||||
if not folder_id:
|
||||
return []
|
||||
|
||||
task_cache = self._tasks_cache.get(folder_id)
|
||||
if task_cache is None or not task_cache.is_valid:
|
||||
self._refresh_tasks_cache(folder_id, sender)
|
||||
task_cache = self._tasks_cache.get(folder_id)
|
||||
return task_cache.get_data()
|
||||
|
||||
def get_folder_entity(self, folder_id):
|
||||
if folder_id not in self._folders_by_id:
|
||||
entity = None
|
||||
if folder_id:
|
||||
project_name = self._controller.get_current_project_name()
|
||||
entity = ayon_api.get_folder_by_id(project_name, folder_id)
|
||||
self._folders_by_id[folder_id] = entity
|
||||
return self._folders_by_id[folder_id]
|
||||
|
||||
def get_task_entity(self, task_id):
|
||||
if task_id not in self._tasks_by_id:
|
||||
entity = None
|
||||
if task_id:
|
||||
project_name = self._controller.get_current_project_name()
|
||||
entity = ayon_api.get_task_by_id(project_name, task_id)
|
||||
self._tasks_by_id[task_id] = entity
|
||||
return self._tasks_by_id[task_id]
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _folder_refresh_event_manager(self, project_name, sender):
|
||||
self._folders_refreshing = True
|
||||
self._controller.emit_event(
|
||||
"folders.refresh.started",
|
||||
{"project_name": project_name, "sender": sender},
|
||||
self.event_source
|
||||
)
|
||||
try:
|
||||
yield
|
||||
|
||||
finally:
|
||||
self._controller.emit_event(
|
||||
"folders.refresh.finished",
|
||||
{"project_name": project_name, "sender": sender},
|
||||
self.event_source
|
||||
)
|
||||
self._folders_refreshing = False
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _task_refresh_event_manager(
|
||||
self, project_name, folder_id, sender
|
||||
):
|
||||
self._tasks_refreshing.add(folder_id)
|
||||
self._controller.emit_event(
|
||||
"tasks.refresh.started",
|
||||
{
|
||||
"project_name": project_name,
|
||||
"folder_id": folder_id,
|
||||
"sender": sender,
|
||||
},
|
||||
self.event_source
|
||||
)
|
||||
try:
|
||||
yield
|
||||
|
||||
finally:
|
||||
self._controller.emit_event(
|
||||
"tasks.refresh.finished",
|
||||
{
|
||||
"project_name": project_name,
|
||||
"folder_id": folder_id,
|
||||
"sender": sender,
|
||||
},
|
||||
self.event_source
|
||||
)
|
||||
self._tasks_refreshing.discard(folder_id)
|
||||
|
||||
def _refresh_folders_cache(self, sender=None):
|
||||
if self._folders_refreshing:
|
||||
return
|
||||
project_name = self._controller.get_current_project_name()
|
||||
with self._folder_refresh_event_manager(project_name, sender):
|
||||
folder_items = self._query_folders(project_name)
|
||||
self._folders_cache.update_data(folder_items)
|
||||
|
||||
def _query_folders(self, project_name):
|
||||
hierarchy = ayon_api.get_folders_hierarchy(project_name)
|
||||
|
||||
folder_items = {}
|
||||
hierachy_queue = collections.deque(hierarchy["hierarchy"])
|
||||
while hierachy_queue:
|
||||
item = hierachy_queue.popleft()
|
||||
folder_item = _get_folder_item_from_hierarchy_item(item)
|
||||
folder_items[folder_item.entity_id] = folder_item
|
||||
hierachy_queue.extend(item["children"] or [])
|
||||
return folder_items
|
||||
|
||||
def _refresh_tasks_cache(self, folder_id, sender=None):
|
||||
if folder_id in self._tasks_refreshing:
|
||||
return
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
with self._task_refresh_event_manager(
|
||||
project_name, folder_id, sender
|
||||
):
|
||||
cache_item = self._tasks_cache.get(folder_id)
|
||||
if cache_item is None:
|
||||
cache_item = CacheItem()
|
||||
self._tasks_cache[folder_id] = cache_item
|
||||
|
||||
task_items = self._query_tasks(project_name, folder_id)
|
||||
cache_item.update_data(task_items)
|
||||
|
||||
def _query_tasks(self, project_name, folder_id):
|
||||
tasks = list(ayon_api.get_tasks(
|
||||
project_name,
|
||||
folder_ids=[folder_id],
|
||||
fields={"id", "name", "label", "folderId", "type"}
|
||||
))
|
||||
return _get_task_items_from_tasks(tasks)
|
||||
91
openpype/tools/ayon_workfiles/models/selection.py
Normal file
91
openpype/tools/ayon_workfiles/models/selection.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
class SelectionModel(object):
|
||||
"""Model handling selection changes.
|
||||
|
||||
Triggering events:
|
||||
- "selection.folder.changed"
|
||||
- "selection.task.changed"
|
||||
- "workarea.selection.changed"
|
||||
- "selection.representation.changed"
|
||||
"""
|
||||
|
||||
event_source = "selection.model"
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
|
||||
self._folder_id = None
|
||||
self._task_name = None
|
||||
self._task_id = None
|
||||
self._workfile_path = None
|
||||
self._representation_id = None
|
||||
|
||||
def get_selected_folder_id(self):
|
||||
return self._folder_id
|
||||
|
||||
def set_selected_folder(self, folder_id):
|
||||
if folder_id == self._folder_id:
|
||||
return
|
||||
|
||||
self._folder_id = folder_id
|
||||
self._controller.emit_event(
|
||||
"selection.folder.changed",
|
||||
{"folder_id": folder_id},
|
||||
self.event_source
|
||||
)
|
||||
|
||||
def get_selected_task_name(self):
|
||||
return self._task_name
|
||||
|
||||
def get_selected_task_id(self):
|
||||
return self._task_id
|
||||
|
||||
def set_selected_task(self, folder_id, task_id, task_name):
|
||||
if folder_id != self._folder_id:
|
||||
self.set_selected_folder(folder_id)
|
||||
|
||||
if task_id == self._task_id:
|
||||
return
|
||||
|
||||
self._task_name = task_name
|
||||
self._task_id = task_id
|
||||
self._controller.emit_event(
|
||||
"selection.task.changed",
|
||||
{
|
||||
"folder_id": folder_id,
|
||||
"task_name": task_name,
|
||||
"task_id": task_id
|
||||
},
|
||||
self.event_source
|
||||
)
|
||||
|
||||
def get_selected_workfile_path(self):
|
||||
return self._workfile_path
|
||||
|
||||
def set_selected_workfile_path(self, path):
|
||||
if path == self._workfile_path:
|
||||
return
|
||||
|
||||
self._workfile_path = path
|
||||
self._controller.emit_event(
|
||||
"workarea.selection.changed",
|
||||
{
|
||||
"path": path,
|
||||
"folder_id": self._folder_id,
|
||||
"task_name": self._task_name,
|
||||
"task_id": self._task_id,
|
||||
},
|
||||
self.event_source
|
||||
)
|
||||
|
||||
def get_selected_representation_id(self):
|
||||
return self._representation_id
|
||||
|
||||
def set_selected_representation_id(self, representation_id):
|
||||
if representation_id == self._representation_id:
|
||||
return
|
||||
self._representation_id = representation_id
|
||||
self._controller.emit_event(
|
||||
"selection.representation.changed",
|
||||
{"representation_id": representation_id},
|
||||
self.event_source
|
||||
)
|
||||
711
openpype/tools/ayon_workfiles/models/workfiles.py
Normal file
711
openpype/tools/ayon_workfiles/models/workfiles.py
Normal file
|
|
@ -0,0 +1,711 @@
|
|||
import os
|
||||
import re
|
||||
import copy
|
||||
|
||||
import arrow
|
||||
import ayon_api
|
||||
from ayon_api.operations import OperationsSession
|
||||
|
||||
from openpype.client import get_project
|
||||
from openpype.client.operations import (
|
||||
prepare_workfile_info_update_data,
|
||||
)
|
||||
from openpype.pipeline.template_data import (
|
||||
get_template_data,
|
||||
)
|
||||
from openpype.pipeline.workfile import (
|
||||
get_workdir_with_workdir_data,
|
||||
get_workfile_template_key,
|
||||
get_last_workfile_with_version,
|
||||
)
|
||||
from openpype.pipeline.version_start import get_versioning_start
|
||||
from openpype.tools.ayon_workfiles.abstract import (
|
||||
WorkareaFilepathResult,
|
||||
FileItem,
|
||||
WorkfileInfo,
|
||||
)
|
||||
|
||||
|
||||
def get_folder_template_data(folder):
|
||||
if not folder:
|
||||
return {}
|
||||
parts = folder["path"].split("/")
|
||||
parts.pop(-1)
|
||||
hierarchy = "/".join(parts)
|
||||
return {
|
||||
"asset": folder["name"],
|
||||
"folder": {
|
||||
"name": folder["name"],
|
||||
"type": folder["folderType"],
|
||||
"path": folder["path"],
|
||||
},
|
||||
"hierarchy": hierarchy,
|
||||
}
|
||||
|
||||
|
||||
def get_task_template_data(task):
|
||||
if not task:
|
||||
return {}
|
||||
return {
|
||||
"task": {
|
||||
"name": task["name"],
|
||||
"type": task["taskType"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CommentMatcher(object):
|
||||
"""Use anatomy and work file data to parse comments from filenames"""
|
||||
def __init__(self, extensions, file_template, data):
|
||||
self.fname_regex = None
|
||||
|
||||
if "{comment}" not in file_template:
|
||||
# Don't look for comment if template doesn't allow it
|
||||
return
|
||||
|
||||
# Create a regex group for extensions
|
||||
any_extension = "(?:{})".format(
|
||||
"|".join(re.escape(ext.lstrip(".")) for ext in extensions)
|
||||
)
|
||||
|
||||
# Use placeholders that will never be in the filename
|
||||
temp_data = copy.deepcopy(data)
|
||||
temp_data["comment"] = "<<comment>>"
|
||||
temp_data["version"] = "<<version>>"
|
||||
temp_data["ext"] = "<<ext>>"
|
||||
|
||||
fname_pattern = file_template.format_strict(temp_data)
|
||||
fname_pattern = re.escape(fname_pattern)
|
||||
|
||||
# Replace comment and version with something we can match with regex
|
||||
replacements = {
|
||||
"<<comment>>": "(.+)",
|
||||
"<<version>>": "[0-9]+",
|
||||
"<<ext>>": any_extension,
|
||||
}
|
||||
for src, dest in replacements.items():
|
||||
fname_pattern = fname_pattern.replace(re.escape(src), dest)
|
||||
|
||||
# Match from beginning to end of string to be safe
|
||||
fname_pattern = "^{}$".format(fname_pattern)
|
||||
|
||||
self.fname_regex = re.compile(fname_pattern)
|
||||
|
||||
def parse_comment(self, filepath):
|
||||
"""Parse the {comment} part from a filename"""
|
||||
if not self.fname_regex:
|
||||
return
|
||||
|
||||
fname = os.path.basename(filepath)
|
||||
match = self.fname_regex.match(fname)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
|
||||
class WorkareaModel:
|
||||
"""Workfiles model looking for workfiles in workare folder.
|
||||
|
||||
Workarea folder is usually task and host specific, defined by
|
||||
anatomy templates. Is looking for files with extensions defined
|
||||
by host integration.
|
||||
"""
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
extensions = None
|
||||
if controller.is_host_valid():
|
||||
extensions = controller.get_workfile_extensions()
|
||||
self._extensions = extensions
|
||||
self._base_data = None
|
||||
self._fill_data_by_folder_id = {}
|
||||
self._task_data_by_folder_id = {}
|
||||
self._workdir_by_context = {}
|
||||
|
||||
@property
|
||||
def project_name(self):
|
||||
return self._controller.get_current_project_name()
|
||||
|
||||
def reset(self):
|
||||
self._base_data = None
|
||||
self._fill_data_by_folder_id = {}
|
||||
self._task_data_by_folder_id = {}
|
||||
|
||||
def _get_base_data(self):
|
||||
if self._base_data is None:
|
||||
base_data = get_template_data(get_project(self.project_name))
|
||||
base_data["app"] = self._controller.get_host_name()
|
||||
self._base_data = base_data
|
||||
return copy.deepcopy(self._base_data)
|
||||
|
||||
def _get_folder_data(self, folder_id):
|
||||
fill_data = self._fill_data_by_folder_id.get(folder_id)
|
||||
if fill_data is None:
|
||||
folder = self._controller.get_folder_entity(folder_id)
|
||||
fill_data = get_folder_template_data(folder)
|
||||
self._fill_data_by_folder_id[folder_id] = fill_data
|
||||
return copy.deepcopy(fill_data)
|
||||
|
||||
def _get_task_data(self, folder_id, task_id):
|
||||
task_data = self._task_data_by_folder_id.setdefault(folder_id, {})
|
||||
if task_id not in task_data:
|
||||
task = self._controller.get_task_entity(task_id)
|
||||
if task:
|
||||
task_data[task_id] = get_task_template_data(task)
|
||||
return copy.deepcopy(task_data[task_id])
|
||||
|
||||
def _prepare_fill_data(self, folder_id, task_id):
|
||||
if not folder_id or not task_id:
|
||||
return {}
|
||||
|
||||
base_data = self._get_base_data()
|
||||
folder_data = self._get_folder_data(folder_id)
|
||||
task_data = self._get_task_data(folder_id, task_id)
|
||||
|
||||
base_data.update(folder_data)
|
||||
base_data.update(task_data)
|
||||
|
||||
return base_data
|
||||
|
||||
def get_workarea_dir_by_context(self, folder_id, task_id):
|
||||
if not folder_id or not task_id:
|
||||
return None
|
||||
folder_mapping = self._workdir_by_context.setdefault(folder_id, {})
|
||||
workdir = folder_mapping.get(task_id)
|
||||
if workdir is not None:
|
||||
return workdir
|
||||
|
||||
workdir_data = self._prepare_fill_data(folder_id, task_id)
|
||||
|
||||
workdir = get_workdir_with_workdir_data(
|
||||
workdir_data,
|
||||
self.project_name,
|
||||
anatomy=self._controller.project_anatomy,
|
||||
)
|
||||
folder_mapping[task_id] = workdir
|
||||
return workdir
|
||||
|
||||
def get_file_items(self, folder_id, task_id):
|
||||
items = []
|
||||
if not folder_id or not task_id:
|
||||
return items
|
||||
|
||||
workdir = self.get_workarea_dir_by_context(folder_id, task_id)
|
||||
if not os.path.exists(workdir):
|
||||
return items
|
||||
|
||||
for filename in os.listdir(workdir):
|
||||
filepath = os.path.join(workdir, filename)
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext not in self._extensions:
|
||||
continue
|
||||
|
||||
modified = os.path.getmtime(filepath)
|
||||
items.append(
|
||||
FileItem(workdir, filename, modified)
|
||||
)
|
||||
return items
|
||||
|
||||
def _get_template_key(self, fill_data):
|
||||
task_type = fill_data.get("task", {}).get("type")
|
||||
# TODO cache
|
||||
return get_workfile_template_key(
|
||||
task_type,
|
||||
self._controller.get_host_name(),
|
||||
project_name=self.project_name
|
||||
)
|
||||
|
||||
def _get_last_workfile_version(
|
||||
self, workdir, file_template, fill_data, extensions
|
||||
):
|
||||
version = get_last_workfile_with_version(
|
||||
workdir, str(file_template), fill_data, extensions
|
||||
)[1]
|
||||
|
||||
if version is None:
|
||||
task_info = fill_data.get("task", {})
|
||||
version = get_versioning_start(
|
||||
self.project_name,
|
||||
self._controller.get_host_name(),
|
||||
task_name=task_info.get("name"),
|
||||
task_type=task_info.get("type"),
|
||||
family="workfile",
|
||||
project_settings=self._controller.project_settings,
|
||||
)
|
||||
else:
|
||||
version += 1
|
||||
return version
|
||||
|
||||
def _get_comments_from_root(
|
||||
self,
|
||||
file_template,
|
||||
extensions,
|
||||
fill_data,
|
||||
root,
|
||||
current_filename,
|
||||
):
|
||||
current_comment = None
|
||||
comment_hints = set()
|
||||
filenames = []
|
||||
if root and os.path.exists(root):
|
||||
for filename in os.listdir(root):
|
||||
path = os.path.join(root, filename)
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
|
||||
ext = os.path.splitext(filename)[-1].lower()
|
||||
if ext in extensions:
|
||||
filenames.append(filename)
|
||||
|
||||
if not filenames:
|
||||
return comment_hints, current_comment
|
||||
|
||||
matcher = CommentMatcher(extensions, file_template, fill_data)
|
||||
|
||||
for filename in filenames:
|
||||
comment = matcher.parse_comment(filename)
|
||||
if comment:
|
||||
comment_hints.add(comment)
|
||||
if filename == current_filename:
|
||||
current_comment = comment
|
||||
|
||||
return list(comment_hints), current_comment
|
||||
|
||||
def _get_workdir(self, anatomy, template_key, fill_data):
|
||||
template_info = anatomy.templates_obj[template_key]
|
||||
directory_template = template_info["folder"]
|
||||
return directory_template.format_strict(fill_data).normalized()
|
||||
|
||||
def get_workarea_save_as_data(self, folder_id, task_id):
|
||||
folder = None
|
||||
task = None
|
||||
if folder_id:
|
||||
folder = self._controller.get_folder_entity(folder_id)
|
||||
if task_id:
|
||||
task = self._controller.get_task_entity(task_id)
|
||||
|
||||
if not folder or not task:
|
||||
return {
|
||||
"template_key": None,
|
||||
"template_has_version": None,
|
||||
"template_has_comment": None,
|
||||
"ext": None,
|
||||
"workdir": None,
|
||||
"comment": None,
|
||||
"comment_hints": None,
|
||||
"last_version": None,
|
||||
"extensions": None,
|
||||
}
|
||||
|
||||
anatomy = self._controller.project_anatomy
|
||||
fill_data = self._prepare_fill_data(folder_id, task_id)
|
||||
template_key = self._get_template_key(fill_data)
|
||||
|
||||
current_workfile = self._controller.get_current_workfile()
|
||||
current_filename = None
|
||||
current_ext = None
|
||||
if current_workfile:
|
||||
current_filename = os.path.basename(current_workfile)
|
||||
current_ext = os.path.splitext(current_filename)[1].lower()
|
||||
|
||||
extensions = self._extensions
|
||||
if not current_ext and extensions:
|
||||
current_ext = tuple(extensions)[0]
|
||||
|
||||
workdir = self._get_workdir(anatomy, template_key, fill_data)
|
||||
|
||||
template_info = anatomy.templates_obj[template_key]
|
||||
file_template = template_info["file"]
|
||||
|
||||
comment_hints, comment = self._get_comments_from_root(
|
||||
file_template,
|
||||
extensions,
|
||||
fill_data,
|
||||
workdir,
|
||||
current_filename,
|
||||
)
|
||||
last_version = self._get_last_workfile_version(
|
||||
workdir, file_template, fill_data, extensions)
|
||||
str_file_template = str(file_template)
|
||||
template_has_version = "{version" in str_file_template
|
||||
template_has_comment = "{comment" in str_file_template
|
||||
|
||||
return {
|
||||
"template_key": template_key,
|
||||
"template_has_version": template_has_version,
|
||||
"template_has_comment": template_has_comment,
|
||||
"ext": current_ext,
|
||||
"workdir": workdir,
|
||||
"comment": comment,
|
||||
"comment_hints": comment_hints,
|
||||
"last_version": last_version,
|
||||
"extensions": extensions,
|
||||
}
|
||||
|
||||
def fill_workarea_filepath(
|
||||
self,
|
||||
folder_id,
|
||||
task_id,
|
||||
extension,
|
||||
use_last_version,
|
||||
version,
|
||||
comment,
|
||||
):
|
||||
anatomy = self._controller.project_anatomy
|
||||
fill_data = self._prepare_fill_data(folder_id, task_id)
|
||||
template_key = self._get_template_key(fill_data)
|
||||
|
||||
workdir = self._get_workdir(anatomy, template_key, fill_data)
|
||||
|
||||
template_info = anatomy.templates_obj[template_key]
|
||||
file_template = template_info["file"]
|
||||
|
||||
if use_last_version:
|
||||
version = self._get_last_workfile_version(
|
||||
workdir, file_template, fill_data, self._extensions
|
||||
)
|
||||
fill_data["version"] = version
|
||||
fill_data["ext"] = extension.lstrip(".")
|
||||
|
||||
if comment:
|
||||
fill_data["comment"] = comment
|
||||
|
||||
filename = file_template.format(fill_data)
|
||||
if not filename.solved:
|
||||
filename = None
|
||||
|
||||
exists = False
|
||||
if filename:
|
||||
filepath = os.path.join(workdir, filename)
|
||||
exists = os.path.exists(filepath)
|
||||
|
||||
return WorkareaFilepathResult(
|
||||
workdir,
|
||||
filename,
|
||||
exists
|
||||
)
|
||||
|
||||
|
||||
class WorkfileEntitiesModel:
|
||||
"""Workfile entities model.
|
||||
|
||||
Args:
|
||||
control (AbstractWorkfileController): Controller object.
|
||||
"""
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
self._cache = {}
|
||||
self._items = {}
|
||||
|
||||
def _get_workfile_info_identifier(
|
||||
self, folder_id, task_id, rootless_path
|
||||
):
|
||||
return "_".join([folder_id, task_id, rootless_path])
|
||||
|
||||
def _get_rootless_path(self, filepath):
|
||||
anatomy = self._controller.project_anatomy
|
||||
|
||||
workdir, filename = os.path.split(filepath)
|
||||
success, rootless_dir = anatomy.find_root_template_from_path(workdir)
|
||||
return "/".join([
|
||||
os.path.normpath(rootless_dir).replace("\\", "/"),
|
||||
filename
|
||||
])
|
||||
|
||||
def _prepare_workfile_info_item(
|
||||
self, folder_id, task_id, workfile_info, filepath
|
||||
):
|
||||
note = ""
|
||||
if workfile_info:
|
||||
note = workfile_info["attrib"].get("description") or ""
|
||||
|
||||
filestat = os.stat(filepath)
|
||||
return WorkfileInfo(
|
||||
folder_id,
|
||||
task_id,
|
||||
filepath,
|
||||
filesize=filestat.st_size,
|
||||
creation_time=filestat.st_ctime,
|
||||
modification_time=filestat.st_mtime,
|
||||
note=note
|
||||
)
|
||||
|
||||
def _get_workfile_info(self, folder_id, task_id, identifier):
|
||||
workfile_info = self._cache.get(identifier)
|
||||
if workfile_info is not None:
|
||||
return workfile_info
|
||||
|
||||
for workfile_info in ayon_api.get_workfiles_info(
|
||||
self._controller.get_current_project_name(),
|
||||
task_ids=[task_id],
|
||||
fields=["id", "path", "attrib"],
|
||||
):
|
||||
workfile_identifier = self._get_workfile_info_identifier(
|
||||
folder_id, task_id, workfile_info["path"]
|
||||
)
|
||||
self._cache[workfile_identifier] = workfile_info
|
||||
return self._cache.get(identifier)
|
||||
|
||||
def get_workfile_info(
|
||||
self, folder_id, task_id, filepath, rootless_path=None
|
||||
):
|
||||
if not folder_id or not task_id or not filepath:
|
||||
return None
|
||||
|
||||
if rootless_path is None:
|
||||
rootless_path = self._get_rootless_path(filepath)
|
||||
|
||||
identifier = self._get_workfile_info_identifier(
|
||||
folder_id, task_id, rootless_path)
|
||||
item = self._items.get(identifier)
|
||||
if item is None:
|
||||
workfile_info = self._get_workfile_info(
|
||||
folder_id, task_id, identifier
|
||||
)
|
||||
item = self._prepare_workfile_info_item(
|
||||
folder_id, task_id, workfile_info, filepath
|
||||
)
|
||||
self._items[identifier] = item
|
||||
return item
|
||||
|
||||
def save_workfile_info(self, folder_id, task_id, filepath, note):
|
||||
rootless_path = self._get_rootless_path(filepath)
|
||||
identifier = self._get_workfile_info_identifier(
|
||||
folder_id, task_id, rootless_path
|
||||
)
|
||||
workfile_info = self._get_workfile_info(
|
||||
folder_id, task_id, identifier
|
||||
)
|
||||
if not workfile_info:
|
||||
self._cache[identifier] = self._create_workfile_info_entity(
|
||||
task_id, rootless_path, note)
|
||||
self._items.pop(identifier, None)
|
||||
return
|
||||
|
||||
new_workfile_info = copy.deepcopy(workfile_info)
|
||||
attrib = new_workfile_info.setdefault("attrib", {})
|
||||
attrib["description"] = note
|
||||
update_data = prepare_workfile_info_update_data(
|
||||
workfile_info, new_workfile_info
|
||||
)
|
||||
self._cache[identifier] = new_workfile_info
|
||||
self._items.pop(identifier, None)
|
||||
if not update_data:
|
||||
return
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
|
||||
session = OperationsSession()
|
||||
session.update_entity(
|
||||
project_name, "workfile", workfile_info["id"], update_data
|
||||
)
|
||||
session.commit()
|
||||
|
||||
def _create_workfile_info_entity(self, task_id, rootless_path, note):
|
||||
extension = os.path.splitext(rootless_path)[1]
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
|
||||
workfile_info = {
|
||||
"path": rootless_path,
|
||||
"taskId": task_id,
|
||||
"attrib": {
|
||||
"extension": extension,
|
||||
"description": note
|
||||
}
|
||||
}
|
||||
|
||||
session = OperationsSession()
|
||||
session.create_entity(project_name, "workfile", workfile_info)
|
||||
session.commit()
|
||||
return workfile_info
|
||||
|
||||
|
||||
class PublishWorkfilesModel:
|
||||
"""Model for handling of published workfiles.
|
||||
|
||||
Todos:
|
||||
Cache workfiles products and representations for some time.
|
||||
Note Representations won't change. Only what can change are
|
||||
versions.
|
||||
"""
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
self._cached_extensions = None
|
||||
self._cached_repre_extensions = None
|
||||
|
||||
@property
|
||||
def _extensions(self):
|
||||
if self._cached_extensions is None:
|
||||
exts = self._controller.get_workfile_extensions() or []
|
||||
self._cached_extensions = exts
|
||||
return self._cached_extensions
|
||||
|
||||
@property
|
||||
def _repre_extensions(self):
|
||||
if self._cached_repre_extensions is None:
|
||||
self._cached_repre_extensions = {
|
||||
ext.lstrip(".") for ext in self._extensions
|
||||
}
|
||||
return self._cached_repre_extensions
|
||||
|
||||
def _file_item_from_representation(
|
||||
self, repre_entity, project_anatomy, task_name=None
|
||||
):
|
||||
if task_name is not None:
|
||||
task_info = repre_entity["context"].get("task")
|
||||
if not task_info or task_info["name"] != task_name:
|
||||
return None
|
||||
|
||||
# Filter by extension
|
||||
extensions = self._repre_extensions
|
||||
workfile_path = None
|
||||
for repre_file in repre_entity["files"]:
|
||||
ext = (
|
||||
os.path.splitext(repre_file["name"])[1]
|
||||
.lower()
|
||||
.lstrip(".")
|
||||
)
|
||||
if ext in extensions:
|
||||
workfile_path = repre_file["path"]
|
||||
break
|
||||
|
||||
if not workfile_path:
|
||||
return None
|
||||
|
||||
try:
|
||||
workfile_path = workfile_path.format(
|
||||
root=project_anatomy.roots)
|
||||
except Exception as exc:
|
||||
print("Failed to format workfile path: {}".format(exc))
|
||||
|
||||
dirpath, filename = os.path.split(workfile_path)
|
||||
created_at = arrow.get(repre_entity["createdAt"])
|
||||
return FileItem(
|
||||
dirpath,
|
||||
filename,
|
||||
created_at.float_timestamp,
|
||||
repre_entity["id"]
|
||||
)
|
||||
|
||||
def get_file_items(self, folder_id, task_name):
|
||||
# TODO refactor to use less server API calls
|
||||
project_name = self._controller.get_current_project_name()
|
||||
# Get subset docs of asset
|
||||
product_entities = ayon_api.get_products(
|
||||
project_name,
|
||||
folder_ids=[folder_id],
|
||||
product_types=["workfile"],
|
||||
fields=["id", "name"]
|
||||
)
|
||||
|
||||
output = []
|
||||
product_ids = {product["id"] for product in product_entities}
|
||||
if not product_ids:
|
||||
return output
|
||||
|
||||
# Get version docs of subsets with their families
|
||||
version_entities = ayon_api.get_versions(
|
||||
project_name,
|
||||
product_ids=product_ids,
|
||||
fields=["id", "productId"]
|
||||
)
|
||||
version_ids = {version["id"] for version in version_entities}
|
||||
if not version_ids:
|
||||
return output
|
||||
|
||||
# Query representations of filtered versions and add filter for
|
||||
# extension
|
||||
repre_entities = ayon_api.get_representations(
|
||||
project_name,
|
||||
version_ids=version_ids
|
||||
)
|
||||
project_anatomy = self._controller.project_anatomy
|
||||
|
||||
# Filter queried representations by task name if task is set
|
||||
file_items = []
|
||||
for repre_entity in repre_entities:
|
||||
file_item = self._file_item_from_representation(
|
||||
repre_entity, project_anatomy, task_name
|
||||
)
|
||||
if file_item is not None:
|
||||
file_items.append(file_item)
|
||||
|
||||
return file_items
|
||||
|
||||
|
||||
class WorkfilesModel:
|
||||
"""Workfiles model."""
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
|
||||
self._entities_model = WorkfileEntitiesModel(controller)
|
||||
self._workarea_model = WorkareaModel(controller)
|
||||
self._published_model = PublishWorkfilesModel(controller)
|
||||
|
||||
def get_workfile_info(self, folder_id, task_id, filepath):
|
||||
return self._entities_model.get_workfile_info(
|
||||
folder_id, task_id, filepath
|
||||
)
|
||||
|
||||
def save_workfile_info(self, folder_id, task_id, filepath, note):
|
||||
self._entities_model.save_workfile_info(
|
||||
folder_id, task_id, filepath, note
|
||||
)
|
||||
|
||||
def get_workarea_dir_by_context(self, folder_id, task_id):
|
||||
"""Workarea dir for passed context.
|
||||
|
||||
The directory path is based on project anatomy templates.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Workarea dir path or None for invalid context.
|
||||
"""
|
||||
|
||||
return self._workarea_model.get_workarea_dir_by_context(
|
||||
folder_id, task_id)
|
||||
|
||||
def get_workarea_file_items(self, folder_id, task_id):
|
||||
"""Workfile items for passed context from workarea.
|
||||
|
||||
Args:
|
||||
folder_id (Union[str, None]): Folder id.
|
||||
task_id (Union[str, None]): Task id.
|
||||
|
||||
Returns:
|
||||
list[FileItem]: List of file items matching workarea of passed
|
||||
context.
|
||||
"""
|
||||
|
||||
return self._workarea_model.get_file_items(folder_id, task_id)
|
||||
|
||||
def get_workarea_save_as_data(self, folder_id, task_id):
|
||||
return self._workarea_model.get_workarea_save_as_data(
|
||||
folder_id, task_id)
|
||||
|
||||
def fill_workarea_filepath(self, *args, **kwargs):
|
||||
return self._workarea_model.fill_workarea_filepath(
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
def get_published_file_items(self, folder_id, task_name):
|
||||
"""Published workfiles for passed context.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_name (str): Task name.
|
||||
|
||||
Returns:
|
||||
list[FileItem]: List of files for published workfiles.
|
||||
"""
|
||||
|
||||
return self._published_model.get_file_items(folder_id, task_name)
|
||||
6
openpype/tools/ayon_workfiles/widgets/__init__.py
Normal file
6
openpype/tools/ayon_workfiles/widgets/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .window import WorkfilesToolWindow
|
||||
|
||||
|
||||
__all__ = (
|
||||
"WorkfilesToolWindow",
|
||||
)
|
||||
7
openpype/tools/ayon_workfiles/widgets/constants.py
Normal file
7
openpype/tools/ayon_workfiles/widgets/constants.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from qtpy import QtCore
|
||||
|
||||
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
PARENT_ID_ROLE = QtCore.Qt.UserRole + 2
|
||||
ITEM_NAME_ROLE = QtCore.Qt.UserRole + 3
|
||||
TASK_TYPE_ROLE = QtCore.Qt.UserRole + 4
|
||||
398
openpype/tools/ayon_workfiles/widgets/files_widget.py
Normal file
398
openpype/tools/ayon_workfiles/widgets/files_widget.py
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
import os
|
||||
|
||||
import qtpy
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from .save_as_dialog import SaveAsDialog
|
||||
from .files_widget_workarea import WorkAreaFilesWidget
|
||||
from .files_widget_published import PublishedFilesWidget
|
||||
|
||||
|
||||
class FilesWidget(QtWidgets.QWidget):
|
||||
"""A widget displaying files that allows to save and open files.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
parent (QtWidgets.QWidget): The parent widget.
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(FilesWidget, self).__init__(parent)
|
||||
|
||||
files_widget = QtWidgets.QStackedWidget(self)
|
||||
workarea_widget = WorkAreaFilesWidget(controller, files_widget)
|
||||
published_widget = PublishedFilesWidget(controller, files_widget)
|
||||
files_widget.addWidget(workarea_widget)
|
||||
files_widget.addWidget(published_widget)
|
||||
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
|
||||
workarea_btns_widget = QtWidgets.QWidget(btns_widget)
|
||||
workarea_btn_open = QtWidgets.QPushButton(
|
||||
"Open", workarea_btns_widget)
|
||||
workarea_btn_browse = QtWidgets.QPushButton(
|
||||
"Browse", workarea_btns_widget)
|
||||
workarea_btn_save = QtWidgets.QPushButton(
|
||||
"Save As", workarea_btns_widget)
|
||||
|
||||
workarea_btns_layout = QtWidgets.QHBoxLayout(workarea_btns_widget)
|
||||
workarea_btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
workarea_btns_layout.addWidget(workarea_btn_open, 1)
|
||||
workarea_btns_layout.addWidget(workarea_btn_browse, 1)
|
||||
workarea_btns_layout.addWidget(workarea_btn_save, 1)
|
||||
|
||||
published_btns_widget = QtWidgets.QWidget(btns_widget)
|
||||
published_btn_copy_n_open = QtWidgets.QPushButton(
|
||||
"Copy && Open", published_btns_widget
|
||||
)
|
||||
published_btn_change_context = QtWidgets.QPushButton(
|
||||
"Choose different context", published_btns_widget
|
||||
)
|
||||
published_btn_cancel = QtWidgets.QPushButton(
|
||||
"Cancel", published_btns_widget
|
||||
)
|
||||
|
||||
published_btns_layout = QtWidgets.QHBoxLayout(published_btns_widget)
|
||||
published_btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
published_btns_layout.addWidget(published_btn_copy_n_open, 1)
|
||||
published_btns_layout.addWidget(published_btn_change_context, 1)
|
||||
published_btns_layout.addWidget(published_btn_cancel, 1)
|
||||
|
||||
btns_layout = QtWidgets.QVBoxLayout(btns_widget)
|
||||
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
btns_layout.addWidget(workarea_btns_widget, 1)
|
||||
btns_layout.addWidget(published_btns_widget, 1)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(files_widget, 1)
|
||||
main_layout.addWidget(btns_widget, 0)
|
||||
|
||||
controller.register_event_callback(
|
||||
"workarea.selection.changed",
|
||||
self._on_workarea_path_changed
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"selection.representation.changed",
|
||||
self._on_published_repre_changed
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"selection.task.changed",
|
||||
self._on_task_changed
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"copy_representation.finished",
|
||||
self._on_copy_representation_finished,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"workfile_save_enable.changed",
|
||||
self._on_workfile_save_enabled_change,
|
||||
)
|
||||
|
||||
workarea_widget.open_current_requested.connect(
|
||||
self._on_current_open_requests)
|
||||
workarea_widget.duplicate_requested.connect(
|
||||
self._on_duplicate_request)
|
||||
workarea_btn_open.clicked.connect(self._on_workarea_open_clicked)
|
||||
workarea_btn_browse.clicked.connect(self._on_workarea_browse_clicked)
|
||||
workarea_btn_save.clicked.connect(self._on_workarea_save_clicked)
|
||||
|
||||
published_widget.save_as_requested.connect(self._on_save_as_request)
|
||||
published_btn_copy_n_open.clicked.connect(
|
||||
self._on_published_save_clicked)
|
||||
published_btn_change_context.clicked.connect(
|
||||
self._on_published_change_context_clicked)
|
||||
published_btn_cancel.clicked.connect(
|
||||
self._on_published_cancel_clicked)
|
||||
|
||||
self._selected_folder_id = None
|
||||
self._selected_tak_name = None
|
||||
|
||||
self._pre_select_folder_id = None
|
||||
self._pre_select_task_name = None
|
||||
|
||||
self._select_context_mode = False
|
||||
self._valid_selected_context = False
|
||||
self._valid_representation_id = False
|
||||
self._tmp_text_filter = None
|
||||
self._is_save_enabled = True
|
||||
|
||||
self._controller = controller
|
||||
self._files_widget = files_widget
|
||||
self._workarea_widget = workarea_widget
|
||||
self._published_widget = published_widget
|
||||
self._workarea_btns_widget = workarea_btns_widget
|
||||
self._published_btns_widget = published_btns_widget
|
||||
|
||||
self._workarea_btn_open = workarea_btn_open
|
||||
self._workarea_btn_browse = workarea_btn_browse
|
||||
self._workarea_btn_save = workarea_btn_save
|
||||
|
||||
self._published_widget = published_widget
|
||||
self._published_btn_copy_n_open = published_btn_copy_n_open
|
||||
self._published_btn_change_context = published_btn_change_context
|
||||
self._published_btn_cancel = published_btn_cancel
|
||||
|
||||
# Initial setup
|
||||
workarea_btn_open.setEnabled(False)
|
||||
published_btn_copy_n_open.setEnabled(False)
|
||||
published_btn_change_context.setEnabled(False)
|
||||
published_btn_cancel.setVisible(False)
|
||||
|
||||
def set_published_mode(self, published_mode):
|
||||
# Make sure context selection is disabled
|
||||
self._set_select_contex_mode(False)
|
||||
# Change current widget
|
||||
self._files_widget.setCurrentWidget((
|
||||
self._published_widget
|
||||
if published_mode
|
||||
else self._workarea_widget
|
||||
))
|
||||
# Pass the mode to the widgets, so they can start/stop handle events
|
||||
self._workarea_widget.set_published_mode(published_mode)
|
||||
self._published_widget.set_published_mode(published_mode)
|
||||
|
||||
# Change available buttons
|
||||
self._workarea_btns_widget.setVisible(not published_mode)
|
||||
self._published_btns_widget.setVisible(published_mode)
|
||||
|
||||
def set_text_filter(self, text_filter):
|
||||
if self._select_context_mode:
|
||||
self._tmp_text_filter = text_filter
|
||||
return
|
||||
self._workarea_widget.set_text_filter(text_filter)
|
||||
self._published_widget.set_text_filter(text_filter)
|
||||
|
||||
def _exec_save_as_dialog(self):
|
||||
"""Show SaveAs dialog using currently selected context.
|
||||
|
||||
Returns:
|
||||
Union[dict[str, Any], None]: Result of the dialog.
|
||||
"""
|
||||
|
||||
dialog = SaveAsDialog(self._controller, self)
|
||||
dialog.update_context()
|
||||
dialog.exec_()
|
||||
return dialog.get_result()
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Workarea workfiles
|
||||
# -------------------------------------------------------------
|
||||
def _open_workfile(self, filepath):
|
||||
if self._controller.has_unsaved_changes():
|
||||
result = self._save_changes_prompt()
|
||||
if result is None:
|
||||
return
|
||||
|
||||
if result:
|
||||
self._controller.save_current_workfile()
|
||||
self._controller.open_workfile(filepath)
|
||||
|
||||
def _on_workarea_open_clicked(self):
|
||||
path = self._workarea_widget.get_selected_path()
|
||||
if path:
|
||||
self._open_workfile(path)
|
||||
|
||||
def _on_current_open_requests(self):
|
||||
self._on_workarea_open_clicked()
|
||||
|
||||
def _on_duplicate_request(self):
|
||||
filepath = self._workarea_widget.get_selected_path()
|
||||
if filepath is None:
|
||||
return
|
||||
|
||||
result = self._exec_save_as_dialog()
|
||||
if result is None:
|
||||
return
|
||||
self._controller.duplicate_workfile(
|
||||
filepath,
|
||||
result["workdir"],
|
||||
result["filename"]
|
||||
)
|
||||
|
||||
def _on_workarea_browse_clicked(self):
|
||||
extnsions = self._controller.get_workfile_extensions()
|
||||
ext_filter = "Work File (*{0})".format(
|
||||
" *".join(extnsions)
|
||||
)
|
||||
dir_key = "directory"
|
||||
if qtpy.API in ("pyside", "pyside2", "pyside6"):
|
||||
dir_key = "dir"
|
||||
|
||||
selected_context = self._controller.get_selected_context()
|
||||
workfile_root = self._controller.get_workarea_dir_by_context(
|
||||
selected_context["folder_id"], selected_context["task_id"]
|
||||
)
|
||||
# Find existing directory of workfile root
|
||||
# - Qt will use 'cwd' instead, if path does not exist, which may lead
|
||||
# to igniter directory
|
||||
while workfile_root:
|
||||
if os.path.exists(workfile_root):
|
||||
break
|
||||
workfile_root = os.path.dirname(workfile_root)
|
||||
|
||||
kwargs = {
|
||||
"caption": "Work Files",
|
||||
"filter": ext_filter,
|
||||
dir_key: workfile_root
|
||||
}
|
||||
|
||||
filepath = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
|
||||
if filepath:
|
||||
self._open_workfile(filepath)
|
||||
|
||||
def _on_workarea_save_clicked(self):
|
||||
result = self._exec_save_as_dialog()
|
||||
if result is None:
|
||||
return
|
||||
self._controller.save_as_workfile(
|
||||
result["folder_id"],
|
||||
result["task_id"],
|
||||
result["workdir"],
|
||||
result["filename"],
|
||||
result["template_key"],
|
||||
)
|
||||
|
||||
def _on_workarea_path_changed(self, event):
|
||||
valid_path = event["path"] is not None
|
||||
self._workarea_btn_open.setEnabled(valid_path)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Published workfiles
|
||||
# -------------------------------------------------------------
|
||||
def _update_published_btns_state(self):
|
||||
enabled = (
|
||||
self._valid_representation_id
|
||||
and self._valid_selected_context
|
||||
and self._is_save_enabled
|
||||
)
|
||||
self._published_btn_copy_n_open.setEnabled(enabled)
|
||||
self._published_btn_change_context.setEnabled(enabled)
|
||||
|
||||
def _update_workarea_btns_state(self):
|
||||
enabled = self._is_save_enabled
|
||||
self._workarea_btn_save.setEnabled(enabled)
|
||||
|
||||
def _on_published_repre_changed(self, event):
|
||||
self._valid_representation_id = event["representation_id"] is not None
|
||||
self._update_published_btns_state()
|
||||
|
||||
def _on_task_changed(self, event):
|
||||
self._selected_folder_id = event["folder_id"]
|
||||
self._selected_tak_name = event["task_name"]
|
||||
self._valid_selected_context = (
|
||||
self._selected_folder_id is not None
|
||||
and self._selected_tak_name is not None
|
||||
)
|
||||
self._update_published_btns_state()
|
||||
|
||||
def _on_published_save_clicked(self):
|
||||
result = self._exec_save_as_dialog()
|
||||
if result is None:
|
||||
return
|
||||
|
||||
repre_info = self._published_widget.get_selected_repre_info()
|
||||
self._controller.copy_workfile_representation(
|
||||
repre_info["representation_id"],
|
||||
repre_info["filepath"],
|
||||
result["folder_id"],
|
||||
result["task_id"],
|
||||
result["workdir"],
|
||||
result["filename"],
|
||||
result["template_key"],
|
||||
)
|
||||
|
||||
def _on_save_as_request(self):
|
||||
self._on_published_save_clicked()
|
||||
|
||||
def _set_select_contex_mode(self, enabled):
|
||||
if self._select_context_mode is enabled:
|
||||
return
|
||||
|
||||
if enabled:
|
||||
self._pre_select_folder_id = self._selected_folder_id
|
||||
self._pre_select_task_name = self._selected_tak_name
|
||||
else:
|
||||
self._pre_select_folder_id = None
|
||||
self._pre_select_task_name = None
|
||||
self._select_context_mode = enabled
|
||||
self._published_btn_cancel.setVisible(enabled)
|
||||
self._published_btn_change_context.setVisible(not enabled)
|
||||
self._published_widget.set_select_context_mode(enabled)
|
||||
|
||||
if not enabled and self._tmp_text_filter is not None:
|
||||
self.set_text_filter(self._tmp_text_filter)
|
||||
self._tmp_text_filter = None
|
||||
|
||||
def _on_published_change_context_clicked(self):
|
||||
self._set_select_contex_mode(True)
|
||||
|
||||
def _should_set_pre_select_context(self):
|
||||
if self._pre_select_folder_id is None:
|
||||
return False
|
||||
if self._pre_select_folder_id != self._selected_folder_id:
|
||||
return True
|
||||
if self._pre_select_task_name is None:
|
||||
return False
|
||||
return self._pre_select_task_name != self._selected_tak_name
|
||||
|
||||
def _on_published_cancel_clicked(self):
|
||||
folder_id = self._pre_select_folder_id
|
||||
task_name = self._pre_select_task_name
|
||||
representation_id = self._published_widget.get_selected_repre_id()
|
||||
should_change_selection = self._should_set_pre_select_context()
|
||||
self._set_select_contex_mode(False)
|
||||
if should_change_selection:
|
||||
self._controller.set_expected_selection(
|
||||
folder_id, task_name, representation_id=representation_id
|
||||
)
|
||||
|
||||
def _on_copy_representation_finished(self, event):
|
||||
"""Callback for when copy representation is finished.
|
||||
|
||||
Make sure that select context mode is disabled when representation
|
||||
copy is finished.
|
||||
|
||||
Args:
|
||||
event (Event): Event object.
|
||||
"""
|
||||
|
||||
if not event["failed"]:
|
||||
self._set_select_contex_mode(False)
|
||||
|
||||
def _on_workfile_save_enabled_change(self, event):
|
||||
enabled = event["enabled"]
|
||||
self._is_save_enabled = enabled
|
||||
self._update_published_btns_state()
|
||||
self._update_workarea_btns_state()
|
||||
|
||||
def _save_changes_prompt(self):
|
||||
"""Ask user if wants to save changes to current file.
|
||||
|
||||
Returns:
|
||||
Union[bool, None]: True if user wants to save changes, False if
|
||||
user does not want to save changes, None if user cancels
|
||||
operation.
|
||||
"""
|
||||
messagebox = QtWidgets.QMessageBox(parent=self)
|
||||
messagebox.setWindowFlags(
|
||||
messagebox.windowFlags() | QtCore.Qt.FramelessWindowHint
|
||||
)
|
||||
messagebox.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
messagebox.setWindowTitle("Unsaved Changes!")
|
||||
messagebox.setText(
|
||||
"There are unsaved changes to the current file."
|
||||
"\nDo you want to save the changes?"
|
||||
)
|
||||
messagebox.setStandardButtons(
|
||||
QtWidgets.QMessageBox.Yes
|
||||
| QtWidgets.QMessageBox.No
|
||||
| QtWidgets.QMessageBox.Cancel
|
||||
)
|
||||
|
||||
result = messagebox.exec_()
|
||||
if result == QtWidgets.QMessageBox.Yes:
|
||||
return True
|
||||
if result == QtWidgets.QMessageBox.No:
|
||||
return False
|
||||
return None
|
||||
378
openpype/tools/ayon_workfiles/widgets/files_widget_published.py
Normal file
378
openpype/tools/ayon_workfiles/widgets/files_widget_published.py
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
import qtawesome
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.style import (
|
||||
get_default_entity_icon_color,
|
||||
get_disabled_entity_icon_color,
|
||||
)
|
||||
from openpype.tools.utils.delegates import PrettyTimeDelegate
|
||||
|
||||
from .utils import TreeView, BaseOverlayFrame
|
||||
|
||||
|
||||
REPRE_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
|
||||
DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3
|
||||
|
||||
|
||||
class PublishedFilesModel(QtGui.QStandardItemModel):
|
||||
"""A model for displaying files.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
"""
|
||||
|
||||
def __init__(self, controller):
|
||||
super(PublishedFilesModel, self).__init__()
|
||||
|
||||
self.setColumnCount(2)
|
||||
|
||||
self.setHeaderData(0, QtCore.Qt.Horizontal, "Name")
|
||||
self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified")
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.task.changed",
|
||||
self._on_task_changed
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"selection.folder.changed",
|
||||
self._on_folder_changed
|
||||
)
|
||||
|
||||
self._file_icon = qtawesome.icon(
|
||||
"fa.file-o",
|
||||
color=get_default_entity_icon_color()
|
||||
)
|
||||
self._controller = controller
|
||||
self._items_by_id = {}
|
||||
self._missing_context_item = None
|
||||
self._missing_context_used = False
|
||||
self._empty_root_item = None
|
||||
self._empty_item_used = False
|
||||
|
||||
self._published_mode = False
|
||||
self._context_select_mode = False
|
||||
|
||||
self._last_folder_id = None
|
||||
self._last_task_id = None
|
||||
|
||||
self._add_empty_item()
|
||||
|
||||
def _clear_items(self):
|
||||
self._remove_missing_context_item()
|
||||
self._remove_empty_item()
|
||||
if self._items_by_id:
|
||||
root = self.invisibleRootItem()
|
||||
root.removeRows(0, root.rowCount())
|
||||
self._items_by_id = {}
|
||||
|
||||
def set_published_mode(self, published_mode):
|
||||
if self._published_mode == published_mode:
|
||||
return
|
||||
self._published_mode = published_mode
|
||||
if published_mode:
|
||||
self._fill_items()
|
||||
elif self._context_select_mode:
|
||||
self.set_select_context_mode(False)
|
||||
|
||||
def set_select_context_mode(self, select_mode):
|
||||
if self._context_select_mode is select_mode:
|
||||
return
|
||||
self._context_select_mode = select_mode
|
||||
if not select_mode and self._published_mode:
|
||||
self._fill_items()
|
||||
|
||||
def get_index_by_representation_id(self, representation_id):
|
||||
item = self._items_by_id.get(representation_id)
|
||||
if item is None:
|
||||
return QtCore.QModelIndex()
|
||||
return self.indexFromItem(item)
|
||||
|
||||
def _get_missing_context_item(self):
|
||||
if self._missing_context_item is None:
|
||||
message = "Select folder"
|
||||
item = QtGui.QStandardItem(message)
|
||||
icon = qtawesome.icon(
|
||||
"fa.times",
|
||||
color=get_disabled_entity_icon_color()
|
||||
)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
item.setColumnCount(self.columnCount())
|
||||
self._missing_context_item = item
|
||||
return self._missing_context_item
|
||||
|
||||
def _add_missing_context_item(self):
|
||||
if self._missing_context_used:
|
||||
return
|
||||
self._clear_items()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(self._get_missing_context_item())
|
||||
self._missing_context_used = True
|
||||
|
||||
def _remove_missing_context_item(self):
|
||||
if not self._missing_context_used:
|
||||
return
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.takeRow(self._missing_context_item.row())
|
||||
self._missing_context_used = False
|
||||
|
||||
def _get_empty_root_item(self):
|
||||
if self._empty_root_item is None:
|
||||
message = "Didn't find any published workfiles."
|
||||
item = QtGui.QStandardItem(message)
|
||||
icon = qtawesome.icon(
|
||||
"fa.times",
|
||||
color=get_disabled_entity_icon_color()
|
||||
)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
item.setColumnCount(self.columnCount())
|
||||
self._empty_root_item = item
|
||||
return self._empty_root_item
|
||||
|
||||
def _add_empty_item(self):
|
||||
if self._empty_item_used:
|
||||
return
|
||||
self._clear_items()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(self._get_empty_root_item())
|
||||
self._empty_item_used = True
|
||||
|
||||
def _remove_empty_item(self):
|
||||
if not self._empty_item_used:
|
||||
return
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.takeRow(self._empty_root_item.row())
|
||||
self._empty_item_used = False
|
||||
|
||||
def _on_folder_changed(self, event):
|
||||
self._last_folder_id = event["folder_id"]
|
||||
self._last_task_id = None
|
||||
if self._context_select_mode:
|
||||
return
|
||||
|
||||
if self._published_mode:
|
||||
self._fill_items()
|
||||
|
||||
def _on_task_changed(self, event):
|
||||
self._last_folder_id = event["folder_id"]
|
||||
self._last_task_id = event["task_id"]
|
||||
if self._context_select_mode:
|
||||
return
|
||||
|
||||
if self._published_mode:
|
||||
self._fill_items()
|
||||
|
||||
def _fill_items(self):
|
||||
folder_id = self._last_folder_id
|
||||
task_id = self._last_task_id
|
||||
if not folder_id:
|
||||
self._add_missing_context_item()
|
||||
return
|
||||
|
||||
file_items = self._controller.get_published_file_items(
|
||||
folder_id, task_id
|
||||
)
|
||||
root_item = self.invisibleRootItem()
|
||||
if not file_items:
|
||||
self._add_empty_item()
|
||||
return
|
||||
self._remove_empty_item()
|
||||
self._remove_missing_context_item()
|
||||
|
||||
items_to_remove = set(self._items_by_id.keys())
|
||||
new_items = []
|
||||
for file_item in file_items:
|
||||
repre_id = file_item.representation_id
|
||||
if repre_id in self._items_by_id:
|
||||
items_to_remove.discard(repre_id)
|
||||
item = self._items_by_id[repre_id]
|
||||
else:
|
||||
item = QtGui.QStandardItem()
|
||||
new_items.append(item)
|
||||
item.setColumnCount(self.columnCount())
|
||||
item.setData(self._file_icon, QtCore.Qt.DecorationRole)
|
||||
item.setData(file_item.filename, QtCore.Qt.DisplayRole)
|
||||
item.setData(repre_id, REPRE_ID_ROLE)
|
||||
|
||||
if file_item.exists:
|
||||
flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||
else:
|
||||
flags = QtCore.Qt.NoItemFlags
|
||||
|
||||
item.setFlags(flags)
|
||||
item.setData(file_item.filepath, FILEPATH_ROLE)
|
||||
item.setData(file_item.modified, DATE_MODIFIED_ROLE)
|
||||
|
||||
self._items_by_id[repre_id] = item
|
||||
|
||||
if new_items:
|
||||
root_item.appendRows(new_items)
|
||||
|
||||
for repre_id in items_to_remove:
|
||||
item = self._items_by_id.pop(repre_id)
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
if root_item.rowCount() == 0:
|
||||
self._add_empty_item()
|
||||
|
||||
def flags(self, index):
|
||||
# Use flags of first column for all columns
|
||||
if index.column() != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
return super(PublishedFilesModel, self).flags(index)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
|
||||
# Handle roles for first column
|
||||
if index.column() == 1:
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
return None
|
||||
|
||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||
role = DATE_MODIFIED_ROLE
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
||||
return super(PublishedFilesModel, self).data(index, role)
|
||||
|
||||
|
||||
class SelectContextOverlay(BaseOverlayFrame):
|
||||
"""Overlay for files view when user should select context.
|
||||
|
||||
Todos:
|
||||
The look of this overlay should be improved, it is "not nice" now.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super(SelectContextOverlay, self).__init__(parent)
|
||||
|
||||
label_widget = QtWidgets.QLabel(
|
||||
"Please choose context on the left<br/><",
|
||||
self
|
||||
)
|
||||
label_widget.setAlignment(QtCore.Qt.AlignCenter)
|
||||
label_widget.setObjectName("OverlayFrameLabel")
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter)
|
||||
|
||||
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
|
||||
class PublishedFilesWidget(QtWidgets.QWidget):
|
||||
"""Published workfiles widget.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
parent (QtWidgets.QWidget): The parent widget.
|
||||
"""
|
||||
|
||||
selection_changed = QtCore.Signal()
|
||||
save_as_requested = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(PublishedFilesWidget, self).__init__(parent)
|
||||
|
||||
view = TreeView(self)
|
||||
view.setSortingEnabled(True)
|
||||
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
# Smaller indentation
|
||||
view.setIndentation(0)
|
||||
|
||||
model = PublishedFilesModel(controller)
|
||||
proxy_model = QtCore.QSortFilterProxyModel()
|
||||
proxy_model.setSourceModel(model)
|
||||
proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
proxy_model.setDynamicSortFilter(True)
|
||||
|
||||
view.setModel(proxy_model)
|
||||
|
||||
time_delegate = PrettyTimeDelegate()
|
||||
view.setItemDelegateForColumn(1, time_delegate)
|
||||
|
||||
# Default to a wider first filename column it is what we mostly care
|
||||
# about and the date modified is relatively small anyway.
|
||||
view.setColumnWidth(0, 330)
|
||||
|
||||
select_overlay = SelectContextOverlay(view)
|
||||
select_overlay.setVisible(False)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(view, 1)
|
||||
|
||||
selection_model = view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self._on_selection_change)
|
||||
view.double_clicked_left.connect(self._on_left_double_click)
|
||||
|
||||
controller.register_event_callback(
|
||||
"expected_selection_changed",
|
||||
self._on_expected_selection_change
|
||||
)
|
||||
|
||||
self._view = view
|
||||
self._select_overlay = select_overlay
|
||||
self._model = model
|
||||
self._proxy_model = proxy_model
|
||||
self._time_delegate = time_delegate
|
||||
self._controller = controller
|
||||
|
||||
def set_published_mode(self, published_mode):
|
||||
self._model.set_published_mode(published_mode)
|
||||
|
||||
def set_select_context_mode(self, select_mode):
|
||||
self._model.set_select_context_mode(select_mode)
|
||||
self._select_overlay.setVisible(select_mode)
|
||||
|
||||
def set_text_filter(self, text_filter):
|
||||
self._proxy_model.setFilterFixedString(text_filter)
|
||||
|
||||
def get_selected_repre_info(self):
|
||||
selection_model = self._view.selectionModel()
|
||||
representation_id = None
|
||||
filepath = None
|
||||
for index in selection_model.selectedIndexes():
|
||||
representation_id = index.data(REPRE_ID_ROLE)
|
||||
filepath = index.data(FILEPATH_ROLE)
|
||||
|
||||
return {
|
||||
"representation_id": representation_id,
|
||||
"filepath": filepath,
|
||||
}
|
||||
|
||||
def get_selected_repre_id(self):
|
||||
return self.get_selected_repre_info()["representation_id"]
|
||||
|
||||
def _on_selection_change(self):
|
||||
repre_id = self.get_selected_repre_id()
|
||||
self._controller.set_selected_representation_id(repre_id)
|
||||
|
||||
def _on_left_double_click(self):
|
||||
self.save_as_requested.emit()
|
||||
|
||||
def _on_expected_selection_change(self, event):
|
||||
if (
|
||||
event["representation_id_selected"]
|
||||
or not event["folder_selected"]
|
||||
or (event["task_name"] and not event["task_selected"])
|
||||
):
|
||||
return
|
||||
|
||||
representation_id = event["representation_id"]
|
||||
selected_repre_id = self.get_selected_repre_id()
|
||||
if (
|
||||
representation_id is not None
|
||||
and representation_id != selected_repre_id
|
||||
):
|
||||
index = self._model.get_index_by_representation_id(
|
||||
representation_id)
|
||||
if index.isValid():
|
||||
proxy_index = self._proxy_model.mapFromSource(index)
|
||||
self._view.setCurrentIndex(proxy_index)
|
||||
|
||||
self._controller.expected_representation_selected(
|
||||
event["folder_id"], event["task_name"], representation_id
|
||||
)
|
||||
380
openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py
Normal file
380
openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
import qtawesome
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.style import (
|
||||
get_default_entity_icon_color,
|
||||
get_disabled_entity_icon_color,
|
||||
)
|
||||
from openpype.tools.utils.delegates import PrettyTimeDelegate
|
||||
|
||||
from .utils import TreeView
|
||||
|
||||
FILENAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
|
||||
DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3
|
||||
|
||||
|
||||
class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
||||
"""A model for workare workfiles.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
"""
|
||||
|
||||
def __init__(self, controller):
|
||||
super(WorkAreaFilesModel, self).__init__()
|
||||
|
||||
self.setColumnCount(2)
|
||||
|
||||
self.setHeaderData(0, QtCore.Qt.Horizontal, "Name")
|
||||
self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified")
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.task.changed",
|
||||
self._on_task_changed
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"workfile_duplicate.finished",
|
||||
self._on_duplicate_finished
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"save_as.finished",
|
||||
self._on_save_as_finished
|
||||
)
|
||||
|
||||
self._file_icon = qtawesome.icon(
|
||||
"fa.file-o",
|
||||
color=get_default_entity_icon_color()
|
||||
)
|
||||
self._controller = controller
|
||||
self._items_by_filename = {}
|
||||
self._missing_context_item = None
|
||||
self._missing_context_used = False
|
||||
self._empty_root_item = None
|
||||
self._empty_item_used = False
|
||||
self._published_mode = False
|
||||
self._selected_folder_id = None
|
||||
self._selected_task_id = None
|
||||
|
||||
self._add_missing_context_item()
|
||||
|
||||
def get_index_by_filename(self, filename):
|
||||
item = self._items_by_filename.get(filename)
|
||||
if item is None:
|
||||
return QtCore.QModelIndex()
|
||||
return self.indexFromItem(item)
|
||||
|
||||
def _get_missing_context_item(self):
|
||||
if self._missing_context_item is None:
|
||||
message = "Select folder and task"
|
||||
item = QtGui.QStandardItem(message)
|
||||
icon = qtawesome.icon(
|
||||
"fa.times",
|
||||
color=get_disabled_entity_icon_color()
|
||||
)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
item.setColumnCount(self.columnCount())
|
||||
self._missing_context_item = item
|
||||
return self._missing_context_item
|
||||
|
||||
def _clear_items(self):
|
||||
self._remove_missing_context_item()
|
||||
self._remove_empty_item()
|
||||
if self._items_by_filename:
|
||||
root = self.invisibleRootItem()
|
||||
root.removeRows(0, root.rowCount())
|
||||
self._items_by_filename = {}
|
||||
|
||||
def _add_missing_context_item(self):
|
||||
if self._missing_context_used:
|
||||
return
|
||||
self._clear_items()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(self._get_missing_context_item())
|
||||
self._missing_context_used = True
|
||||
|
||||
def _remove_missing_context_item(self):
|
||||
if not self._missing_context_used:
|
||||
return
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.takeRow(self._missing_context_item.row())
|
||||
self._missing_context_used = False
|
||||
|
||||
def _get_empty_root_item(self):
|
||||
if self._empty_root_item is None:
|
||||
message = "Work Area is empty.."
|
||||
item = QtGui.QStandardItem(message)
|
||||
icon = qtawesome.icon(
|
||||
"fa.exclamation-circle",
|
||||
color=get_disabled_entity_icon_color()
|
||||
)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
item.setColumnCount(self.columnCount())
|
||||
self._empty_root_item = item
|
||||
return self._empty_root_item
|
||||
|
||||
def _add_empty_item(self):
|
||||
if self._empty_item_used:
|
||||
return
|
||||
self._clear_items()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(self._get_empty_root_item())
|
||||
self._empty_item_used = True
|
||||
|
||||
def _remove_empty_item(self):
|
||||
if not self._empty_item_used:
|
||||
return
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.takeRow(self._empty_root_item.row())
|
||||
self._empty_item_used = False
|
||||
|
||||
def _on_task_changed(self, event):
|
||||
self._selected_folder_id = event["folder_id"]
|
||||
self._selected_task_id = event["task_id"]
|
||||
if not self._published_mode:
|
||||
self._fill_items()
|
||||
|
||||
def _on_duplicate_finished(self, event):
|
||||
if event["failed"]:
|
||||
return
|
||||
|
||||
if not self._published_mode:
|
||||
self._fill_items()
|
||||
|
||||
def _on_save_as_finished(self, event):
|
||||
if event["failed"]:
|
||||
return
|
||||
|
||||
if not self._published_mode:
|
||||
self._fill_items()
|
||||
|
||||
def _fill_items(self):
|
||||
folder_id = self._selected_folder_id
|
||||
task_id = self._selected_task_id
|
||||
if not folder_id or not task_id:
|
||||
self._add_missing_context_item()
|
||||
return
|
||||
|
||||
file_items = self._controller.get_workarea_file_items(
|
||||
folder_id, task_id
|
||||
)
|
||||
root_item = self.invisibleRootItem()
|
||||
if not file_items:
|
||||
self._add_empty_item()
|
||||
return
|
||||
self._remove_empty_item()
|
||||
self._remove_missing_context_item()
|
||||
|
||||
items_to_remove = set(self._items_by_filename.keys())
|
||||
new_items = []
|
||||
for file_item in file_items:
|
||||
filename = file_item.filename
|
||||
if filename in self._items_by_filename:
|
||||
items_to_remove.discard(filename)
|
||||
item = self._items_by_filename[filename]
|
||||
else:
|
||||
item = QtGui.QStandardItem()
|
||||
new_items.append(item)
|
||||
item.setColumnCount(self.columnCount())
|
||||
item.setFlags(
|
||||
QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||
)
|
||||
item.setData(self._file_icon, QtCore.Qt.DecorationRole)
|
||||
item.setData(file_item.filename, QtCore.Qt.DisplayRole)
|
||||
item.setData(file_item.filename, FILENAME_ROLE)
|
||||
|
||||
item.setData(file_item.filepath, FILEPATH_ROLE)
|
||||
item.setData(file_item.modified, DATE_MODIFIED_ROLE)
|
||||
|
||||
self._items_by_filename[file_item.filename] = item
|
||||
|
||||
if new_items:
|
||||
root_item.appendRows(new_items)
|
||||
|
||||
for filename in items_to_remove:
|
||||
item = self._items_by_filename.pop(filename)
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
if root_item.rowCount() == 0:
|
||||
self._add_empty_item()
|
||||
|
||||
def flags(self, index):
|
||||
# Use flags of first column for all columns
|
||||
if index.column() != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
return super(WorkAreaFilesModel, self).flags(index)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
|
||||
# Handle roles for first column
|
||||
if index.column() == 1:
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
return None
|
||||
|
||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||
role = DATE_MODIFIED_ROLE
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
||||
return super(WorkAreaFilesModel, self).data(index, role)
|
||||
|
||||
def set_published_mode(self, published_mode):
|
||||
if self._published_mode == published_mode:
|
||||
return
|
||||
self._published_mode = published_mode
|
||||
if not published_mode:
|
||||
self._fill_items()
|
||||
|
||||
|
||||
class WorkAreaFilesWidget(QtWidgets.QWidget):
|
||||
"""Workarea files widget.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
parent (QtWidgets.QWidget): The parent widget.
|
||||
"""
|
||||
|
||||
selection_changed = QtCore.Signal()
|
||||
open_current_requested = QtCore.Signal()
|
||||
duplicate_requested = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(WorkAreaFilesWidget, self).__init__(parent)
|
||||
|
||||
view = TreeView(self)
|
||||
view.setSortingEnabled(True)
|
||||
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
# Smaller indentation
|
||||
view.setIndentation(0)
|
||||
|
||||
model = WorkAreaFilesModel(controller)
|
||||
proxy_model = QtCore.QSortFilterProxyModel()
|
||||
proxy_model.setSourceModel(model)
|
||||
proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
proxy_model.setDynamicSortFilter(True)
|
||||
|
||||
view.setModel(proxy_model)
|
||||
|
||||
time_delegate = PrettyTimeDelegate()
|
||||
view.setItemDelegateForColumn(1, time_delegate)
|
||||
|
||||
# Default to a wider first filename column it is what we mostly care
|
||||
# about and the date modified is relatively small anyway.
|
||||
view.setColumnWidth(0, 330)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(view, 1)
|
||||
|
||||
selection_model = view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self._on_selection_change)
|
||||
view.double_clicked_left.connect(self._on_left_double_click)
|
||||
view.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
controller.register_event_callback(
|
||||
"expected_selection_changed",
|
||||
self._on_expected_selection_change
|
||||
)
|
||||
|
||||
self._view = view
|
||||
self._model = model
|
||||
self._proxy_model = proxy_model
|
||||
self._time_delegate = time_delegate
|
||||
self._controller = controller
|
||||
|
||||
self._published_mode = False
|
||||
|
||||
def set_published_mode(self, published_mode):
|
||||
"""Set the published mode.
|
||||
|
||||
Widget should ignore most of events when in published mode is enabled.
|
||||
|
||||
Args:
|
||||
published_mode (bool): The published mode.
|
||||
"""
|
||||
|
||||
self._model.set_published_mode(published_mode)
|
||||
self._published_mode = published_mode
|
||||
|
||||
def set_text_filter(self, text_filter):
|
||||
"""Set the text filter.
|
||||
|
||||
Args:
|
||||
text_filter (str): The text filter.
|
||||
"""
|
||||
|
||||
self._proxy_model.setFilterFixedString(text_filter)
|
||||
|
||||
def _get_selected_info(self):
|
||||
selection_model = self._view.selectionModel()
|
||||
filepath = None
|
||||
filename = None
|
||||
for index in selection_model.selectedIndexes():
|
||||
filepath = index.data(FILEPATH_ROLE)
|
||||
filename = index.data(FILENAME_ROLE)
|
||||
return {
|
||||
"filepath": filepath,
|
||||
"filename": filename,
|
||||
}
|
||||
|
||||
def get_selected_path(self):
|
||||
"""Selected filepath.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: The selected filepath or None if nothing is
|
||||
selected.
|
||||
"""
|
||||
return self._get_selected_info()["filepath"]
|
||||
|
||||
def _on_selection_change(self):
|
||||
filepath = self.get_selected_path()
|
||||
self._controller.set_selected_workfile_path(filepath)
|
||||
|
||||
def _on_left_double_click(self):
|
||||
self.open_current_requested.emit()
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
index = self._view.indexAt(point)
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
if not index.flags() & QtCore.Qt.ItemIsEnabled:
|
||||
return
|
||||
|
||||
menu = QtWidgets.QMenu(self)
|
||||
|
||||
# Duplicate
|
||||
action = QtWidgets.QAction("Duplicate", menu)
|
||||
tip = "Duplicate selected file."
|
||||
action.setToolTip(tip)
|
||||
action.setStatusTip(tip)
|
||||
action.triggered.connect(self._on_duplicate_pressed)
|
||||
menu.addAction(action)
|
||||
|
||||
# Show the context action menu
|
||||
global_point = self._view.mapToGlobal(point)
|
||||
_ = menu.exec_(global_point)
|
||||
|
||||
def _on_duplicate_pressed(self):
|
||||
self.duplicate_requested.emit()
|
||||
|
||||
def _on_expected_selection_change(self, event):
|
||||
if event["workfile_name_selected"]:
|
||||
return
|
||||
|
||||
workfile_name = event["workfile_name"]
|
||||
if (
|
||||
workfile_name is not None
|
||||
and workfile_name != self._get_selected_info()["filename"]
|
||||
):
|
||||
index = self._model.get_index_by_filename(workfile_name)
|
||||
if index.isValid():
|
||||
proxy_index = self._proxy_model.mapFromSource(index)
|
||||
self._view.setCurrentIndex(proxy_index)
|
||||
|
||||
self._controller.expected_workfile_selected(
|
||||
event["folder_id"], event["task_name"], workfile_name
|
||||
)
|
||||
324
openpype/tools/ayon_workfiles/widgets/folders_widget.py
Normal file
324
openpype/tools/ayon_workfiles/widgets/folders_widget.py
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
import uuid
|
||||
import collections
|
||||
|
||||
import qtawesome
|
||||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
from openpype.tools.utils import (
|
||||
RecursiveSortFilterProxyModel,
|
||||
DeselectableTreeView,
|
||||
)
|
||||
|
||||
from .constants import ITEM_ID_ROLE, ITEM_NAME_ROLE
|
||||
|
||||
SENDER_NAME = "qt_folders_model"
|
||||
|
||||
|
||||
class FoldersRefreshThread(QtCore.QThread):
|
||||
"""Thread for refreshing folders.
|
||||
|
||||
Call controller to get folders and emit signal when finished.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
"""
|
||||
|
||||
refresh_finished = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, controller):
|
||||
super(FoldersRefreshThread, self).__init__()
|
||||
self._id = uuid.uuid4().hex
|
||||
self._controller = controller
|
||||
self._result = None
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Thread id.
|
||||
|
||||
Returns:
|
||||
str: Unique id of the thread.
|
||||
"""
|
||||
|
||||
return self._id
|
||||
|
||||
def run(self):
|
||||
self._result = self._controller.get_folder_items(SENDER_NAME)
|
||||
self.refresh_finished.emit(self.id)
|
||||
|
||||
def get_result(self):
|
||||
return self._result
|
||||
|
||||
|
||||
class FoldersModel(QtGui.QStandardItemModel):
|
||||
"""Folders model which cares about refresh of folders.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
"""
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller):
|
||||
super(FoldersModel, self).__init__()
|
||||
|
||||
self._controller = controller
|
||||
self._items_by_id = {}
|
||||
self._parent_id_by_id = {}
|
||||
|
||||
self._refresh_threads = {}
|
||||
self._current_refresh_thread = None
|
||||
|
||||
self._has_content = False
|
||||
self._is_refreshing = False
|
||||
|
||||
@property
|
||||
def is_refreshing(self):
|
||||
"""Model is refreshing.
|
||||
|
||||
Returns:
|
||||
bool: True if model is refreshing.
|
||||
"""
|
||||
return self._is_refreshing
|
||||
|
||||
@property
|
||||
def has_content(self):
|
||||
"""Has at least one folder.
|
||||
|
||||
Returns:
|
||||
bool: True if model has at least one folder.
|
||||
"""
|
||||
|
||||
return self._has_content
|
||||
|
||||
def clear(self):
|
||||
self._items_by_id = {}
|
||||
self._parent_id_by_id = {}
|
||||
self._has_content = False
|
||||
super(FoldersModel, self).clear()
|
||||
|
||||
def get_index_by_id(self, item_id):
|
||||
"""Get index by folder id.
|
||||
|
||||
Returns:
|
||||
QtCore.QModelIndex: Index of the folder. Can be invalid if folder
|
||||
is not available.
|
||||
"""
|
||||
item = self._items_by_id.get(item_id)
|
||||
if item is None:
|
||||
return QtCore.QModelIndex()
|
||||
return self.indexFromItem(item)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh folders items.
|
||||
|
||||
Refresh start thread because it can cause that controller can
|
||||
start query from database if folders are not cached.
|
||||
"""
|
||||
|
||||
self._is_refreshing = True
|
||||
|
||||
thread = FoldersRefreshThread(self._controller)
|
||||
self._current_refresh_thread = thread.id
|
||||
self._refresh_threads[thread.id] = thread
|
||||
thread.refresh_finished.connect(self._on_refresh_thread)
|
||||
thread.start()
|
||||
|
||||
def _on_refresh_thread(self, thread_id):
|
||||
"""Callback when refresh thread is finished.
|
||||
|
||||
Technically can be running multiple refresh threads at the same time,
|
||||
to avoid using values from wrong thread, we check if thread id is
|
||||
current refresh thread id.
|
||||
|
||||
Folders are stored by id.
|
||||
|
||||
Args:
|
||||
thread_id (str): Thread id.
|
||||
"""
|
||||
|
||||
thread = self._refresh_threads.pop(thread_id)
|
||||
if thread_id != self._current_refresh_thread:
|
||||
return
|
||||
|
||||
folder_items_by_id = thread.get_result()
|
||||
if not folder_items_by_id:
|
||||
if folder_items_by_id is not None:
|
||||
self.clear()
|
||||
self._is_refreshing = False
|
||||
return
|
||||
|
||||
self._has_content = True
|
||||
|
||||
folder_ids = set(folder_items_by_id)
|
||||
ids_to_remove = set(self._items_by_id) - folder_ids
|
||||
|
||||
folder_items_by_parent = collections.defaultdict(list)
|
||||
for folder_item in folder_items_by_id.values():
|
||||
folder_items_by_parent[folder_item.parent_id].append(folder_item)
|
||||
|
||||
hierarchy_queue = collections.deque()
|
||||
hierarchy_queue.append(None)
|
||||
|
||||
while hierarchy_queue:
|
||||
parent_id = hierarchy_queue.popleft()
|
||||
folder_items = folder_items_by_parent[parent_id]
|
||||
if parent_id is None:
|
||||
parent_item = self.invisibleRootItem()
|
||||
else:
|
||||
parent_item = self._items_by_id[parent_id]
|
||||
|
||||
new_items = []
|
||||
for folder_item in folder_items:
|
||||
item_id = folder_item.entity_id
|
||||
item = self._items_by_id.get(item_id)
|
||||
if item is None:
|
||||
is_new = True
|
||||
item = QtGui.QStandardItem()
|
||||
item.setEditable(False)
|
||||
else:
|
||||
is_new = self._parent_id_by_id[item_id] != parent_id
|
||||
|
||||
icon = qtawesome.icon(
|
||||
folder_item.icon_name,
|
||||
color=folder_item.icon_color,
|
||||
)
|
||||
item.setData(item_id, ITEM_ID_ROLE)
|
||||
item.setData(folder_item.name, ITEM_NAME_ROLE)
|
||||
item.setData(folder_item.label, QtCore.Qt.DisplayRole)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
if is_new:
|
||||
new_items.append(item)
|
||||
self._items_by_id[item_id] = item
|
||||
self._parent_id_by_id[item_id] = parent_id
|
||||
|
||||
hierarchy_queue.append(item_id)
|
||||
|
||||
if new_items:
|
||||
parent_item.appendRows(new_items)
|
||||
|
||||
for item_id in ids_to_remove:
|
||||
item = self._items_by_id[item_id]
|
||||
parent_id = self._parent_id_by_id[item_id]
|
||||
if parent_id is None:
|
||||
parent_item = self.invisibleRootItem()
|
||||
else:
|
||||
parent_item = self._items_by_id[parent_id]
|
||||
parent_item.takeChild(item.row())
|
||||
|
||||
for item_id in ids_to_remove:
|
||||
self._items_by_id.pop(item_id)
|
||||
self._parent_id_by_id.pop(item_id)
|
||||
|
||||
self._is_refreshing = False
|
||||
self.refreshed.emit()
|
||||
|
||||
|
||||
class FoldersWidget(QtWidgets.QWidget):
|
||||
"""Folders widget.
|
||||
|
||||
Widget that handles folders view, model and selection.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
parent (QtWidgets.QWidget): The parent widget.
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(FoldersWidget, self).__init__(parent)
|
||||
|
||||
folders_view = DeselectableTreeView(self)
|
||||
folders_view.setHeaderHidden(True)
|
||||
|
||||
folders_model = FoldersModel(controller)
|
||||
folders_proxy_model = RecursiveSortFilterProxyModel()
|
||||
folders_proxy_model.setSourceModel(folders_model)
|
||||
|
||||
folders_view.setModel(folders_proxy_model)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(folders_view, 1)
|
||||
|
||||
controller.register_event_callback(
|
||||
"folders.refresh.finished",
|
||||
self._on_folders_refresh_finished
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"controller.refresh.finished",
|
||||
self._on_controller_refresh
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"expected_selection_changed",
|
||||
self._on_expected_selection_change
|
||||
)
|
||||
|
||||
selection_model = folders_view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self._on_selection_change)
|
||||
|
||||
folders_model.refreshed.connect(self._on_model_refresh)
|
||||
|
||||
self._controller = controller
|
||||
self._folders_view = folders_view
|
||||
self._folders_model = folders_model
|
||||
self._folders_proxy_model = folders_proxy_model
|
||||
|
||||
self._expected_selection = None
|
||||
|
||||
def set_name_filer(self, name):
|
||||
self._folders_proxy_model.setFilterFixedString(name)
|
||||
|
||||
def _clear(self):
|
||||
self._folders_model.clear()
|
||||
|
||||
def _on_folders_refresh_finished(self, event):
|
||||
if event["sender"] != SENDER_NAME:
|
||||
self._folders_model.refresh()
|
||||
|
||||
def _on_controller_refresh(self):
|
||||
self._update_expected_selection()
|
||||
|
||||
def _update_expected_selection(self, expected_data=None):
|
||||
if expected_data is None:
|
||||
expected_data = self._controller.get_expected_selection_data()
|
||||
|
||||
# We're done
|
||||
if expected_data["folder_selected"]:
|
||||
return
|
||||
|
||||
folder_id = expected_data["folder_id"]
|
||||
self._expected_selection = folder_id
|
||||
if not self._folders_model.is_refreshing:
|
||||
self._set_expected_selection()
|
||||
|
||||
def _set_expected_selection(self):
|
||||
folder_id = self._expected_selection
|
||||
self._expected_selection = None
|
||||
if (
|
||||
folder_id is not None
|
||||
and folder_id != self._get_selected_item_id()
|
||||
):
|
||||
index = self._folders_model.get_index_by_id(folder_id)
|
||||
if index.isValid():
|
||||
proxy_index = self._folders_proxy_model.mapFromSource(index)
|
||||
self._folders_view.setCurrentIndex(proxy_index)
|
||||
self._controller.expected_folder_selected(folder_id)
|
||||
|
||||
def _on_model_refresh(self):
|
||||
if self._expected_selection:
|
||||
self._set_expected_selection()
|
||||
self._folders_proxy_model.sort(0)
|
||||
|
||||
def _on_expected_selection_change(self, event):
|
||||
self._update_expected_selection(event.data)
|
||||
|
||||
def _get_selected_item_id(self):
|
||||
selection_model = self._folders_view.selectionModel()
|
||||
for index in selection_model.selectedIndexes():
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
if item_id is not None:
|
||||
return item_id
|
||||
return None
|
||||
|
||||
def _on_selection_change(self):
|
||||
item_id = self._get_selected_item_id()
|
||||
self._controller.set_selected_folder(item_id)
|
||||
351
openpype/tools/ayon_workfiles/widgets/save_as_dialog.py
Normal file
351
openpype/tools/ayon_workfiles/widgets/save_as_dialog.py
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from openpype.tools.utils import PlaceholderLineEdit
|
||||
|
||||
|
||||
class SubversionLineEdit(QtWidgets.QWidget):
|
||||
"""QLineEdit with QPushButton for drop down selection of list of strings"""
|
||||
|
||||
text_changed = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SubversionLineEdit, self).__init__(*args, **kwargs)
|
||||
|
||||
input_field = PlaceholderLineEdit(self)
|
||||
menu_btn = QtWidgets.QPushButton(self)
|
||||
menu_btn.setFixedWidth(18)
|
||||
|
||||
menu = QtWidgets.QMenu(self)
|
||||
menu_btn.setMenu(menu)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(3)
|
||||
|
||||
layout.addWidget(input_field, 1)
|
||||
layout.addWidget(menu_btn, 0)
|
||||
|
||||
input_field.textChanged.connect(self.text_changed)
|
||||
|
||||
self.setFocusProxy(input_field)
|
||||
|
||||
self._input_field = input_field
|
||||
self._menu_btn = menu_btn
|
||||
self._menu = menu
|
||||
|
||||
def set_placeholder(self, placeholder):
|
||||
self._input_field.setPlaceholderText(placeholder)
|
||||
|
||||
def set_text(self, text):
|
||||
self._input_field.setText(text)
|
||||
|
||||
def set_values(self, values):
|
||||
self._update(values)
|
||||
|
||||
def _on_button_clicked(self):
|
||||
self._menu.exec_()
|
||||
|
||||
def _on_action_clicked(self, action):
|
||||
self._input_field.setText(action.text())
|
||||
|
||||
def _update(self, values):
|
||||
"""Create optional predefined subset names
|
||||
|
||||
Args:
|
||||
default_names(list): all predefined names
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
menu = self._menu
|
||||
button = self._menu_btn
|
||||
|
||||
state = any(values)
|
||||
button.setEnabled(state)
|
||||
if state is False:
|
||||
return
|
||||
|
||||
# Include an empty string
|
||||
values = [""] + sorted(values)
|
||||
|
||||
# Get and destroy the action group
|
||||
group = button.findChild(QtWidgets.QActionGroup)
|
||||
if group:
|
||||
group.deleteLater()
|
||||
|
||||
# Build new action group
|
||||
group = QtWidgets.QActionGroup(button)
|
||||
for name in values:
|
||||
action = group.addAction(name)
|
||||
menu.addAction(action)
|
||||
|
||||
group.triggered.connect(self._on_action_clicked)
|
||||
|
||||
|
||||
class SaveAsDialog(QtWidgets.QDialog):
|
||||
"""Save as dialog to define a unique filename inside workdir.
|
||||
|
||||
The filename is calculated in controller where UI sends values from
|
||||
dialog inputs.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(SaveAsDialog, self).__init__(parent=parent)
|
||||
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
self._folder_id = None
|
||||
self._task_id = None
|
||||
self._last_version = None
|
||||
self._template_key = None
|
||||
self._comment_value = None
|
||||
self._version_value = None
|
||||
self._ext_value = None
|
||||
self._filename = None
|
||||
self._workdir = None
|
||||
|
||||
self._result = None
|
||||
|
||||
# Btns widget
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
|
||||
btn_ok = QtWidgets.QPushButton("Ok", btns_widget)
|
||||
btn_cancel = QtWidgets.QPushButton("Cancel", btns_widget)
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||
btns_layout.addWidget(btn_ok)
|
||||
btns_layout.addWidget(btn_cancel)
|
||||
|
||||
# Inputs widget
|
||||
inputs_widget = QtWidgets.QWidget(self)
|
||||
|
||||
# Version widget
|
||||
version_widget = QtWidgets.QWidget(inputs_widget)
|
||||
|
||||
# Version number input
|
||||
version_input = QtWidgets.QSpinBox(version_widget)
|
||||
version_input.setMinimum(1)
|
||||
version_input.setMaximum(9999)
|
||||
|
||||
# Last version checkbox
|
||||
last_version_check = QtWidgets.QCheckBox(
|
||||
"Next Available Version", version_widget
|
||||
)
|
||||
last_version_check.setChecked(True)
|
||||
|
||||
version_layout = QtWidgets.QHBoxLayout(version_widget)
|
||||
version_layout.setContentsMargins(0, 0, 0, 0)
|
||||
version_layout.addWidget(version_input)
|
||||
version_layout.addWidget(last_version_check)
|
||||
|
||||
# Preview widget
|
||||
preview_widget = QtWidgets.QLabel("Preview filename", inputs_widget)
|
||||
preview_widget.setWordWrap(True)
|
||||
|
||||
# Subversion input
|
||||
subversion_input = SubversionLineEdit(inputs_widget)
|
||||
subversion_input.set_placeholder("Will be part of filename.")
|
||||
|
||||
# Extensions combobox
|
||||
extension_combobox = QtWidgets.QComboBox(inputs_widget)
|
||||
# Add styled delegate to use stylesheets
|
||||
extension_delegate = QtWidgets.QStyledItemDelegate()
|
||||
extension_combobox.setItemDelegate(extension_delegate)
|
||||
|
||||
version_label = QtWidgets.QLabel("Version:", inputs_widget)
|
||||
subversion_label = QtWidgets.QLabel("Subversion:", inputs_widget)
|
||||
extension_label = QtWidgets.QLabel("Extension:", inputs_widget)
|
||||
preview_label = QtWidgets.QLabel("Preview:", inputs_widget)
|
||||
|
||||
# Build inputs
|
||||
inputs_layout = QtWidgets.QGridLayout(inputs_widget)
|
||||
inputs_layout.addWidget(version_label, 0, 0)
|
||||
inputs_layout.addWidget(version_widget, 0, 1)
|
||||
inputs_layout.addWidget(subversion_label, 1, 0)
|
||||
inputs_layout.addWidget(subversion_input, 1, 1)
|
||||
inputs_layout.addWidget(extension_label, 2, 0)
|
||||
inputs_layout.addWidget(extension_combobox, 2, 1)
|
||||
inputs_layout.addWidget(preview_label, 3, 0)
|
||||
inputs_layout.addWidget(preview_widget, 3, 1)
|
||||
|
||||
# Build layout
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.addWidget(inputs_widget)
|
||||
main_layout.addWidget(btns_widget)
|
||||
|
||||
# Signal callback registration
|
||||
version_input.valueChanged.connect(self._on_version_spinbox_change)
|
||||
last_version_check.stateChanged.connect(
|
||||
self._on_version_checkbox_change
|
||||
)
|
||||
|
||||
subversion_input.text_changed.connect(self._on_comment_change)
|
||||
extension_combobox.currentIndexChanged.connect(
|
||||
self._on_extension_change)
|
||||
|
||||
btn_ok.pressed.connect(self._on_ok_pressed)
|
||||
btn_cancel.pressed.connect(self._on_cancel_pressed)
|
||||
|
||||
# Store objects
|
||||
self._inputs_layout = inputs_layout
|
||||
|
||||
self._btn_ok = btn_ok
|
||||
self._btn_cancel = btn_cancel
|
||||
|
||||
self._version_widget = version_widget
|
||||
|
||||
self._version_input = version_input
|
||||
self._last_version_check = last_version_check
|
||||
|
||||
self._extension_delegate = extension_delegate
|
||||
self._extension_combobox = extension_combobox
|
||||
self._subversion_input = subversion_input
|
||||
self._preview_widget = preview_widget
|
||||
|
||||
self._version_label = version_label
|
||||
self._subversion_label = subversion_label
|
||||
self._extension_label = extension_label
|
||||
self._preview_label = preview_label
|
||||
|
||||
# Post init setup
|
||||
|
||||
# Allow "Enter" key to accept the save.
|
||||
btn_ok.setDefault(True)
|
||||
|
||||
# Disable version input if last version is checked
|
||||
version_input.setEnabled(not last_version_check.isChecked())
|
||||
|
||||
# Force default focus to comment, some hosts didn't automatically
|
||||
# apply focus to this line edit (e.g. Houdini)
|
||||
subversion_input.setFocus()
|
||||
|
||||
def get_result(self):
|
||||
return self._result
|
||||
|
||||
def update_context(self):
|
||||
# Add version only if template contains version key
|
||||
# - since the version can be padded with "{version:0>4}" we only search
|
||||
# for "{version".
|
||||
selected_context = self._controller.get_selected_context()
|
||||
folder_id = selected_context["folder_id"]
|
||||
task_id = selected_context["task_id"]
|
||||
data = self._controller.get_workarea_save_as_data(folder_id, task_id)
|
||||
last_version = data["last_version"]
|
||||
comment = data["comment"]
|
||||
comment_hints = data["comment_hints"]
|
||||
|
||||
template_has_version = data["template_has_version"]
|
||||
template_has_comment = data["template_has_comment"]
|
||||
|
||||
self._folder_id = folder_id
|
||||
self._task_id = task_id
|
||||
self._workdir = data["workdir"]
|
||||
self._comment_value = data["comment"]
|
||||
self._ext_value = data["ext"]
|
||||
self._template_key = data["template_key"]
|
||||
self._last_version = data["last_version"]
|
||||
|
||||
self._extension_combobox.clear()
|
||||
self._extension_combobox.addItems(data["extensions"])
|
||||
|
||||
self._version_input.setValue(last_version)
|
||||
|
||||
vw_idx = self._inputs_layout.indexOf(self._version_widget)
|
||||
self._version_label.setVisible(template_has_version)
|
||||
self._version_widget.setVisible(template_has_version)
|
||||
if template_has_version:
|
||||
if vw_idx == -1:
|
||||
self._inputs_layout.addWidget(self._version_label, 0, 0)
|
||||
self._inputs_layout.addWidget(self._version_widget, 0, 1)
|
||||
elif vw_idx != -1:
|
||||
self._inputs_layout.takeAt(vw_idx)
|
||||
self._inputs_layout.takeAt(
|
||||
self._inputs_layout.indexOf(self._version_label)
|
||||
)
|
||||
|
||||
cw_idx = self._inputs_layout.indexOf(self._subversion_input)
|
||||
self._subversion_label.setVisible(template_has_comment)
|
||||
self._subversion_input.setVisible(template_has_comment)
|
||||
if template_has_comment:
|
||||
if cw_idx == -1:
|
||||
self._inputs_layout.addWidget(self._subversion_label, 1, 0)
|
||||
self._inputs_layout.addWidget(self._subversion_input, 1, 1)
|
||||
elif cw_idx != -1:
|
||||
self._inputs_layout.takeAt(cw_idx)
|
||||
self._inputs_layout.takeAt(
|
||||
self._inputs_layout.indexOf(self._subversion_label)
|
||||
)
|
||||
|
||||
if template_has_comment:
|
||||
self._subversion_input.set_text(comment or "")
|
||||
self._subversion_input.set_values(comment_hints)
|
||||
self._update_filename()
|
||||
|
||||
def _on_version_spinbox_change(self, value):
|
||||
if value == self._version_value:
|
||||
return
|
||||
self._version_value = value
|
||||
if not self._last_version_check.isChecked():
|
||||
self._update_filename()
|
||||
|
||||
def _on_version_checkbox_change(self):
|
||||
use_last_version = self._last_version_check.isChecked()
|
||||
self._version_input.setEnabled(not use_last_version)
|
||||
if use_last_version:
|
||||
self._version_input.blockSignals(True)
|
||||
self._version_input.setValue(self._last_version)
|
||||
self._version_input.blockSignals(False)
|
||||
self._update_filename()
|
||||
|
||||
def _on_comment_change(self, text):
|
||||
if self._comment_value == text:
|
||||
return
|
||||
self._comment_value = text
|
||||
self._update_filename()
|
||||
|
||||
def _on_extension_change(self):
|
||||
ext = self._extension_combobox.currentText()
|
||||
if ext == self._ext_value:
|
||||
return
|
||||
self._ext_value = ext
|
||||
self._update_filename()
|
||||
|
||||
def _on_ok_pressed(self):
|
||||
self._result = {
|
||||
"filename": self._filename,
|
||||
"workdir": self._workdir,
|
||||
"folder_id": self._folder_id,
|
||||
"task_id": self._task_id,
|
||||
"template_key": self._template_key,
|
||||
}
|
||||
self.close()
|
||||
|
||||
def _on_cancel_pressed(self):
|
||||
self.close()
|
||||
|
||||
def _update_filename(self):
|
||||
result = self._controller.fill_workarea_filepath(
|
||||
self._folder_id,
|
||||
self._task_id,
|
||||
self._ext_value,
|
||||
self._last_version_check.isChecked(),
|
||||
self._version_value,
|
||||
self._comment_value,
|
||||
)
|
||||
self._filename = result.filename
|
||||
self._btn_ok.setEnabled(not result.exists)
|
||||
|
||||
if result.exists:
|
||||
self._preview_widget.setText((
|
||||
"<font color='red'>Cannot create \"{}\" because file exists!"
|
||||
"</font>"
|
||||
).format(result.filename))
|
||||
else:
|
||||
self._preview_widget.setText(
|
||||
"<font color='green'>{}</font>".format(result.filename)
|
||||
)
|
||||
163
openpype/tools/ayon_workfiles/widgets/side_panel.py
Normal file
163
openpype/tools/ayon_workfiles/widgets/side_panel.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import datetime
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
|
||||
def file_size_to_string(file_size):
|
||||
size = 0
|
||||
size_ending_mapping = {
|
||||
"KB": 1024 ** 1,
|
||||
"MB": 1024 ** 2,
|
||||
"GB": 1024 ** 3
|
||||
}
|
||||
ending = "B"
|
||||
for _ending, _size in size_ending_mapping.items():
|
||||
if file_size < _size:
|
||||
break
|
||||
size = file_size / _size
|
||||
ending = _ending
|
||||
return "{:.2f} {}".format(size, ending)
|
||||
|
||||
|
||||
class SidePanelWidget(QtWidgets.QWidget):
|
||||
"""Details about selected workfile.
|
||||
|
||||
Todos:
|
||||
At this moment only shows created and modified date of file
|
||||
or its size.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
parent (QtWidgets.QWidget): The parent widget.
|
||||
"""
|
||||
|
||||
published_workfile_message = (
|
||||
"<b>INFO</b>: Opened published workfiles will be stored in"
|
||||
" temp directory on your machine. Current temp size: <b>{}</b>."
|
||||
)
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(SidePanelWidget, self).__init__(parent)
|
||||
|
||||
details_label = QtWidgets.QLabel("Details", self)
|
||||
details_input = QtWidgets.QPlainTextEdit(self)
|
||||
details_input.setReadOnly(True)
|
||||
|
||||
artist_note_widget = QtWidgets.QWidget(self)
|
||||
note_label = QtWidgets.QLabel("Artist note", artist_note_widget)
|
||||
note_input = QtWidgets.QPlainTextEdit(artist_note_widget)
|
||||
btn_note_save = QtWidgets.QPushButton("Save note", artist_note_widget)
|
||||
|
||||
artist_note_layout = QtWidgets.QVBoxLayout(artist_note_widget)
|
||||
artist_note_layout.setContentsMargins(0, 0, 0, 0)
|
||||
artist_note_layout.addWidget(note_label, 0)
|
||||
artist_note_layout.addWidget(note_input, 1)
|
||||
artist_note_layout.addWidget(
|
||||
btn_note_save, 0, alignment=QtCore.Qt.AlignRight
|
||||
)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(details_label, 0)
|
||||
main_layout.addWidget(details_input, 1)
|
||||
main_layout.addWidget(artist_note_widget, 1)
|
||||
|
||||
note_input.textChanged.connect(self._on_note_change)
|
||||
btn_note_save.clicked.connect(self._on_save_click)
|
||||
|
||||
controller.register_event_callback(
|
||||
"workarea.selection.changed", self._on_selection_change
|
||||
)
|
||||
|
||||
self._details_input = details_input
|
||||
self._artist_note_widget = artist_note_widget
|
||||
self._note_input = note_input
|
||||
self._btn_note_save = btn_note_save
|
||||
|
||||
self._folder_id = None
|
||||
self._task_id = None
|
||||
self._filepath = None
|
||||
self._orig_note = ""
|
||||
self._controller = controller
|
||||
|
||||
self._set_context(None, None, None)
|
||||
|
||||
def set_published_mode(self, published_mode):
|
||||
"""Change published mode.
|
||||
|
||||
Args:
|
||||
published_mode (bool): Published mode enabled.
|
||||
"""
|
||||
|
||||
self._artist_note_widget.setVisible(not published_mode)
|
||||
|
||||
def _on_selection_change(self, event):
|
||||
folder_id = event["folder_id"]
|
||||
task_id = event["task_id"]
|
||||
filepath = event["path"]
|
||||
|
||||
self._set_context(folder_id, task_id, filepath)
|
||||
|
||||
def _on_note_change(self):
|
||||
text = self._note_input.toPlainText()
|
||||
self._btn_note_save.setEnabled(self._orig_note != text)
|
||||
|
||||
def _on_save_click(self):
|
||||
note = self._note_input.toPlainText()
|
||||
self._controller.save_workfile_info(
|
||||
self._folder_id,
|
||||
self._task_id,
|
||||
self._filepath,
|
||||
note
|
||||
)
|
||||
self._orig_note = note
|
||||
self._btn_note_save.setEnabled(False)
|
||||
|
||||
def _set_context(self, folder_id, task_id, filepath):
|
||||
workfile_info = None
|
||||
# Check if folder, task and file are selected
|
||||
if bool(folder_id) and bool(task_id) and bool(filepath):
|
||||
workfile_info = self._controller.get_workfile_info(
|
||||
folder_id, task_id, filepath
|
||||
)
|
||||
enabled = workfile_info is not None
|
||||
|
||||
self._details_input.setEnabled(enabled)
|
||||
self._note_input.setEnabled(enabled)
|
||||
self._btn_note_save.setEnabled(enabled)
|
||||
|
||||
self._folder_id = folder_id
|
||||
self._task_id = task_id
|
||||
self._filepath = filepath
|
||||
|
||||
# Disable inputs and remove texts if any required arguments are
|
||||
# missing
|
||||
if not enabled:
|
||||
self._orig_note = ""
|
||||
self._details_input.setPlainText("")
|
||||
self._note_input.setPlainText("")
|
||||
return
|
||||
|
||||
note = workfile_info.note
|
||||
size_value = file_size_to_string(workfile_info.filesize)
|
||||
|
||||
# Append html string
|
||||
datetime_format = "%b %d %Y %H:%M:%S"
|
||||
creation_time = datetime.datetime.fromtimestamp(
|
||||
workfile_info.creation_time)
|
||||
modification_time = datetime.datetime.fromtimestamp(
|
||||
workfile_info.modification_time)
|
||||
lines = (
|
||||
"<b>Size:</b>",
|
||||
size_value,
|
||||
"<b>Created:</b>",
|
||||
creation_time.strftime(datetime_format),
|
||||
"<b>Modified:</b>",
|
||||
modification_time.strftime(datetime_format)
|
||||
)
|
||||
self._orig_note = note
|
||||
self._note_input.setPlainText(note)
|
||||
|
||||
# Set as empty string
|
||||
self._details_input.setPlainText("")
|
||||
self._details_input.appendHtml("<br>".join(lines))
|
||||
420
openpype/tools/ayon_workfiles/widgets/tasks_widget.py
Normal file
420
openpype/tools/ayon_workfiles/widgets/tasks_widget.py
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
import uuid
|
||||
import qtawesome
|
||||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
from openpype.style import get_disabled_entity_icon_color
|
||||
from openpype.tools.utils import DeselectableTreeView
|
||||
|
||||
from .constants import (
|
||||
ITEM_NAME_ROLE,
|
||||
ITEM_ID_ROLE,
|
||||
PARENT_ID_ROLE,
|
||||
)
|
||||
|
||||
SENDER_NAME = "qt_tasks_model"
|
||||
|
||||
|
||||
class RefreshThread(QtCore.QThread):
|
||||
"""Thread for refreshing tasks.
|
||||
|
||||
Call controller to get tasks and emit signal when finished.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
folder_id (str): Folder id.
|
||||
"""
|
||||
|
||||
refresh_finished = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, controller, folder_id):
|
||||
super(RefreshThread, self).__init__()
|
||||
self._id = uuid.uuid4().hex
|
||||
self._controller = controller
|
||||
self._folder_id = folder_id
|
||||
self._result = None
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def run(self):
|
||||
self._result = self._controller.get_task_items(
|
||||
self._folder_id, SENDER_NAME)
|
||||
self.refresh_finished.emit(self.id)
|
||||
|
||||
def get_result(self):
|
||||
return self._result
|
||||
|
||||
|
||||
class TasksModel(QtGui.QStandardItemModel):
|
||||
"""Tasks model which cares about refresh of tasks by folder id.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
"""
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller):
|
||||
super(TasksModel, self).__init__()
|
||||
|
||||
self._controller = controller
|
||||
|
||||
self._items_by_name = {}
|
||||
self._has_content = False
|
||||
self._is_refreshing = False
|
||||
|
||||
self._invalid_selection_item_used = False
|
||||
self._invalid_selection_item = None
|
||||
self._empty_tasks_item_used = False
|
||||
self._empty_tasks_item = None
|
||||
|
||||
self._last_folder_id = None
|
||||
|
||||
self._refresh_threads = {}
|
||||
self._current_refresh_thread = None
|
||||
|
||||
# Initial state
|
||||
self._add_invalid_selection_item()
|
||||
|
||||
def clear(self):
|
||||
self._items_by_name = {}
|
||||
self._has_content = False
|
||||
self._remove_invalid_items()
|
||||
super(TasksModel, self).clear()
|
||||
|
||||
def refresh(self, folder_id):
|
||||
"""Refresh tasks for folder.
|
||||
|
||||
Args:
|
||||
folder_id (Union[str, None]): Folder id.
|
||||
"""
|
||||
|
||||
self._refresh(folder_id)
|
||||
|
||||
def get_index_by_name(self, task_name):
|
||||
"""Find item by name and return its index.
|
||||
|
||||
Returns:
|
||||
QtCore.QModelIndex: Index of item. Is invalid if task is not
|
||||
found by name.
|
||||
"""
|
||||
|
||||
item = self._items_by_name.get(task_name)
|
||||
if item is None:
|
||||
return QtCore.QModelIndex()
|
||||
return self.indexFromItem(item)
|
||||
|
||||
def get_last_folder_id(self):
|
||||
"""Get last refreshed folder id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Folder id.
|
||||
"""
|
||||
|
||||
return self._last_folder_id
|
||||
|
||||
def _get_invalid_selection_item(self):
|
||||
if self._invalid_selection_item is None:
|
||||
item = QtGui.QStandardItem("Select a folder")
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
icon = qtawesome.icon(
|
||||
"fa.times",
|
||||
color=get_disabled_entity_icon_color()
|
||||
)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
self._invalid_selection_item = item
|
||||
return self._invalid_selection_item
|
||||
|
||||
def _get_empty_task_item(self):
|
||||
if self._empty_tasks_item is None:
|
||||
item = QtGui.QStandardItem("No task")
|
||||
icon = qtawesome.icon(
|
||||
"fa.exclamation-circle",
|
||||
color=get_disabled_entity_icon_color()
|
||||
)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
self._empty_tasks_item = item
|
||||
return self._empty_tasks_item
|
||||
|
||||
def _add_invalid_item(self, item):
|
||||
self.clear()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(item)
|
||||
|
||||
def _remove_invalid_item(self, item):
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.takeRow(item.row())
|
||||
|
||||
def _remove_invalid_items(self):
|
||||
self._remove_invalid_selection_item()
|
||||
self._remove_empty_task_item()
|
||||
|
||||
def _add_invalid_selection_item(self):
|
||||
if not self._invalid_selection_item_used:
|
||||
self._add_invalid_item(self._get_invalid_selection_item())
|
||||
self._invalid_selection_item_used = True
|
||||
|
||||
def _remove_invalid_selection_item(self):
|
||||
if self._invalid_selection_item:
|
||||
self._remove_invalid_item(self._get_invalid_selection_item())
|
||||
self._invalid_selection_item_used = False
|
||||
|
||||
def _add_empty_task_item(self):
|
||||
if not self._empty_tasks_item_used:
|
||||
self._add_invalid_item(self._get_empty_task_item())
|
||||
self._empty_tasks_item_used = True
|
||||
|
||||
def _remove_empty_task_item(self):
|
||||
if self._empty_tasks_item_used:
|
||||
self._remove_invalid_item(self._get_empty_task_item())
|
||||
self._empty_tasks_item_used = False
|
||||
|
||||
def _refresh(self, folder_id):
|
||||
self._is_refreshing = True
|
||||
self._last_folder_id = folder_id
|
||||
if not folder_id:
|
||||
self._add_invalid_selection_item()
|
||||
self._current_refresh_thread = None
|
||||
self._is_refreshing = False
|
||||
self.refreshed.emit()
|
||||
return
|
||||
|
||||
thread = RefreshThread(self._controller, folder_id)
|
||||
self._current_refresh_thread = thread.id
|
||||
self._refresh_threads[thread.id] = thread
|
||||
thread.refresh_finished.connect(self._on_refresh_thread)
|
||||
thread.start()
|
||||
|
||||
def _on_refresh_thread(self, thread_id):
|
||||
"""Callback when refresh thread is finished.
|
||||
|
||||
Technically can be running multiple refresh threads at the same time,
|
||||
to avoid using values from wrong thread, we check if thread id is
|
||||
current refresh thread id.
|
||||
|
||||
Tasks are stored by name, so if a folder has same task name as
|
||||
previously selected folder it keeps the selection.
|
||||
|
||||
Args:
|
||||
thread_id (str): Thread id.
|
||||
"""
|
||||
|
||||
thread = self._refresh_threads.pop(thread_id)
|
||||
if thread_id != self._current_refresh_thread:
|
||||
return
|
||||
|
||||
task_items = thread.get_result()
|
||||
# Task items are refreshed
|
||||
if task_items is None:
|
||||
return
|
||||
|
||||
# No tasks are available on folder
|
||||
if not task_items:
|
||||
self._add_empty_task_item()
|
||||
return
|
||||
self._remove_invalid_items()
|
||||
|
||||
new_items = []
|
||||
new_names = set()
|
||||
for task_item in task_items:
|
||||
name = task_item.name
|
||||
new_names.add(name)
|
||||
item = self._items_by_name.get(name)
|
||||
if item is None:
|
||||
item = QtGui.QStandardItem()
|
||||
item.setEditable(False)
|
||||
new_items.append(item)
|
||||
self._items_by_name[name] = item
|
||||
|
||||
# TODO cache locally
|
||||
icon = qtawesome.icon(
|
||||
task_item.icon_name,
|
||||
color=task_item.icon_color,
|
||||
)
|
||||
item.setData(task_item.label, QtCore.Qt.DisplayRole)
|
||||
item.setData(name, ITEM_NAME_ROLE)
|
||||
item.setData(task_item.id, ITEM_ID_ROLE)
|
||||
item.setData(task_item.parent_id, PARENT_ID_ROLE)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
|
||||
for name in set(self._items_by_name) - new_names:
|
||||
item = self._items_by_name.pop(name)
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
if new_items:
|
||||
root_item.appendRows(new_items)
|
||||
|
||||
self._has_content = root_item.rowCount() > 0
|
||||
self._is_refreshing = False
|
||||
self.refreshed.emit()
|
||||
|
||||
@property
|
||||
def is_refreshing(self):
|
||||
"""Model is refreshing.
|
||||
|
||||
Returns:
|
||||
bool: Model is refreshing
|
||||
"""
|
||||
|
||||
return self._is_refreshing
|
||||
|
||||
@property
|
||||
def has_content(self):
|
||||
"""Model has content.
|
||||
|
||||
Returns:
|
||||
bools: Have at least one task.
|
||||
"""
|
||||
|
||||
return self._has_content
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
# Show nice labels in the header
|
||||
if (
|
||||
role == QtCore.Qt.DisplayRole
|
||||
and orientation == QtCore.Qt.Horizontal
|
||||
):
|
||||
if section == 0:
|
||||
return "Tasks"
|
||||
|
||||
return super(TasksModel, self).headerData(
|
||||
section, orientation, role
|
||||
)
|
||||
|
||||
|
||||
class TasksWidget(QtWidgets.QWidget):
|
||||
"""Tasks widget.
|
||||
|
||||
Widget that handles tasks view, model and selection.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): Workfiles controller.
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(TasksWidget, self).__init__(parent)
|
||||
|
||||
tasks_view = DeselectableTreeView(self)
|
||||
tasks_view.setIndentation(0)
|
||||
|
||||
tasks_model = TasksModel(controller)
|
||||
tasks_proxy_model = QtCore.QSortFilterProxyModel()
|
||||
tasks_proxy_model.setSourceModel(tasks_model)
|
||||
|
||||
tasks_view.setModel(tasks_proxy_model)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(tasks_view, 1)
|
||||
|
||||
controller.register_event_callback(
|
||||
"tasks.refresh.finished",
|
||||
self._on_tasks_refresh_finished
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"selection.folder.changed",
|
||||
self._folder_selection_changed
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"expected_selection_changed",
|
||||
self._on_expected_selection_change
|
||||
)
|
||||
|
||||
selection_model = tasks_view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self._on_selection_change)
|
||||
|
||||
tasks_model.refreshed.connect(self._on_tasks_model_refresh)
|
||||
|
||||
self._controller = controller
|
||||
self._tasks_view = tasks_view
|
||||
self._tasks_model = tasks_model
|
||||
self._tasks_proxy_model = tasks_proxy_model
|
||||
|
||||
self._selected_folder_id = None
|
||||
|
||||
self._expected_selection_data = None
|
||||
|
||||
def _clear(self):
|
||||
self._tasks_model.clear()
|
||||
|
||||
def _on_tasks_refresh_finished(self, event):
|
||||
"""Tasks were refreshed in controller.
|
||||
|
||||
Ignore if refresh was triggered by tasks model, or refreshed folder is
|
||||
not the same as currently selected folder.
|
||||
|
||||
Args:
|
||||
event (Event): Event object.
|
||||
"""
|
||||
|
||||
# Refresh only if current folder id is the same
|
||||
if (
|
||||
event["sender"] == SENDER_NAME
|
||||
or event["folder_id"] != self._selected_folder_id
|
||||
):
|
||||
return
|
||||
self._tasks_model.refresh(self._selected_folder_id)
|
||||
|
||||
def _folder_selection_changed(self, event):
|
||||
self._selected_folder_id = event["folder_id"]
|
||||
self._tasks_model.refresh(self._selected_folder_id)
|
||||
|
||||
def _on_tasks_model_refresh(self):
|
||||
if not self._set_expected_selection():
|
||||
self._on_selection_change()
|
||||
self._tasks_proxy_model.sort(0)
|
||||
|
||||
def _set_expected_selection(self):
|
||||
if self._expected_selection_data is None:
|
||||
return False
|
||||
folder_id = self._expected_selection_data["folder_id"]
|
||||
task_name = self._expected_selection_data["task_name"]
|
||||
self._expected_selection_data = None
|
||||
model_folder_id = self._tasks_model.get_last_folder_id()
|
||||
if folder_id != model_folder_id:
|
||||
return False
|
||||
if task_name is not None:
|
||||
index = self._tasks_model.get_index_by_name(task_name)
|
||||
if index.isValid():
|
||||
proxy_index = self._tasks_proxy_model.mapFromSource(index)
|
||||
self._tasks_view.setCurrentIndex(proxy_index)
|
||||
self._controller.expected_task_selected(folder_id, task_name)
|
||||
return True
|
||||
|
||||
def _on_expected_selection_change(self, event):
|
||||
if event["task_selected"] or not event["folder_selected"]:
|
||||
return
|
||||
|
||||
model_folder_id = self._tasks_model.get_last_folder_id()
|
||||
folder_id = event["folder_id"]
|
||||
self._expected_selection_data = {
|
||||
"task_name": event["task_name"],
|
||||
"folder_id": folder_id,
|
||||
}
|
||||
|
||||
if folder_id != model_folder_id or self._tasks_model.is_refreshing:
|
||||
return
|
||||
self._set_expected_selection()
|
||||
|
||||
def _get_selected_item_ids(self):
|
||||
selection_model = self._tasks_view.selectionModel()
|
||||
for index in selection_model.selectedIndexes():
|
||||
task_id = index.data(ITEM_ID_ROLE)
|
||||
task_name = index.data(ITEM_NAME_ROLE)
|
||||
parent_id = index.data(PARENT_ID_ROLE)
|
||||
if task_name is not None:
|
||||
return parent_id, task_id, task_name
|
||||
return self._selected_folder_id, None, None
|
||||
|
||||
def _on_selection_change(self):
|
||||
# Don't trigger task change during refresh
|
||||
# - a task was deselected if that happens
|
||||
# - can cause crash triggered during tasks refreshing
|
||||
if self._tasks_model.is_refreshing:
|
||||
return
|
||||
parent_id, task_id, task_name = self._get_selected_item_ids()
|
||||
self._controller.set_selected_task(parent_id, task_id, task_name)
|
||||
94
openpype/tools/ayon_workfiles/widgets/utils.py
Normal file
94
openpype/tools/ayon_workfiles/widgets/utils.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from qtpy import QtWidgets, QtCore
|
||||
from openpype.tools.flickcharm import FlickCharm
|
||||
|
||||
|
||||
class TreeView(QtWidgets.QTreeView):
|
||||
"""Ultimate TreeView with flick charm and double click signals.
|
||||
|
||||
Tree view have deselectable mode, which allows to deselect items by
|
||||
clicking on item area without any items.
|
||||
|
||||
Todos:
|
||||
Add to tools utils.
|
||||
"""
|
||||
|
||||
double_clicked_left = QtCore.Signal()
|
||||
double_clicked_right = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TreeView, self).__init__(*args, **kwargs)
|
||||
self._deselectable = False
|
||||
|
||||
self._flick_charm_activated = False
|
||||
self._flick_charm = FlickCharm(parent=self)
|
||||
self._before_flick_scroll_mode = None
|
||||
|
||||
def is_deselectable(self):
|
||||
return self._deselectable
|
||||
|
||||
def set_deselectable(self, deselectable):
|
||||
self._deselectable = deselectable
|
||||
|
||||
deselectable = property(is_deselectable, set_deselectable)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if self._deselectable:
|
||||
index = self.indexAt(event.pos())
|
||||
if not index.isValid():
|
||||
# clear the selection
|
||||
self.clearSelection()
|
||||
# clear the current index
|
||||
self.setCurrentIndex(QtCore.QModelIndex())
|
||||
super(TreeView, self).mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.double_clicked_left.emit()
|
||||
|
||||
elif event.button() == QtCore.Qt.RightButton:
|
||||
self.double_clicked_right.emit()
|
||||
|
||||
return super(TreeView, self).mouseDoubleClickEvent(event)
|
||||
|
||||
def activate_flick_charm(self):
|
||||
if self._flick_charm_activated:
|
||||
return
|
||||
self._flick_charm_activated = True
|
||||
self._before_flick_scroll_mode = self.verticalScrollMode()
|
||||
self._flick_charm.activateOn(self)
|
||||
self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
|
||||
|
||||
def deactivate_flick_charm(self):
|
||||
if not self._flick_charm_activated:
|
||||
return
|
||||
self._flick_charm_activated = False
|
||||
self._flick_charm.deactivateFrom(self)
|
||||
if self._before_flick_scroll_mode is not None:
|
||||
self.setVerticalScrollMode(self._before_flick_scroll_mode)
|
||||
|
||||
|
||||
class BaseOverlayFrame(QtWidgets.QFrame):
|
||||
"""Base frame for overlay widgets.
|
||||
|
||||
Has implemented automated resize and event filtering.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super(BaseOverlayFrame, self).__init__(parent)
|
||||
self.setObjectName("OverlayFrame")
|
||||
|
||||
self._parent = parent
|
||||
|
||||
def setVisible(self, visible):
|
||||
super(BaseOverlayFrame, self).setVisible(visible)
|
||||
if visible:
|
||||
self._parent.installEventFilter(self)
|
||||
self.resize(self._parent.size())
|
||||
else:
|
||||
self._parent.removeEventFilter(self)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QtCore.QEvent.Resize:
|
||||
self.resize(obj.size())
|
||||
|
||||
return super(BaseOverlayFrame, self).eventFilter(obj, event)
|
||||
400
openpype/tools/ayon_workfiles/widgets/window.py
Normal file
400
openpype/tools/ayon_workfiles/widgets/window.py
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
from qtpy import QtCore, QtWidgets, QtGui
|
||||
|
||||
from openpype import style, resources
|
||||
from openpype.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
MessageOverlayObject,
|
||||
)
|
||||
from openpype.tools.utils.lib import get_qta_icon_by_name_and_color
|
||||
|
||||
from openpype.tools.ayon_workfiles.control import BaseWorkfileController
|
||||
|
||||
from .side_panel import SidePanelWidget
|
||||
from .folders_widget import FoldersWidget
|
||||
from .tasks_widget import TasksWidget
|
||||
from .files_widget import FilesWidget
|
||||
from .utils import BaseOverlayFrame
|
||||
|
||||
|
||||
# TODO move to utils
|
||||
# from openpype.tools.utils.lib import (
|
||||
# get_refresh_icon, get_go_to_current_icon)
|
||||
def get_refresh_icon():
|
||||
return get_qta_icon_by_name_and_color(
|
||||
"fa.refresh", style.get_default_tools_icon_color()
|
||||
)
|
||||
|
||||
|
||||
def get_go_to_current_icon():
|
||||
return get_qta_icon_by_name_and_color(
|
||||
"fa.arrow-down", style.get_default_tools_icon_color()
|
||||
)
|
||||
|
||||
|
||||
class InvalidHostOverlay(BaseOverlayFrame):
|
||||
def __init__(self, parent):
|
||||
super(InvalidHostOverlay, self).__init__(parent)
|
||||
|
||||
label_widget = QtWidgets.QLabel(
|
||||
(
|
||||
"Workfiles tool is not supported in this host/DCCs."
|
||||
"<br/><br/>This may be caused by a bug."
|
||||
" Please contact your TD for more information."
|
||||
),
|
||||
self
|
||||
)
|
||||
label_widget.setAlignment(QtCore.Qt.AlignCenter)
|
||||
label_widget.setObjectName("OverlayFrameLabel")
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addStretch(2)
|
||||
layout.addWidget(label_widget, 0, QtCore.Qt.AlignCenter)
|
||||
layout.addStretch(3)
|
||||
|
||||
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
|
||||
class WorkfilesToolWindow(QtWidgets.QWidget):
|
||||
"""WorkFiles Window.
|
||||
|
||||
Main windows of workfiles tool.
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): Frontend controller.
|
||||
parent (Optional[QtWidgets.QWidget]): Parent widget.
|
||||
"""
|
||||
|
||||
title = "Work Files"
|
||||
|
||||
def __init__(self, controller=None, parent=None):
|
||||
super(WorkfilesToolWindow, self).__init__(parent=parent)
|
||||
|
||||
if controller is None:
|
||||
controller = BaseWorkfileController()
|
||||
|
||||
self.setWindowTitle(self.title)
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
flags = self.windowFlags() | QtCore.Qt.Window
|
||||
self.setWindowFlags(flags)
|
||||
|
||||
self._default_window_flags = flags
|
||||
|
||||
self._folder_widget = None
|
||||
self._folder_filter_input = None
|
||||
|
||||
self._files_widget = None
|
||||
|
||||
self._first_show = True
|
||||
self._controller_refreshed = False
|
||||
self._context_to_set = None
|
||||
# Host validation should happen only once
|
||||
self._host_is_valid = None
|
||||
|
||||
self._controller = controller
|
||||
|
||||
# Create pages widget and set it as central widget
|
||||
pages_widget = QtWidgets.QStackedWidget(self)
|
||||
|
||||
home_page_widget = QtWidgets.QWidget(pages_widget)
|
||||
home_body_widget = QtWidgets.QWidget(home_page_widget)
|
||||
|
||||
col_1_widget = self._create_col_1_widget(controller, parent)
|
||||
tasks_widget = TasksWidget(controller, home_body_widget)
|
||||
col_3_widget = self._create_col_3_widget(controller, home_body_widget)
|
||||
side_panel = SidePanelWidget(controller, home_body_widget)
|
||||
|
||||
pages_widget.addWidget(home_page_widget)
|
||||
|
||||
# Build home
|
||||
home_page_layout = QtWidgets.QVBoxLayout(home_page_widget)
|
||||
home_page_layout.addWidget(home_body_widget)
|
||||
|
||||
# Build home - body
|
||||
body_layout = QtWidgets.QVBoxLayout(home_body_widget)
|
||||
split_widget = QtWidgets.QSplitter(home_body_widget)
|
||||
split_widget.addWidget(col_1_widget)
|
||||
split_widget.addWidget(tasks_widget)
|
||||
split_widget.addWidget(col_3_widget)
|
||||
split_widget.addWidget(side_panel)
|
||||
split_widget.setSizes([255, 160, 455, 175])
|
||||
|
||||
body_layout.addWidget(split_widget)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.addWidget(pages_widget, 1)
|
||||
|
||||
overlay_messages_widget = MessageOverlayObject(self)
|
||||
overlay_invalid_host = InvalidHostOverlay(self)
|
||||
overlay_invalid_host.setVisible(False)
|
||||
|
||||
first_show_timer = QtCore.QTimer()
|
||||
first_show_timer.setSingleShot(True)
|
||||
first_show_timer.setInterval(50)
|
||||
|
||||
first_show_timer.timeout.connect(self._on_first_show)
|
||||
|
||||
controller.register_event_callback(
|
||||
"save_as.finished",
|
||||
self._on_save_as_finished,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"copy_representation.finished",
|
||||
self._on_copy_representation_finished,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"workfile_duplicate.finished",
|
||||
self._on_duplicate_finished
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"open_workfile.finished",
|
||||
self._on_open_finished
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"controller.refresh.started",
|
||||
self._on_controller_refresh_started,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"controller.refresh.finished",
|
||||
self._on_controller_refresh_finished,
|
||||
)
|
||||
|
||||
self._overlay_messages_widget = overlay_messages_widget
|
||||
self._overlay_invalid_host = overlay_invalid_host
|
||||
self._home_page_widget = home_page_widget
|
||||
self._pages_widget = pages_widget
|
||||
self._home_body_widget = home_body_widget
|
||||
self._split_widget = split_widget
|
||||
|
||||
self._tasks_widget = tasks_widget
|
||||
self._side_panel = side_panel
|
||||
|
||||
self._first_show_timer = first_show_timer
|
||||
|
||||
self._post_init()
|
||||
|
||||
def _post_init(self):
|
||||
self._on_published_checkbox_changed()
|
||||
|
||||
# Force focus on the open button by default, required for Houdini.
|
||||
self._files_widget.setFocus()
|
||||
|
||||
self.resize(1200, 600)
|
||||
|
||||
def _create_col_1_widget(self, controller, parent):
|
||||
col_widget = QtWidgets.QWidget(parent)
|
||||
header_widget = QtWidgets.QWidget(col_widget)
|
||||
|
||||
folder_filter_input = PlaceholderLineEdit(header_widget)
|
||||
folder_filter_input.setPlaceholderText("Filter folders..")
|
||||
|
||||
go_to_current_btn = QtWidgets.QPushButton(header_widget)
|
||||
go_to_current_btn.setIcon(get_go_to_current_icon())
|
||||
go_to_current_btn_sp = go_to_current_btn.sizePolicy()
|
||||
go_to_current_btn_sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum)
|
||||
go_to_current_btn.setSizePolicy(go_to_current_btn_sp)
|
||||
|
||||
refresh_btn = QtWidgets.QPushButton(header_widget)
|
||||
refresh_btn.setIcon(get_refresh_icon())
|
||||
refresh_btn_sp = refresh_btn.sizePolicy()
|
||||
refresh_btn_sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum)
|
||||
refresh_btn.setSizePolicy(refresh_btn_sp)
|
||||
|
||||
folder_widget = FoldersWidget(controller, col_widget)
|
||||
|
||||
header_layout = QtWidgets.QHBoxLayout(header_widget)
|
||||
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||
header_layout.addWidget(folder_filter_input, 1)
|
||||
header_layout.addWidget(go_to_current_btn, 0)
|
||||
header_layout.addWidget(refresh_btn, 0)
|
||||
|
||||
col_layout = QtWidgets.QVBoxLayout(col_widget)
|
||||
col_layout.setContentsMargins(0, 0, 0, 0)
|
||||
col_layout.addWidget(header_widget, 0)
|
||||
col_layout.addWidget(folder_widget, 1)
|
||||
|
||||
folder_filter_input.textChanged.connect(self._on_folder_filter_change)
|
||||
go_to_current_btn.clicked.connect(self._on_go_to_current_clicked)
|
||||
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
||||
|
||||
self._folder_filter_input = folder_filter_input
|
||||
self._folder_widget = folder_widget
|
||||
|
||||
return col_widget
|
||||
|
||||
def _create_col_3_widget(self, controller, parent):
|
||||
col_widget = QtWidgets.QWidget(parent)
|
||||
|
||||
header_widget = QtWidgets.QWidget(col_widget)
|
||||
|
||||
files_filter_input = PlaceholderLineEdit(header_widget)
|
||||
files_filter_input.setPlaceholderText("Filter files..")
|
||||
|
||||
published_checkbox = QtWidgets.QCheckBox("Published", header_widget)
|
||||
published_checkbox.setToolTip("Show published workfiles")
|
||||
|
||||
header_layout = QtWidgets.QHBoxLayout(header_widget)
|
||||
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||
header_layout.addWidget(files_filter_input, 1)
|
||||
header_layout.addWidget(published_checkbox, 0)
|
||||
|
||||
files_widget = FilesWidget(controller, col_widget)
|
||||
|
||||
col_layout = QtWidgets.QVBoxLayout(col_widget)
|
||||
col_layout.setContentsMargins(0, 0, 0, 0)
|
||||
col_layout.addWidget(header_widget, 0)
|
||||
col_layout.addWidget(files_widget, 1)
|
||||
|
||||
files_filter_input.textChanged.connect(
|
||||
self._on_file_text_filter_change)
|
||||
published_checkbox.stateChanged.connect(
|
||||
self._on_published_checkbox_changed
|
||||
)
|
||||
|
||||
self._files_filter_input = files_filter_input
|
||||
self._published_checkbox = published_checkbox
|
||||
|
||||
self._files_widget = files_widget
|
||||
|
||||
return col_widget
|
||||
|
||||
def set_window_on_top(self, on_top):
|
||||
"""Set window on top of other windows.
|
||||
|
||||
Args:
|
||||
on_top (bool): Show on top of other windows.
|
||||
"""
|
||||
|
||||
flags = self._default_window_flags
|
||||
if on_top:
|
||||
flags |= QtCore.Qt.WindowStaysOnTopHint
|
||||
if self.windowFlags() != flags:
|
||||
self.setWindowFlags(flags)
|
||||
|
||||
def ensure_visible(self, use_context=True, save=True, on_top=False):
|
||||
"""Ensure the window is visible.
|
||||
|
||||
This method expects arguments for compatibility with previous variant
|
||||
of Workfiles tool.
|
||||
|
||||
Args:
|
||||
use_context (Optional[bool]): DEPRECATED: This argument is
|
||||
ignored.
|
||||
save (Optional[bool]): Allow to save workfiles.
|
||||
on_top (Optional[bool]): Show on top of other windows.
|
||||
"""
|
||||
|
||||
save = True if save is None else save
|
||||
on_top = False if on_top is None else on_top
|
||||
|
||||
is_visible = self.isVisible()
|
||||
self._controller.set_save_enabled(save)
|
||||
self.set_window_on_top(on_top)
|
||||
|
||||
self.show()
|
||||
self.raise_()
|
||||
self.activateWindow()
|
||||
if is_visible:
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
"""Trigger refresh of workfiles tool controller."""
|
||||
|
||||
self._controller.refresh()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(WorkfilesToolWindow, self).showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self._first_show_timer.start()
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Custom keyPressEvent.
|
||||
|
||||
Override keyPressEvent to do nothing so that Maya's panels won't
|
||||
take focus when pressing "SHIFT" whilst mouse is over viewport or
|
||||
outliner. This way users don't accidentally perform Maya commands
|
||||
whilst trying to name an instance.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def _on_first_show(self):
|
||||
if not self._controller_refreshed:
|
||||
self.refresh()
|
||||
|
||||
def _on_file_text_filter_change(self, text):
|
||||
self._files_widget.set_text_filter(text)
|
||||
|
||||
def _on_published_checkbox_changed(self):
|
||||
"""Publish mode changed.
|
||||
|
||||
Tell children widgets about it so they can handle the mode.
|
||||
"""
|
||||
|
||||
published_mode = self._published_checkbox.isChecked()
|
||||
self._files_widget.set_published_mode(published_mode)
|
||||
self._side_panel.set_published_mode(published_mode)
|
||||
|
||||
def _on_folder_filter_change(self, text):
|
||||
self._folder_widget.set_name_filer(text)
|
||||
|
||||
def _on_go_to_current_clicked(self):
|
||||
self._controller.go_to_current_context()
|
||||
|
||||
def _on_refresh_clicked(self):
|
||||
self.refresh()
|
||||
|
||||
def _on_controller_refresh_started(self):
|
||||
self._controller_refreshed = True
|
||||
|
||||
def _on_controller_refresh_finished(self):
|
||||
if self._host_is_valid is None:
|
||||
self._host_is_valid = self._controller.is_host_valid()
|
||||
self._overlay_invalid_host.setVisible(not self._host_is_valid)
|
||||
|
||||
if not self._host_is_valid:
|
||||
return
|
||||
|
||||
def _on_save_as_finished(self, event):
|
||||
if event["failed"]:
|
||||
self._overlay_messages_widget.add_message(
|
||||
"Failed to save workfile",
|
||||
"error",
|
||||
)
|
||||
else:
|
||||
self._overlay_messages_widget.add_message(
|
||||
"Workfile saved"
|
||||
)
|
||||
|
||||
def _on_copy_representation_finished(self, event):
|
||||
if event["failed"]:
|
||||
self._overlay_messages_widget.add_message(
|
||||
"Failed to copy published workfile",
|
||||
"error",
|
||||
)
|
||||
else:
|
||||
self._overlay_messages_widget.add_message(
|
||||
"Publish workfile saved"
|
||||
)
|
||||
|
||||
def _on_duplicate_finished(self, event):
|
||||
if event["failed"]:
|
||||
self._overlay_messages_widget.add_message(
|
||||
"Failed to duplicate workfile",
|
||||
"error",
|
||||
)
|
||||
else:
|
||||
self._overlay_messages_widget.add_message(
|
||||
"Workfile duplicated"
|
||||
)
|
||||
|
||||
def _on_open_finished(self, event):
|
||||
if event["failed"]:
|
||||
self._overlay_messages_widget.add_message(
|
||||
"Failed to open workfile",
|
||||
"error",
|
||||
)
|
||||
else:
|
||||
self.close()
|
||||
|
|
@ -31,7 +31,6 @@ class ScreenMarquee(QtWidgets.QDialog):
|
|||
fade_anim.setEndValue(50)
|
||||
fade_anim.setDuration(200)
|
||||
fade_anim.setEasingCurve(QtCore.QEasingCurve.OutCubic)
|
||||
fade_anim.start(QtCore.QAbstractAnimation.DeleteWhenStopped)
|
||||
|
||||
fade_anim.valueChanged.connect(self._on_fade_anim)
|
||||
|
||||
|
|
@ -46,7 +45,7 @@ class ScreenMarquee(QtWidgets.QDialog):
|
|||
for screen in QtWidgets.QApplication.screens():
|
||||
screen.geometryChanged.connect(self._fit_screen_geometry)
|
||||
|
||||
self._opacity = fade_anim.currentValue()
|
||||
self._opacity = fade_anim.startValue()
|
||||
self._click_pos = None
|
||||
self._capture_rect = None
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ use singleton approach with global functions (using helper anyway).
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.host import IWorkfileHost, ILoadHost
|
||||
from openpype.lib import Logger
|
||||
from openpype.pipeline import (
|
||||
|
|
@ -46,17 +48,29 @@ class HostToolsHelper:
|
|||
self._log = Logger.get_logger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
def _init_ayon_workfiles_tool(self, parent):
|
||||
from openpype.tools.ayon_workfiles.widgets import WorkfilesToolWindow
|
||||
|
||||
workfiles_window = WorkfilesToolWindow(parent=parent)
|
||||
self._workfiles_tool = workfiles_window
|
||||
|
||||
def _init_openpype_workfiles_tool(self, parent):
|
||||
from openpype.tools.workfiles.app import Window
|
||||
|
||||
# Host validation
|
||||
host = registered_host()
|
||||
IWorkfileHost.validate_workfile_methods(host)
|
||||
|
||||
workfiles_window = Window(parent=parent)
|
||||
self._workfiles_tool = workfiles_window
|
||||
|
||||
def get_workfiles_tool(self, parent):
|
||||
"""Create, cache and return workfiles tool window."""
|
||||
if self._workfiles_tool is None:
|
||||
from openpype.tools.workfiles.app import Window
|
||||
|
||||
# Host validation
|
||||
host = registered_host()
|
||||
IWorkfileHost.validate_workfile_methods(host)
|
||||
|
||||
workfiles_window = Window(parent=parent)
|
||||
self._workfiles_tool = workfiles_window
|
||||
if AYON_SERVER_ENABLED:
|
||||
self._init_ayon_workfiles_tool(parent)
|
||||
else:
|
||||
self._init_openpype_workfiles_tool(parent)
|
||||
|
||||
return self._workfiles_tool
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ class ValidateWorkfilePathsModel(BaseSettingsModel):
|
|||
)
|
||||
|
||||
|
||||
class ValidateContainersModel(BaseSettingsModel):
|
||||
class BasicValidateModel(BaseSettingsModel):
|
||||
enabled: bool = Field(title="Enabled")
|
||||
optional: bool = Field(title="Optional")
|
||||
active: bool = Field(title="Active")
|
||||
|
|
@ -130,8 +130,11 @@ class PublishPluginsModel(BaseSettingsModel):
|
|||
ValidateWorkfilePaths: ValidateWorkfilePathsModel = Field(
|
||||
default_factory=ValidateWorkfilePathsModel,
|
||||
title="Validate workfile paths settings.")
|
||||
ValidateContainers: ValidateContainersModel = Field(
|
||||
default_factory=ValidateContainersModel,
|
||||
ValidateReviewColorspace: BasicValidateModel = Field(
|
||||
default_factory=BasicValidateModel,
|
||||
title="Validate Review Colorspace.")
|
||||
ValidateContainers: BasicValidateModel = Field(
|
||||
default_factory=BasicValidateModel,
|
||||
title="Validate Latest Containers.")
|
||||
|
||||
|
||||
|
|
@ -148,6 +151,11 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = {
|
|||
"$JOB"
|
||||
]
|
||||
},
|
||||
"ValidateReviewColorspace": {
|
||||
"enabled": True,
|
||||
"optional": True,
|
||||
"active": True
|
||||
},
|
||||
"ValidateContainers": {
|
||||
"enabled": True,
|
||||
"optional": True,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.1"
|
||||
__version__ = "0.1.2"
|
||||
|
|
|
|||
|
|
@ -28,10 +28,9 @@ class TestPipelineColorspace(TestPipeline):
|
|||
cd to OpenPype repo root dir
|
||||
poetry run python ./start.py runtests ../tests/unit/openpype/pipeline
|
||||
"""
|
||||
|
||||
TEST_FILES = [
|
||||
(
|
||||
"1Lf-mFxev7xiwZCWfImlRcw7Fj8XgNQMh",
|
||||
"1csqimz8bbNcNgxtEXklLz6GRv91D3KgA",
|
||||
"test_pipeline_colorspace.zip",
|
||||
""
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue