Merge branch 'develop' into feature/OP-3845_nuke-convert-to-new-publisher

This commit is contained in:
Jakub Jezek 2022-11-10 17:48:07 +01:00
commit 175e7090d0
No known key found for this signature in database
GPG key ID: 730D7C02726179A7
117 changed files with 2477 additions and 857 deletions

View file

@ -1,8 +1,43 @@
# Changelog
## [3.14.5](https://github.com/pypeclub/OpenPype/tree/HEAD)
## [3.14.6](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...HEAD)
### 📖 Documentation
- Documentation: Minor updates to dev\_requirements.md [\#4025](https://github.com/pypeclub/OpenPype/pull/4025)
**🆕 New features**
- Nuke: add 13.2 variant [\#4041](https://github.com/pypeclub/OpenPype/pull/4041)
**🚀 Enhancements**
- Publish Report Viewer: Store reports locally on machine [\#4040](https://github.com/pypeclub/OpenPype/pull/4040)
- General: More specific error in burnins script [\#4026](https://github.com/pypeclub/OpenPype/pull/4026)
- General: Extract review does not crash with old settings overrides [\#4023](https://github.com/pypeclub/OpenPype/pull/4023)
- Publisher: Convertors for legacy instances [\#4020](https://github.com/pypeclub/OpenPype/pull/4020)
- workflows: adding milestone creator and assigner [\#4018](https://github.com/pypeclub/OpenPype/pull/4018)
- Publisher: Catch creator errors [\#4015](https://github.com/pypeclub/OpenPype/pull/4015)
**🐛 Bug fixes**
- Hiero - effect collection fixes [\#4038](https://github.com/pypeclub/OpenPype/pull/4038)
- Nuke - loader clip correct hash conversion in path [\#4037](https://github.com/pypeclub/OpenPype/pull/4037)
- Maya: Soft fail when applying capture preset [\#4034](https://github.com/pypeclub/OpenPype/pull/4034)
- Igniter: handle missing directory [\#4032](https://github.com/pypeclub/OpenPype/pull/4032)
- StandalonePublisher: Fix thumbnail publishing [\#4029](https://github.com/pypeclub/OpenPype/pull/4029)
- Experimental Tools: Fix publisher import [\#4027](https://github.com/pypeclub/OpenPype/pull/4027)
- Houdini: fix wrong path in ASS loader [\#4016](https://github.com/pypeclub/OpenPype/pull/4016)
**🔀 Refactored code**
- General: Import lib functions from lib [\#4017](https://github.com/pypeclub/OpenPype/pull/4017)
## [3.14.5](https://github.com/pypeclub/OpenPype/tree/3.14.5) (2022-10-24)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...3.14.5)
**🚀 Enhancements**

View file

@ -1,5 +1,128 @@
# Changelog
## [3.14.5](https://github.com/pypeclub/OpenPype/tree/3.14.5) (2022-10-24)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...3.14.5)
**🚀 Enhancements**
- Maya: add OBJ extractor to model family [\#4021](https://github.com/pypeclub/OpenPype/pull/4021)
- Publish report viewer tool [\#4010](https://github.com/pypeclub/OpenPype/pull/4010)
- Nuke | Global: adding custom tags representation filtering [\#4009](https://github.com/pypeclub/OpenPype/pull/4009)
- Publisher: Create context has shared data for collection phase [\#3995](https://github.com/pypeclub/OpenPype/pull/3995)
- Resolve: updating to v18 compatibility [\#3986](https://github.com/pypeclub/OpenPype/pull/3986)
**🐛 Bug fixes**
- TrayPublisher: Fix missing argument [\#4019](https://github.com/pypeclub/OpenPype/pull/4019)
- General: Fix python 2 compatibility of ffmpeg and oiio tools discovery [\#4011](https://github.com/pypeclub/OpenPype/pull/4011)
**🔀 Refactored code**
- Maya: Removed unused imports [\#4008](https://github.com/pypeclub/OpenPype/pull/4008)
- Unreal: Fix import of moved function [\#4007](https://github.com/pypeclub/OpenPype/pull/4007)
- Houdini: Change import of RepairAction [\#4005](https://github.com/pypeclub/OpenPype/pull/4005)
- Nuke/Hiero: Refactor openpype.api imports [\#4000](https://github.com/pypeclub/OpenPype/pull/4000)
- TVPaint: Defined with HostBase [\#3994](https://github.com/pypeclub/OpenPype/pull/3994)
**Merged pull requests:**
- Unreal: Remove redundant Creator stub [\#4012](https://github.com/pypeclub/OpenPype/pull/4012)
- Unreal: add `uproject` extension to Unreal project template [\#4004](https://github.com/pypeclub/OpenPype/pull/4004)
- Unreal: fix order of includes [\#4002](https://github.com/pypeclub/OpenPype/pull/4002)
- Fusion: Implement backwards compatibility \(+/- Fusion 17.2\) [\#3958](https://github.com/pypeclub/OpenPype/pull/3958)
## [3.14.4](https://github.com/pypeclub/OpenPype/tree/3.14.4) (2022-10-19)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...3.14.4)
**🆕 New features**
- Webpublisher: use max next published version number for all items in batch [\#3961](https://github.com/pypeclub/OpenPype/pull/3961)
- General: Control Thumbnail integration via explicit configuration profiles [\#3951](https://github.com/pypeclub/OpenPype/pull/3951)
**🚀 Enhancements**
- Publisher: Multiselection in card view [\#3993](https://github.com/pypeclub/OpenPype/pull/3993)
- TrayPublisher: Original Basename cause crash too early [\#3990](https://github.com/pypeclub/OpenPype/pull/3990)
- Tray Publisher: add `originalBasename` data to simple creators [\#3988](https://github.com/pypeclub/OpenPype/pull/3988)
- General: Custom paths to ffmpeg and OpenImageIO tools [\#3982](https://github.com/pypeclub/OpenPype/pull/3982)
- Integrate: Preserve existing subset group if instance does not set it for new version [\#3976](https://github.com/pypeclub/OpenPype/pull/3976)
- Publisher: Prepare publisher controller for remote publishing [\#3972](https://github.com/pypeclub/OpenPype/pull/3972)
- Maya: new style dataclasses in maya deadline submitter plugin [\#3968](https://github.com/pypeclub/OpenPype/pull/3968)
- Maya: Define preffered Qt bindings for Qt.py and qtpy [\#3963](https://github.com/pypeclub/OpenPype/pull/3963)
- Settings: Move imageio from project anatomy to project settings \[pypeclub\] [\#3959](https://github.com/pypeclub/OpenPype/pull/3959)
- TrayPublisher: Extract thumbnail for other families [\#3952](https://github.com/pypeclub/OpenPype/pull/3952)
- Publisher: Pass instance to subset name method on update [\#3949](https://github.com/pypeclub/OpenPype/pull/3949)
- General: Set root environments before DCC launch [\#3947](https://github.com/pypeclub/OpenPype/pull/3947)
- Refactor: changed legacy way to update database for Hero version integrate [\#3941](https://github.com/pypeclub/OpenPype/pull/3941)
- Maya: Moved plugin from global to maya [\#3939](https://github.com/pypeclub/OpenPype/pull/3939)
- Publisher: Create dialog is part of main window [\#3936](https://github.com/pypeclub/OpenPype/pull/3936)
- Fusion: Implement Alembic and FBX mesh loader [\#3927](https://github.com/pypeclub/OpenPype/pull/3927)
**🐛 Bug fixes**
- TrayPublisher: Disable sequences in batch mov creator [\#3996](https://github.com/pypeclub/OpenPype/pull/3996)
- Fix - tags might be missing on representation [\#3985](https://github.com/pypeclub/OpenPype/pull/3985)
- Resolve: Fix usage of functions from lib [\#3983](https://github.com/pypeclub/OpenPype/pull/3983)
- Maya: remove invalid prefix token for non-multipart outputs [\#3981](https://github.com/pypeclub/OpenPype/pull/3981)
- Ftrack: Fix schema cache for Python 2 [\#3980](https://github.com/pypeclub/OpenPype/pull/3980)
- Maya: add object to attr.s declaration [\#3973](https://github.com/pypeclub/OpenPype/pull/3973)
- Maya: Deadline OutputFilePath hack regression for Renderman [\#3950](https://github.com/pypeclub/OpenPype/pull/3950)
- Houdini: Fix validate workfile paths for non-parm file references [\#3948](https://github.com/pypeclub/OpenPype/pull/3948)
- Photoshop: missed sync published version of workfile with workfile [\#3946](https://github.com/pypeclub/OpenPype/pull/3946)
- Maya: Set default value for RenderSetupIncludeLights option [\#3944](https://github.com/pypeclub/OpenPype/pull/3944)
- Maya: fix regression of Renderman Deadline hack [\#3943](https://github.com/pypeclub/OpenPype/pull/3943)
- Kitsu: 2 fixes, nb\_frames and Shot type error [\#3940](https://github.com/pypeclub/OpenPype/pull/3940)
- Tray: Change order of attribute changes [\#3938](https://github.com/pypeclub/OpenPype/pull/3938)
- AttributeDefs: Fix crashing multivalue of files widget [\#3937](https://github.com/pypeclub/OpenPype/pull/3937)
- General: Fix links query on hero version [\#3900](https://github.com/pypeclub/OpenPype/pull/3900)
- Publisher: Files Drag n Drop cleanup [\#3888](https://github.com/pypeclub/OpenPype/pull/3888)
**🔀 Refactored code**
- Flame: Import lib functions from lib [\#3992](https://github.com/pypeclub/OpenPype/pull/3992)
- General: Fix deprecated warning in legacy creator [\#3978](https://github.com/pypeclub/OpenPype/pull/3978)
- Blender: Remove openpype api imports [\#3977](https://github.com/pypeclub/OpenPype/pull/3977)
- General: Use direct import of resources [\#3964](https://github.com/pypeclub/OpenPype/pull/3964)
- General: Direct settings imports [\#3934](https://github.com/pypeclub/OpenPype/pull/3934)
- General: import 'Logger' from 'openpype.lib' [\#3926](https://github.com/pypeclub/OpenPype/pull/3926)
- General: Remove deprecated functions from lib [\#3907](https://github.com/pypeclub/OpenPype/pull/3907)
**Merged pull requests:**
- Maya + Yeti: Load Yeti Cache fix frame number recognition [\#3942](https://github.com/pypeclub/OpenPype/pull/3942)
- Fusion: Implement callbacks to Fusion's event system thread [\#3928](https://github.com/pypeclub/OpenPype/pull/3928)
- Photoshop: create single frame image in Ftrack as review [\#3908](https://github.com/pypeclub/OpenPype/pull/3908)
## [3.14.3](https://github.com/pypeclub/OpenPype/tree/3.14.3) (2022-10-03)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.2...3.14.3)
**🚀 Enhancements**
- Publisher: Enhancement proposals [\#3897](https://github.com/pypeclub/OpenPype/pull/3897)
**🐛 Bug fixes**
- Maya: Fix Render single camera validator [\#3929](https://github.com/pypeclub/OpenPype/pull/3929)
- Flame: loading multilayer exr to batch/reel is working [\#3901](https://github.com/pypeclub/OpenPype/pull/3901)
- Hiero: Fix inventory check on launch [\#3895](https://github.com/pypeclub/OpenPype/pull/3895)
- WebPublisher: Fix import after refactor [\#3891](https://github.com/pypeclub/OpenPype/pull/3891)
**🔀 Refactored code**
- Maya: Remove unused 'openpype.api' imports in plugins [\#3925](https://github.com/pypeclub/OpenPype/pull/3925)
- Resolve: Use new Extractor location [\#3918](https://github.com/pypeclub/OpenPype/pull/3918)
- Unreal: Use new Extractor location [\#3917](https://github.com/pypeclub/OpenPype/pull/3917)
- Flame: Use new Extractor location [\#3916](https://github.com/pypeclub/OpenPype/pull/3916)
- Houdini: Use new Extractor location [\#3894](https://github.com/pypeclub/OpenPype/pull/3894)
- Harmony: Use new Extractor location [\#3893](https://github.com/pypeclub/OpenPype/pull/3893)
**Merged pull requests:**
- Maya: Fix Scene Inventory possibly starting off-screen due to maya preferences [\#3923](https://github.com/pypeclub/OpenPype/pull/3923)
## [3.14.2](https://github.com/pypeclub/OpenPype/tree/3.14.2) (2022-09-12)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.1...3.14.2)

View file

@ -817,6 +817,13 @@ class BootstrapRepos:
except Exception as e:
self._print(str(e), LOG_ERROR, exc_info=True)
return None
if not destination_dir.exists():
destination_dir.mkdir(parents=True)
elif not destination_dir.is_dir():
self._print(
"Destination exists but is not directory.", LOG_ERROR)
return None
try:
shutil.move(zip_file.as_posix(), destination_dir.as_posix())
except shutil.Error as e:

View file

@ -1,5 +1,4 @@
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
class AfterEffectsAddon(OpenPypeModule, IHostAddon):

View file

@ -1,6 +1,5 @@
import os
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
BLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -1,6 +1,5 @@
import os
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
HOST_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -225,7 +225,8 @@ class FlameMenuUniversal(_FlameMenuApp):
menu['actions'].append({
"name": "Load...",
"execute": lambda x: self.tools_helper.show_loader()
"execute": lambda x: callback_selection(
x, self.tools_helper.show_loader)
})
menu['actions'].append({
"name": "Manage...",

View file

@ -1,3 +1,4 @@
from copy import deepcopy
import os
import flame
from pprint import pformat
@ -22,7 +23,7 @@ class LoadClipBatch(opfapi.ClipLoader):
# settings
reel_name = "OP_LoadedReel"
clip_name_template = "{asset}_{subset}<_{output}>"
clip_name_template = "{batch}_{asset}_{subset}<_{output}>"
def load(self, context, name, namespace, options):
@ -40,8 +41,11 @@ class LoadClipBatch(opfapi.ClipLoader):
if not context["representation"]["context"].get("output"):
self.clip_name_template.replace("output", "representation")
formating_data = deepcopy(context["representation"]["context"])
formating_data["batch"] = self.batch.name.get_value()
clip_name = StringTemplate(self.clip_name_template).format(
context["representation"]["context"])
formating_data)
# TODO: settings in imageio
# convert colorspace with ocio to flame mapping
@ -56,6 +60,7 @@ class LoadClipBatch(opfapi.ClipLoader):
openclip_path = os.path.join(
openclip_dir, clip_name + ".clip"
)
if not os.path.exists(openclip_dir):
os.makedirs(openclip_dir)

View file

@ -1,6 +1,5 @@
import os
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
FUSION_HOST_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -1,6 +1,5 @@
import os
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
HARMONY_HOST_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -1,7 +1,6 @@
import os
import platform
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
HIERO_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -170,7 +170,10 @@ class CreatorWidget(QtWidgets.QDialog):
for func, val in kwargs.items():
if getattr(item, func):
func_attr = getattr(item, func)
func_attr(val)
if isinstance(val, tuple):
func_attr(*val)
else:
func_attr(val)
# add to layout
layout.addRow(label, item)
@ -273,8 +276,8 @@ class CreatorWidget(QtWidgets.QDialog):
elif v["type"] == "QSpinBox":
data[k]["value"] = self.create_row(
content_layout, "QSpinBox", v["label"],
setValue=v["value"], setMinimum=0,
setMaximum=100000, setToolTip=tool_tip)
setRange=(1, 9999999), setValue=v["value"],
setToolTip=tool_tip)
return data

View file

@ -326,8 +326,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
return hiero_export.create_otio_time_range(
frame_start, frame_duration, fps)
@staticmethod
def collect_sub_track_items(tracks):
def collect_sub_track_items(self, tracks):
"""
Returns dictionary with track index as key and list of subtracks
"""
@ -336,8 +335,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
for track in tracks:
items = track.items()
effet_items = track.subTrackItems()
# skip if no clips on track > need track with effect only
if items:
if not effet_items:
continue
# skip all disabled tracks
@ -345,10 +346,11 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
continue
track_index = track.trackIndex()
_sub_track_items = phiero.flatten(track.subTrackItems())
_sub_track_items = phiero.flatten(effet_items)
_sub_track_items = list(_sub_track_items)
# continue only if any subtrack items are collected
if not list(_sub_track_items):
if not _sub_track_items:
continue
enabled_sti = []

View file

@ -1,6 +1,5 @@
import os
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
HOUDINI_HOST_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -1,11 +1,10 @@
import os
import re
import clique
from openpype.pipeline import (
load,
get_representation_path,
)
from openpype.hosts.houdini.api import pipeline
@ -20,7 +19,6 @@ class AssLoader(load.LoaderPlugin):
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
import hou
# Get the root node
@ -32,7 +30,11 @@ class AssLoader(load.LoaderPlugin):
# Create a new geo node
procedural = obj.createNode("arnold::procedural", node_name=node_name)
procedural.setParms({"ar_filename": self.get_path(self.fname)})
procedural.setParms(
{
"ar_filename": self.format_path(context["representation"])
})
nodes = [procedural]
self[:] = nodes
@ -46,62 +48,43 @@ class AssLoader(load.LoaderPlugin):
suffix="",
)
def get_path(self, path):
# Find all frames in the folder
ext = ".ass.gz" if path.endswith(".ass.gz") else ".ass"
folder = os.path.dirname(path)
frames = [f for f in os.listdir(folder) if f.endswith(ext)]
# Get the collection of frames to detect frame padding
patterns = [clique.PATTERNS["frames"]]
collections, remainder = clique.assemble(frames,
minimum_items=1,
patterns=patterns)
self.log.debug("Detected collections: {}".format(collections))
self.log.debug("Detected remainder: {}".format(remainder))
if not collections and remainder:
if len(remainder) != 1:
raise ValueError("Frames not correctly detected "
"in: {}".format(remainder))
# A single frame without frame range detected
filepath = remainder[0]
return os.path.normpath(filepath).replace("\\", "/")
# Frames detected with a valid "frame" number pattern
# Then we don't want to have any remainder files found
assert len(collections) == 1 and not remainder
collection = collections[0]
num_frames = len(collection.indexes)
if num_frames == 1:
# Return the input path without dynamic $F variable
result = path
else:
# More than a single frame detected - use $F{padding}
fname = "{}$F{}{}".format(collection.head,
collection.padding,
collection.tail)
result = os.path.join(folder, fname)
# Format file name, Houdini only wants forward slashes
return os.path.normpath(result).replace("\\", "/")
def update(self, container, representation):
# Update the file path
file_path = get_representation_path(representation)
file_path = file_path.replace("\\", "/")
procedural = container["node"]
procedural.setParms({"ar_filename": self.get_path(file_path)})
procedural.setParms({"ar_filename": self.format_path(representation)})
# Update attribute
procedural.setParms({"representation": str(representation["_id"])})
def remove(self, container):
node = container["node"]
node.destroy()
@staticmethod
def format_path(representation):
"""Format file path correctly for single ass.* or ass.* sequence.
Args:
representation (dict): representation to be loaded.
Returns:
str: Formatted path to be used by the input node.
"""
path = get_representation_path(representation)
if not os.path.exists(path):
raise RuntimeError("Path does not exist: {}".format(path))
is_sequence = bool(representation["context"].get("frame"))
# The path is either a single file or sequence in a folder.
if is_sequence:
dir_path, file_name = os.path.split(path)
path = os.path.join(
dir_path,
re.sub(r"(.*)\.(\d+)\.(ass.*)", "\\1.$F4.\\3", file_name)
)
return os.path.normpath(path).replace("\\", "/")
def switch(self, container, representation):
self.update(container, representation)

View file

@ -1,6 +1,5 @@
import os
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
MAYA_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -1532,7 +1532,7 @@ def get_container_members(container):
if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"):
continue
reference_members = cmds.referenceQuery(ref, nodes=True)
reference_members = cmds.referenceQuery(ref, nodes=True, dagPath=True)
reference_members = cmds.ls(reference_members,
long=True,
objectsOnly=True)

View file

@ -133,7 +133,7 @@ class ExtractPlayblast(publish.Extractor):
preset.update(panel_preset)
cmds.setFocus(panel)
path = capture.capture(**preset)
path = capture.capture(log=self.log, **preset)
self.log.debug("playblast path {}".format(path))

View file

@ -1,7 +1,6 @@
import os
import platform
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
NUKE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -1,6 +1,8 @@
import nuke
import qargparse
from pprint import pformat
from copy import deepcopy
from openpype.lib import Logger
from openpype.client import (
get_version_by_id,
get_last_version_by_subset_id,
@ -27,6 +29,7 @@ class LoadClip(plugin.NukeLoader):
Either it is image sequence or video file.
"""
log = Logger.get_logger(__name__)
families = [
"source",
@ -85,13 +88,18 @@ class LoadClip(plugin.NukeLoader):
)
def load(self, context, name, namespace, options):
repre = context["representation"]
representation = context["representation"]
# reste container id so it is always unique for each instance
self.reset_container_id()
is_sequence = len(repre["files"]) > 1
is_sequence = len(representation["files"]) > 1
file = self.fname.replace("\\", "/")
if is_sequence:
representation = self._representation_with_hash_in_frame(
representation
)
filepath = get_representation_path(representation).replace("\\", "/")
self.log.debug("_ filepath: {}".format(filepath))
start_at_workfile = options.get(
"start_at_workfile", self.options_defaults["start_at_workfile"])
@ -101,11 +109,10 @@ class LoadClip(plugin.NukeLoader):
version = context['version']
version_data = version.get("data", {})
repre_id = repre["_id"]
repre_id = representation["_id"]
repre_cont = repre["context"]
self.log.info("version_data: {}\n".format(version_data))
self.log.debug("_ version_data: {}\n".format(
pformat(version_data)))
self.log.debug(
"Representation id `{}` ".format(repre_id))
@ -121,23 +128,17 @@ class LoadClip(plugin.NukeLoader):
duration = last - first
first = 1
last = first + duration
elif "#" not in file:
frame = repre_cont.get("frame")
assert frame, "Representation is not sequence"
padding = len(frame)
file = file.replace(frame, "#" * padding)
# Fallback to asset name when namespace is None
if namespace is None:
namespace = context['asset']['name']
if not file:
if not filepath:
self.log.warning(
"Representation id `{}` is failing to load".format(repre_id))
return
read_name = self._get_node_name(repre)
read_name = self._get_node_name(representation)
# Create the Loader with the filename path set
read_node = nuke.createNode(
@ -147,10 +148,10 @@ class LoadClip(plugin.NukeLoader):
# to avoid multiple undo steps for rest of process
# we will switch off undo-ing
with viewer_update_and_undo_stop():
read_node["file"].setValue(file)
read_node["file"].setValue(filepath)
used_colorspace = self._set_colorspace(
read_node, version_data, repre["data"])
read_node, version_data, representation["data"])
self._set_range_to_node(read_node, first, last, start_at_workfile)
@ -172,7 +173,7 @@ class LoadClip(plugin.NukeLoader):
data_imprint[k] = version
elif k == 'colorspace':
colorspace = repre["data"].get(k)
colorspace = representation["data"].get(k)
colorspace = colorspace or version_data.get(k)
data_imprint["db_colorspace"] = colorspace
if used_colorspace:
@ -206,6 +207,20 @@ class LoadClip(plugin.NukeLoader):
def switch(self, container, representation):
self.update(container, representation)
def _representation_with_hash_in_frame(self, representation):
"""Convert frame key value to padded hash
Args:
representation (dict): representation data
Returns:
dict: altered representation data
"""
representation = deepcopy(representation)
frame = representation["context"]["frame"]
representation["context"]["frame"] = "#" * len(str(frame))
return representation
def update(self, container, representation):
"""Update the Loader's path
@ -218,7 +233,13 @@ class LoadClip(plugin.NukeLoader):
is_sequence = len(representation["files"]) > 1
read_node = nuke.toNode(container['objectName'])
file = get_representation_path(representation).replace("\\", "/")
if is_sequence:
representation = self._representation_with_hash_in_frame(
representation
)
filepath = get_representation_path(representation).replace("\\", "/")
self.log.debug("_ filepath: {}".format(filepath))
start_at_workfile = "start at" in read_node['frame_mode'].value()
@ -233,8 +254,6 @@ class LoadClip(plugin.NukeLoader):
version_data = version_doc.get("data", {})
repre_id = representation["_id"]
repre_cont = representation["context"]
# colorspace profile
colorspace = representation["data"].get("colorspace")
colorspace = colorspace or version_data.get("colorspace")
@ -251,14 +270,8 @@ class LoadClip(plugin.NukeLoader):
duration = last - first
first = 1
last = first + duration
elif "#" not in file:
frame = repre_cont.get("frame")
assert frame, "Representation is not sequence"
padding = len(frame)
file = file.replace(frame, "#" * padding)
if not file:
if not filepath:
self.log.warning(
"Representation id `{}` is failing to load".format(repre_id))
return
@ -266,14 +279,14 @@ class LoadClip(plugin.NukeLoader):
read_name = self._get_node_name(representation)
read_node["name"].setValue(read_name)
read_node["file"].setValue(file)
read_node["file"].setValue(filepath)
# to avoid multiple undo steps for rest of process
# we will switch off undo-ing
with viewer_update_and_undo_stop():
used_colorspace = self._set_colorspace(
read_node, version_data, representation["data"],
path=file)
path=filepath)
self._set_range_to_node(read_node, first, last, start_at_workfile)
@ -345,8 +358,10 @@ class LoadClip(plugin.NukeLoader):
time_warp_nodes = version_data.get('timewarps', [])
last_node = None
source_id = self.get_container_id(parent_node)
self.log.info("__ source_id: {}".format(source_id))
self.log.info("__ members: {}".format(self.get_members(parent_node)))
self.log.debug("__ source_id: {}".format(source_id))
self.log.debug("__ members: {}".format(
self.get_members(parent_node)))
dependent_nodes = self.clear_members(parent_node)
with maintained_selection():

View file

@ -1,6 +1,5 @@
import os
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
PHOTOSHOP_HOST_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -1,7 +1,6 @@
import os
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
from .utils import RESOLVE_ROOT_DIR

View file

@ -4,8 +4,7 @@ import click
from openpype.lib import get_openpype_execute_args
from openpype.lib.execute import run_detached_process
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import ITrayAction, IHostAddon
from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon
STANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -1,11 +1,11 @@
import os
import tempfile
import pyblish.api
import openpype.api
from openpype.lib import (
get_ffmpeg_tool_path,
get_ffprobe_streams,
path_to_subprocess_arg,
run_subprocess,
)
@ -96,7 +96,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
# run subprocess
self.log.debug("Executing: {}".format(subprocess_jpeg))
openpype.api.run_subprocess(
run_subprocess(
subprocess_jpeg, shell=True, logger=self.log
)
@ -118,6 +118,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
'files': filename,
"stagingDir": staging_dir,
"tags": ["thumbnail", "delete"],
"thumbnail": True
}
if width and height:
representation["width"] = width

View file

@ -4,8 +4,7 @@ import click
from openpype.lib import get_openpype_execute_args
from openpype.lib.execute import run_detached_process
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import ITrayAction, IHostAddon
from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon
TRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -1,49 +1,33 @@
from openpype.lib.attribute_definitions import FileDef
from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS
from openpype.pipeline.create import (
Creator,
HiddenCreator,
CreatedInstance
CreatedInstance,
cache_and_get_instances,
PRE_CREATE_THUMBNAIL_KEY,
)
from .pipeline import (
list_instances,
update_instances,
remove_instances,
HostContext,
)
from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS
REVIEW_EXTENSIONS = IMAGE_EXTENSIONS + VIDEO_EXTENSIONS
def _cache_and_get_instances(creator):
"""Cache instances in shared data.
Args:
creator (Creator): Plugin which would like to get instances from host.
Returns:
List[Dict[str, Any]]: Cached instances list from host implementation.
"""
shared_key = "openpype.traypublisher.instances"
if shared_key not in creator.collection_shared_data:
creator.collection_shared_data[shared_key] = list_instances()
return creator.collection_shared_data[shared_key]
REVIEW_EXTENSIONS = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS)
SHARED_DATA_KEY = "openpype.traypublisher.instances"
class HiddenTrayPublishCreator(HiddenCreator):
host_name = "traypublisher"
def collect_instances(self):
for instance_data in _cache_and_get_instances(self):
creator_id = instance_data.get("creator_identifier")
if creator_id == self.identifier:
instance = CreatedInstance.from_existing(
instance_data, self
)
self._add_instance_to_context(instance)
instances_by_identifier = cache_and_get_instances(
self, SHARED_DATA_KEY, list_instances
)
for instance_data in instances_by_identifier[self.identifier]:
instance = CreatedInstance.from_existing(instance_data, self)
self._add_instance_to_context(instance)
def update_instances(self, update_list):
update_instances(update_list)
@ -74,13 +58,12 @@ class TrayPublishCreator(Creator):
host_name = "traypublisher"
def collect_instances(self):
for instance_data in _cache_and_get_instances(self):
creator_id = instance_data.get("creator_identifier")
if creator_id == self.identifier:
instance = CreatedInstance.from_existing(
instance_data, self
)
self._add_instance_to_context(instance)
instances_by_identifier = cache_and_get_instances(
self, SHARED_DATA_KEY, list_instances
)
for instance_data in instances_by_identifier[self.identifier]:
instance = CreatedInstance.from_existing(instance_data, self)
self._add_instance_to_context(instance)
def update_instances(self, update_list):
update_instances(update_list)
@ -110,11 +93,14 @@ class TrayPublishCreator(Creator):
class SettingsCreator(TrayPublishCreator):
create_allow_context_change = True
create_allow_thumbnail = True
extensions = []
def create(self, subset_name, data, pre_create_data):
# Pass precreate data to creator attributes
thumbnail_path = pre_create_data.pop(PRE_CREATE_THUMBNAIL_KEY, None)
data["creator_attributes"] = pre_create_data
data["settings_creator"] = True
# Create new instance
@ -122,6 +108,9 @@ class SettingsCreator(TrayPublishCreator):
self._store_new_instance(new_instance)
if thumbnail_path:
self.set_instance_thumbnail_path(new_instance.id, thumbnail_path)
def get_instance_attr_defs(self):
return [
FileDef(

View file

@ -40,7 +40,8 @@ class CollectMovieBatch(
if creator_attributes["add_review_family"]:
repre["tags"].append("review")
instance.data["families"].append("review")
instance.data["thumbnailSource"] = file_url
if not instance.data.get("thumbnailSource"):
instance.data["thumbnailSource"] = file_url
instance.data["source"] = file_url

View file

@ -188,7 +188,8 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin):
if "review" not in instance.data["families"]:
instance.data["families"].append("review")
instance.data["thumbnailSource"] = first_filepath
if not instance.data.get("thumbnailSource"):
instance.data["thumbnailSource"] = first_filepath
review_representation["tags"].append("review")
self.log.debug("Representation {} was marked for review. {}".format(

View file

@ -1,6 +1,5 @@
import os
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
TVPAINT_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -1,4 +1,4 @@
import qargparse
from openpype.lib.attribute_definitions import BoolDef
from openpype.hosts.tvpaint.api import plugin
from openpype.hosts.tvpaint.api.lib import execute_george_through_file
@ -27,26 +27,28 @@ class ImportImage(plugin.Loader):
"preload": True
}
options = [
qargparse.Boolean(
"stretch",
label="Stretch to project size",
default=True,
help="Stretch loaded image/s to project resolution?"
),
qargparse.Boolean(
"timestretch",
label="Stretch to timeline length",
default=True,
help="Clip loaded image/s to timeline length?"
),
qargparse.Boolean(
"preload",
label="Preload loaded image/s",
default=True,
help="Preload image/s?"
)
]
@classmethod
def get_options(cls, contexts):
return [
BoolDef(
"stretch",
label="Stretch to project size",
default=cls.defaults["stretch"],
tooltip="Stretch loaded image/s to project resolution?"
),
BoolDef(
"timestretch",
label="Stretch to timeline length",
default=cls.defaults["timestretch"],
tooltip="Clip loaded image/s to timeline length?"
),
BoolDef(
"preload",
label="Preload loaded image/s",
default=cls.defaults["preload"],
tooltip="Preload image/s?"
)
]
def load(self, context, name, namespace, options):
stretch = options.get("stretch", self.defaults["stretch"])

View file

@ -1,7 +1,6 @@
import collections
import qargparse
from openpype.lib.attribute_definitions import BoolDef
from openpype.pipeline import (
get_representation_context,
register_host,
@ -42,26 +41,28 @@ class LoadImage(plugin.Loader):
"preload": True
}
options = [
qargparse.Boolean(
"stretch",
label="Stretch to project size",
default=True,
help="Stretch loaded image/s to project resolution?"
),
qargparse.Boolean(
"timestretch",
label="Stretch to timeline length",
default=True,
help="Clip loaded image/s to timeline length?"
),
qargparse.Boolean(
"preload",
label="Preload loaded image/s",
default=True,
help="Preload image/s?"
)
]
@classmethod
def get_options(cls, contexts):
return [
BoolDef(
"stretch",
label="Stretch to project size",
default=cls.defaults["stretch"],
tooltip="Stretch loaded image/s to project resolution?"
),
BoolDef(
"timestretch",
label="Stretch to timeline length",
default=cls.defaults["timestretch"],
tooltip="Clip loaded image/s to timeline length?"
),
BoolDef(
"preload",
label="Preload loaded image/s",
default=cls.defaults["preload"],
tooltip="Preload image/s?"
)
]
def load(self, context, name, namespace, options):
stretch = options.get("stretch", self.defaults["stretch"])

View file

@ -1,6 +1,5 @@
import os
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -2,8 +2,7 @@ import os
import click
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon
from openpype.modules import OpenPypeModule, IHostAddon
WEBPUBLISHER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -83,8 +83,9 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
self.log.info("task_data:: {}".format(task_data))
is_sequence = len(task_data["files"]) > 1
first_file = task_data["files"][0]
_, extension = os.path.splitext(task_data["files"][0])
_, extension = os.path.splitext(first_file)
family, families, tags = self._get_family(
self.task_type_to_family,
task_type,
@ -149,10 +150,13 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
self.log.warning("Unable to count frames "
"duration {}".format(no_of_frames))
# raise ValueError("STOP")
instance.data["handleStart"] = asset_doc["data"]["handleStart"]
instance.data["handleEnd"] = asset_doc["data"]["handleEnd"]
if "review" in tags:
first_file_path = os.path.join(task_dir, first_file)
instance.data["thumbnailSource"] = first_file_path
instances.append(instance)
self.log.info("instance.data:: {}".format(instance.data))

View file

@ -1,137 +0,0 @@
import os
import shutil
import pyblish.api
from openpype.lib import (
get_ffmpeg_tool_path,
run_subprocess,
get_transcode_temp_directory,
convert_input_paths_for_ffmpeg,
should_convert_for_ffmpeg
)
class ExtractThumbnail(pyblish.api.InstancePlugin):
"""Create jpg thumbnail from input using ffmpeg."""
label = "Extract Thumbnail"
order = pyblish.api.ExtractorOrder
families = [
"render",
"image"
]
hosts = ["webpublisher"]
targets = ["filespublish"]
def process(self, instance):
self.log.info("subset {}".format(instance.data['subset']))
filtered_repres = self._get_filtered_repres(instance)
for repre in filtered_repres:
repre_files = repre["files"]
if not isinstance(repre_files, (list, tuple)):
input_file = repre_files
else:
file_index = int(float(len(repre_files)) * 0.5)
input_file = repre_files[file_index]
stagingdir = os.path.normpath(repre["stagingDir"])
full_input_path = os.path.join(stagingdir, input_file)
self.log.info("Input filepath: {}".format(full_input_path))
do_convert = should_convert_for_ffmpeg(full_input_path)
# If result is None the requirement of conversion can't be
# determined
if do_convert is None:
self.log.info((
"Can't determine if representation requires conversion."
" Skipped."
))
continue
# Do conversion if needed
# - change staging dir of source representation
# - must be set back after output definitions processing
convert_dir = None
if do_convert:
convert_dir = get_transcode_temp_directory()
filename = os.path.basename(full_input_path)
convert_input_paths_for_ffmpeg(
[full_input_path],
convert_dir,
self.log
)
full_input_path = os.path.join(convert_dir, filename)
filename = os.path.splitext(input_file)[0]
while filename.endswith("."):
filename = filename[:-1]
thumbnail_filename = filename + "_thumbnail.jpg"
full_output_path = os.path.join(stagingdir, thumbnail_filename)
self.log.info("output {}".format(full_output_path))
ffmpeg_args = [
get_ffmpeg_tool_path("ffmpeg"),
"-y",
"-i", full_input_path,
"-vframes", "1",
full_output_path
]
# run subprocess
self.log.debug("{}".format(" ".join(ffmpeg_args)))
try: # temporary until oiiotool is supported cross platform
run_subprocess(
ffmpeg_args, logger=self.log
)
except RuntimeError as exp:
if "Compression" in str(exp):
self.log.debug(
"Unsupported compression on input files. Skipping!!!"
)
return
self.log.warning("Conversion crashed", exc_info=True)
raise
new_repre = {
"name": "thumbnail",
"ext": "jpg",
"files": thumbnail_filename,
"stagingDir": stagingdir,
"thumbnail": True,
"tags": ["thumbnail"]
}
# adding representation
self.log.debug("Adding: {}".format(new_repre))
instance.data["representations"].append(new_repre)
# Cleanup temp folder
if convert_dir is not None and os.path.exists(convert_dir):
shutil.rmtree(convert_dir)
def _get_filtered_repres(self, instance):
filtered_repres = []
repres = instance.data.get("representations") or []
for repre in repres:
self.log.debug(repre)
tags = repre.get("tags") or []
# Skip instance if already has thumbnail representation
if "thumbnail" in tags:
return []
if "review" not in tags:
continue
if not repre.get("files"):
self.log.info((
"Representation \"{}\" don't have files. Skipping"
).format(repre["name"]))
continue
filtered_repres.append(repre)
return filtered_repres

View file

@ -91,7 +91,7 @@ class AbstractAttrDefMeta(ABCMeta):
@six.add_metaclass(AbstractAttrDefMeta)
class AbtractAttrDef:
class AbtractAttrDef(object):
"""Abstraction of attribute definiton.
Each attribute definition must have implemented validation and

View file

@ -42,7 +42,7 @@ XML_CHAR_REF_REGEX_HEX = re.compile(r"&#x?[0-9a-fA-F]+;")
# Regex to parse array attributes
ARRAY_TYPE_REGEX = re.compile(r"^(int|float|string)\[\d+\]$")
IMAGE_EXTENSIONS = [
IMAGE_EXTENSIONS = {
".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".cal",
".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".fits",
".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer",
@ -54,15 +54,15 @@ IMAGE_EXTENSIONS = [
".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep",
".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf",
".xpm", ".xwd"
]
}
VIDEO_EXTENSIONS = [
VIDEO_EXTENSIONS = {
".3g2", ".3gp", ".amv", ".asf", ".avi", ".drc", ".f4a", ".f4b",
".f4p", ".f4v", ".flv", ".gif", ".gifv", ".m2v", ".m4p", ".m4v",
".mkv", ".mng", ".mov", ".mp2", ".mp4", ".mpe", ".mpeg", ".mpg",
".mpv", ".mxf", ".nsv", ".ogg", ".ogv", ".qt", ".rm", ".rmvb",
".roq", ".svi", ".vob", ".webm", ".wmv", ".yuv"
]
}
def get_transcode_temp_directory():

View file

@ -1,4 +1,14 @@
# -*- coding: utf-8 -*-
from .interfaces import (
ILaunchHookPaths,
IPluginPaths,
ITrayModule,
ITrayAction,
ITrayService,
ISettingsChangeListener,
IHostAddon,
)
from .base import (
OpenPypeModule,
OpenPypeAddOn,
@ -17,6 +27,14 @@ from .base import (
__all__ = (
"ILaunchHookPaths",
"IPluginPaths",
"ITrayModule",
"ITrayAction",
"ITrayService",
"ISettingsChangeListener",
"IHostAddon",
"OpenPypeModule",
"OpenPypeAddOn",

View file

@ -1,7 +1,6 @@
import os
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayModule
from openpype.modules import OpenPypeModule, ITrayModule
class AvalonModule(OpenPypeModule, ITrayModule):

View file

@ -9,6 +9,7 @@ import logging
import platform
import threading
import collections
import traceback
from uuid import uuid4
from abc import ABCMeta, abstractmethod
import six
@ -139,6 +140,15 @@ class _InterfacesClass(_ModuleClass):
"cannot import name '{}' from 'openpype_interfaces'"
).format(attr_name))
if _LoadCache.interfaces_loaded and attr_name != "log":
stack = list(traceback.extract_stack())
stack.pop(-1)
self.log.warning((
"Using deprecated import of \"{}\" from 'openpype_interfaces'."
" Please switch to use import"
" from 'openpype.modules.interfaces'"
" (will be removed after 3.16.x).{}"
).format(attr_name, "".join(traceback.format_list(stack))))
return self.__attributes__[attr_name]

View file

@ -2,16 +2,17 @@ import os
import threading
import time
from openpype.modules import (
OpenPypeModule,
ITrayModule,
IPluginPaths
)
from .clockify_api import ClockifyAPI
from .constants import (
CLOCKIFY_FTRACK_USER_PATH,
CLOCKIFY_FTRACK_SERVER_PATH
)
from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayModule,
IPluginPaths
)
class ClockifyModule(

View file

@ -4,8 +4,7 @@ import six
import sys
from openpype.lib import requests_get, Logger
from openpype.modules import OpenPypeModule
from openpype_interfaces import IPluginPaths
from openpype.modules import OpenPypeModule, IPluginPaths
class DeadlineWebserviceError(Exception):

View file

@ -457,9 +457,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
cam = [c for c in cameras if c in col.head]
if cam:
subset_name = '{}_{}_{}'.format(group_name, cam, aov)
if aov:
subset_name = '{}_{}_{}'.format(group_name, cam, aov)
else:
subset_name = '{}_{}'.format(group_name, cam)
else:
subset_name = '{}_{}'.format(group_name, aov)
if aov:
subset_name = '{}_{}'.format(group_name, aov)
else:
subset_name = '{}'.format(group_name)
if isinstance(col, (list, tuple)):
staging = os.path.dirname(col[0])

View file

@ -7,7 +7,12 @@ import json
import platform
import uuid
import re
from Deadline.Scripting import RepositoryUtils, FileUtils, DirectoryUtils
from Deadline.Scripting import (
RepositoryUtils,
FileUtils,
DirectoryUtils,
ProcessUtils,
)
def get_openpype_version_from_path(path, build=True):
@ -162,9 +167,8 @@ def inject_openpype_environment(deadlinePlugin):
print(">>> Temporary path: {}".format(export_url))
args = [
exe,
"--headless",
'extractenvironments',
"extractenvironments",
export_url
]
@ -188,15 +192,18 @@ def inject_openpype_environment(deadlinePlugin):
if not os.environ.get("OPENPYPE_MONGO"):
print(">>> Missing OPENPYPE_MONGO env var, process won't work")
env = os.environ
env["OPENPYPE_HEADLESS_MODE"] = "1"
env["AVALON_TIMEOUT"] = "5000"
os.environ["AVALON_TIMEOUT"] = "5000"
print(">>> Executing: {}".format(" ".join(args)))
std_output = subprocess.check_output(args,
cwd=os.path.dirname(exe),
env=env)
print(">>> Process result {}".format(std_output))
args_str = subprocess.list2cmdline(args)
print(">>> Executing: {} {}".format(exe, args_str))
process = ProcessUtils.SpawnProcess(
exe, args_str, os.path.dirname(exe)
)
ProcessUtils.WaitForExit(process, -1)
if process.ExitCode != 0:
raise RuntimeError(
"Failed to run OpenPype process to extract environments."
)
print(">>> Loading file ...")
with open(export_url) as fp:

View file

@ -13,10 +13,7 @@ import click
from openpype.modules import (
JsonFilesSettingsDef,
OpenPypeAddOn,
ModulesManager
)
# Import interface defined by this addon to be able find other addons using it
from openpype_interfaces import (
ModulesManager,
IPluginPaths,
ITrayAction
)

View file

@ -132,7 +132,7 @@ class UserAssigmentEvent(BaseEvent):
"""
Get data to fill template from task
.. seealso:: :mod:`openpype.api.Anatomy`
.. seealso:: :mod:`openpype.pipeline.Anatomy`
:param task: Task entity
:type task: dict

View file

@ -5,8 +5,8 @@ import platform
import click
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import (
from openpype.modules import (
OpenPypeModule,
ITrayModule,
IPluginPaths,
ISettingsChangeListener
@ -195,7 +195,7 @@ class FtrackModule(
app_definitions_from_app_manager,
tool_definitions_from_app_manager
)
from openpype.api import ApplicationManager
from openpype.lib import ApplicationManager
query_keys = [
"id",
"key",

View file

@ -151,7 +151,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
first_thumbnail_component = None
first_thumbnail_component_repre = None
if has_movie_review:
if not review_representations or has_movie_review:
for repre in thumbnail_representations:
repre_path = self._get_repre_path(instance, repre, False)
if not repre_path:

View file

@ -3,8 +3,11 @@
import click
import os
from openpype.modules import OpenPypeModule
from openpype_interfaces import IPluginPaths, ITrayAction
from openpype.modules import (
OpenPypeModule,
IPluginPaths,
ITrayAction,
)
class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction):

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
import os
import re
import pyblish.api
class CollectKitsuUsername(pyblish.api.ContextPlugin):
"""Collect Kitsu username from the kitsu login"""
order = pyblish.api.CollectorOrder + 0.499
label = "Kitsu username"
def process(self, context):
kitsu_login = os.environ.get('KITSU_LOGIN')
if not kitsu_login:
return
kitsu_username = kitsu_login.split("@")[0].replace('.', ' ')
new_username = re.sub('[^a-zA-Z]', ' ', kitsu_username).title()
for instance in context:
# Don't override customData if it already exists
if 'customData' not in instance.data:
instance.data['customData'] = {}
instance.data['customData']["kitsuUsername"] = new_username

View file

@ -1,5 +1,7 @@
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayAction
from openpype.modules import (
OpenPypeModule,
ITrayAction,
)
class LauncherAction(OpenPypeModule, ITrayAction):

View file

@ -1,5 +1,4 @@
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayModule
from openpype.modules import OpenPypeModule, ITrayModule
class LogViewModule(OpenPypeModule, ITrayModule):

View file

@ -2,8 +2,7 @@ import os
import json
import appdirs
import requests
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayModule
from openpype.modules import OpenPypeModule, ITrayModule
class MusterModule(OpenPypeModule, ITrayModule):

View file

@ -1,5 +1,4 @@
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayAction
from openpype.modules import OpenPypeModule, ITrayAction
class ProjectManagerAction(OpenPypeModule, ITrayAction):

View file

@ -1,5 +1,4 @@
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayAction
from openpype.modules import OpenPypeModule, ITrayAction
class PythonInterpreterAction(OpenPypeModule, ITrayAction):

View file

@ -2,8 +2,7 @@
"""Module providing support for Royal Render."""
import os
import openpype.modules
from openpype.modules import OpenPypeModule
from openpype_interfaces import IPluginPaths
from openpype.modules import OpenPypeModule, IPluginPaths
class RoyalRenderModule(OpenPypeModule, IPluginPaths):

View file

@ -1,5 +1,4 @@
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayAction
from openpype.modules import OpenPypeModule, ITrayAction
class SettingsAction(OpenPypeModule, ITrayAction):

View file

@ -1,12 +1,11 @@
import os
from openpype_interfaces import (
from openpype.modules import (
OpenPypeModule,
ITrayModule,
IPluginPaths,
)
from openpype.modules import OpenPypeModule
SHOTGRID_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -1,6 +1,5 @@
import os
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IPluginPaths
from openpype.modules import OpenPypeModule, IPluginPaths
SLACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))

View file

@ -11,9 +11,12 @@ from collections import deque, defaultdict
import click
from bson.objectid import ObjectId
from openpype.client import get_projects
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayModule
from openpype.client import (
get_projects,
get_representations,
get_representation_by_id,
)
from openpype.modules import OpenPypeModule, ITrayModule
from openpype.settings import (
get_project_settings,
get_system_settings,
@ -30,9 +33,6 @@ from .providers import lib
from .utils import time_function, SyncStatus, SiteAlreadyPresentError
from openpype.client import get_representations, get_representation_by_id
log = Logger.get_logger("SyncServer")

View file

@ -3,8 +3,8 @@ import platform
from openpype.client import get_asset_by_name
from openpype.modules import OpenPypeModule
from openpype_interfaces import (
from openpype.modules import (
OpenPypeModule,
ITrayService,
IPluginPaths
)

View file

@ -5,7 +5,7 @@ import logging
from concurrent.futures import CancelledError
from Qt import QtWidgets
from openpype_interfaces import ITrayService
from openpype.modules import ITrayService
log = logging.getLogger(__name__)

View file

@ -24,8 +24,7 @@ import os
import socket
from openpype import resources
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayService
from openpype.modules import OpenPypeModule, ITrayService
class WebServerModule(OpenPypeModule, ITrayService):

View file

@ -85,6 +85,7 @@ from .context_tools import (
register_host,
registered_host,
deregister_host,
get_process_id,
)
install = install_host
uninstall = uninstall_host

View file

@ -5,6 +5,7 @@ import json
import types
import logging
import platform
import uuid
import pyblish.api
from pyblish.lib import MessageHandler
@ -37,6 +38,7 @@ from . import (
_is_installed = False
_process_id = None
_registered_root = {"_": ""}
_registered_host = {"_": None}
# Keep modules manager (and it's modules) in memory
@ -546,3 +548,18 @@ def change_current_context(asset_doc, task_name, template_key=None):
emit_event("taskChanged", data)
return changes
def get_process_id():
"""Fake process id created on demand using uuid.
Can be used to create process specific folders in temp directory.
Returns:
str: Process id.
"""
global _process_id
if _process_id is None:
_process_id = str(uuid.uuid4())
return _process_id

View file

@ -1,6 +1,7 @@
from .constants import (
SUBSET_NAME_ALLOWED_SYMBOLS,
DEFAULT_SUBSET_TEMPLATE,
PRE_CREATE_THUMBNAIL_KEY,
)
from .subset_name import (
@ -24,6 +25,8 @@ from .creator_plugins import (
deregister_creator_plugin,
register_creator_plugin_path,
deregister_creator_plugin_path,
cache_and_get_instances,
)
from .context import (
@ -40,6 +43,7 @@ from .legacy_create import (
__all__ = (
"SUBSET_NAME_ALLOWED_SYMBOLS",
"DEFAULT_SUBSET_TEMPLATE",
"PRE_CREATE_THUMBNAIL_KEY",
"TaskNotSetError",
"get_subset_name",

View file

@ -1,8 +1,10 @@
SUBSET_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_."
DEFAULT_SUBSET_TEMPLATE = "{family}{Variant}"
PRE_CREATE_THUMBNAIL_KEY = "thumbnail_source"
__all__ = (
"SUBSET_NAME_ALLOWED_SYMBOLS",
"DEFAULT_SUBSET_TEMPLATE",
"PRE_CREATE_THUMBNAIL_KEY",
)

View file

@ -1077,6 +1077,8 @@ class CreateContext:
# Shared data across creators during collection phase
self._collection_shared_data = None
self.thumbnail_paths_by_instance_id = {}
# Trigger reset if was enabled
if reset:
self.reset(discover_publish_plugins)
@ -1146,6 +1148,29 @@ class CreateContext:
self.reset_finalization()
def refresh_thumbnails(self):
"""Cleanup thumbnail paths.
Remove all thumbnail filepaths that are empty or lead to files which
does not exists or of instances that are not available anymore.
"""
invalid = set()
for instance_id, path in self.thumbnail_paths_by_instance_id.items():
instance_available = True
if instance_id is not None:
instance_available = instance_id in self._instances_by_id
if (
not instance_available
or not path
or not os.path.exists(path)
):
invalid.add(instance_id)
for instance_id in invalid:
self.thumbnail_paths_by_instance_id.pop(instance_id)
def reset_preparation(self):
"""Prepare attributes that must be prepared/cleaned before reset."""
@ -1157,6 +1182,7 @@ class CreateContext:
# Stop access to collection shared data
self._collection_shared_data = None
self.refresh_thumbnails()
def reset_avalon_context(self):
"""Give ability to reset avalon context.

View file

@ -1,5 +1,6 @@
import os
import copy
import collections
from abc import (
ABCMeta,
@ -442,6 +443,13 @@ class BaseCreator:
return self.create_context.collection_shared_data
def set_instance_thumbnail_path(self, instance_id, thumbnail_path=None):
"""Set path to thumbnail for instance."""
self.create_context.thumbnail_paths_by_instance_id[instance_id] = (
thumbnail_path
)
class Creator(BaseCreator):
"""Creator that has more information for artist to show in UI.
@ -468,6 +476,13 @@ class Creator(BaseCreator):
# - in some cases it may confuse artists because it would not be used
# e.g. for buld creators
create_allow_context_change = True
# A thumbnail can be passed in precreate attributes
# - if is set to True is should expect that a thumbnail path under key
# PRE_CREATE_THUMBNAIL_KEY can be sent in data with precreate data
# - is disabled by default because the feature was added in later stages
# and creators who would not expect PRE_CREATE_THUMBNAIL_KEY could
# cause issues with instance data
create_allow_thumbnail = False
# Precreate attribute definitions showed before creation
# - similar to instance attribute definitions
@ -660,3 +675,34 @@ def deregister_creator_plugin_path(path):
deregister_plugin_path(BaseCreator, path)
deregister_plugin_path(LegacyCreator, path)
deregister_plugin_path(SubsetConvertorPlugin, path)
def cache_and_get_instances(creator, shared_key, list_instances_func):
"""Common approach to cache instances in shared data.
This is helper function which does not handle cases when a 'shared_key' is
used for different list instances functions. The same approach of caching
instances into 'collection_shared_data' is not required but is so common
we've decided to unify it to some degree.
Function 'list_instances_func' is called only if 'shared_key' is not
available in 'collection_shared_data' on creator.
Args:
creator (Creator): Plugin which would like to get instance data.
shared_key (str): Key under which output of function will be stored.
list_instances_func (Function): Function that will return instance data
if data were not yet stored under 'shared_key'.
Returns:
Dict[str, Dict[str, Any]]: Cached instances by creator identifier from
result of passed function.
"""
if shared_key not in creator.collection_shared_data:
value = collections.defaultdict(list)
for instance in list_instances_func():
identifier = instance.get("creator_identifier")
value[identifier].append(instance)
creator.collection_shared_data[shared_key] = value
return creator.collection_shared_data[shared_key]

View file

@ -1,9 +1,9 @@
import os
import json
from uuid import uuid4
from openpype.lib import Logger, filter_profiles
from openpype.lib.pype_info import get_workstation_info
from openpype.settings import get_project_settings
from openpype.pipeline import get_process_id
def _read_lock_file(lock_filepath):
@ -37,7 +37,7 @@ def is_workfile_locked_for_current_process(filepath):
lock_filepath = _get_lock_file(filepath)
data = _read_lock_file(lock_filepath)
return data["process_id"] == _get_process_id()
return data["process_id"] == get_process_id()
def delete_workfile_lock(filepath):
@ -49,7 +49,7 @@ def delete_workfile_lock(filepath):
def create_workfile_lock(filepath):
lock_filepath = _get_lock_file(filepath)
info = get_workstation_info()
info["process_id"] = _get_process_id()
info["process_id"] = get_process_id()
with open(lock_filepath, "w") as stream:
json.dump(info, stream)
@ -59,14 +59,6 @@ def remove_workfile_lock(filepath):
delete_workfile_lock(filepath)
def _get_process_id():
process_id = os.environ.get("OPENPYPE_PROCESS_ID")
if not process_id:
process_id = str(uuid4())
os.environ["OPENPYPE_PROCESS_ID"] = process_id
return process_id
def is_workfile_lock_enabled(host_name, project_name, project_setting=None):
if project_setting is None:
project_setting = get_project_settings(project_name)

View file

@ -1,5 +1,5 @@
import os
from openpype.api import ApplicationManager
from openpype.lib import ApplicationManager
from openpype.pipeline import load

View file

@ -19,14 +19,28 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
if not create_context:
return
thumbnail_paths_by_instance_id = (
create_context.thumbnail_paths_by_instance_id
)
context.data["thumbnailSource"] = (
thumbnail_paths_by_instance_id.get(None)
)
project_name = create_context.project_name
if project_name:
context.data["projectName"] = project_name
for created_instance in create_context.instances:
instance_data = created_instance.data_to_store()
if instance_data["active"]:
thumbnail_path = thumbnail_paths_by_instance_id.get(
created_instance.id
)
self.create_instance(
context, instance_data, created_instance.transient_data
context,
instance_data,
created_instance.transient_data,
thumbnail_path
)
# Update global data to context
@ -39,7 +53,13 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
legacy_io.Session[key] = value
os.environ[key] = value
def create_instance(self, context, in_data, transient_data):
def create_instance(
self,
context,
in_data,
transient_data,
thumbnail_path
):
subset = in_data["subset"]
# If instance data already contain families then use it
instance_families = in_data.get("families") or []
@ -53,7 +73,8 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
"name": subset,
"family": in_data["family"],
"families": instance_families,
"representations": []
"representations": [],
"thumbnailSource": thumbnail_path
})
for key, value in in_data.items():
if key not in instance.data:

View file

@ -1,6 +1,7 @@
import os
import pyblish.api
import openpype.api as pype
from openpype.lib import get_version_from_path
class CollectSceneVersion(pyblish.api.ContextPlugin):
@ -46,7 +47,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin):
if '<shell>' in filename:
return
version = pype.get_version_from_path(filename)
version = get_version_from_path(filename)
assert version, "Cannot determine version"
rootVersion = int(version)

View file

@ -1,9 +1,8 @@
import os
import pyblish
import openpype.api
from openpype.lib import (
get_ffmpeg_tool_path,
path_to_subprocess_arg
run_subprocess
)
import tempfile
import opentimelineio as otio
@ -102,9 +101,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
# run subprocess
self.log.debug("Executing: {}".format(" ".join(cmd)))
openpype.api.run_subprocess(
cmd, logger=self.log
)
run_subprocess(cmd, logger=self.log)
else:
audio_fpath = recycling_file.pop()
@ -225,7 +222,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
# run subprocess
self.log.debug("Executing: {}".format(" ".join(cmd)))
openpype.api.run_subprocess(
run_subprocess(
cmd, logger=self.log
)
@ -308,7 +305,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
# run subprocess
self.log.debug("Executing: {}".format(args))
openpype.api.run_subprocess(args, logger=self.log)
run_subprocess(args, logger=self.log)
os.remove(filters_tmp_filepath)

View file

@ -3,25 +3,26 @@ import re
import copy
import json
import shutil
from abc import ABCMeta, abstractmethod
import six
import clique
import speedcopy
import pyblish.api
import openpype.api
from openpype.lib import (
get_ffmpeg_tool_path,
get_ffprobe_streams,
path_to_subprocess_arg,
run_subprocess,
)
from openpype.lib.transcoding import (
IMAGE_EXTENSIONS,
get_ffprobe_streams,
should_convert_for_ffmpeg,
convert_input_paths_for_ffmpeg,
get_transcode_temp_directory
get_transcode_temp_directory,
)
import speedcopy
class ExtractReview(pyblish.api.InstancePlugin):
@ -174,6 +175,26 @@ class ExtractReview(pyblish.api.InstancePlugin):
outputs_per_representations.append((repre, outputs))
return outputs_per_representations
def _single_frame_filter(self, input_filepaths, output_defs):
single_frame_image = False
if len(input_filepaths) == 1:
ext = os.path.splitext(input_filepaths[0])[-1]
single_frame_image = ext in IMAGE_EXTENSIONS
filtered_defs = []
for output_def in output_defs:
output_filters = output_def.get("filter") or {}
frame_filter = output_filters.get("single_frame_filter")
if (
(not single_frame_image and frame_filter == "single_frame")
or (single_frame_image and frame_filter == "multi_frame")
):
continue
filtered_defs.append(output_def)
return filtered_defs
@staticmethod
def get_instance_label(instance):
return (
@ -194,7 +215,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
outputs_per_repres = self._get_outputs_per_representations(
instance, profile_outputs
)
for repre, outpu_defs in outputs_per_repres:
for repre, output_defs in outputs_per_repres:
# Check if input should be preconverted before processing
# Store original staging dir (it's value may change)
src_repre_staging_dir = repre["stagingDir"]
@ -215,6 +236,16 @@ class ExtractReview(pyblish.api.InstancePlugin):
if first_input_path is None:
first_input_path = filepath
filtered_output_defs = self._single_frame_filter(
input_filepaths, output_defs
)
if not filtered_output_defs:
self.log.debug((
"Repre: {} - All output definitions were filtered"
" out by single frame filter. Skipping"
).format(repre["name"]))
continue
# Skip if file is not set
if first_input_path is None:
self.log.warning((
@ -248,7 +279,10 @@ class ExtractReview(pyblish.api.InstancePlugin):
try:
self._render_output_definitions(
instance, repre, src_repre_staging_dir, outpu_defs
instance,
repre,
src_repre_staging_dir,
filtered_output_defs
)
finally:
@ -262,10 +296,10 @@ class ExtractReview(pyblish.api.InstancePlugin):
shutil.rmtree(new_staging_dir)
def _render_output_definitions(
self, instance, repre, src_repre_staging_dir, outpu_defs
self, instance, repre, src_repre_staging_dir, output_defs
):
fill_data = copy.deepcopy(instance.data["anatomyData"])
for _output_def in outpu_defs:
for _output_def in output_defs:
output_def = copy.deepcopy(_output_def)
# Make sure output definition has "tags" key
if "tags" not in output_def:
@ -354,9 +388,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
# run subprocess
self.log.debug("Executing: {}".format(subprcs_cmd))
openpype.api.run_subprocess(
subprcs_cmd, shell=True, logger=self.log
)
run_subprocess(subprcs_cmd, shell=True, logger=self.log)
# delete files added to fill gaps
if files_to_clean:
@ -1660,9 +1692,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
return True
return False
def filter_output_defs(
self, profile, subset_name, families
):
def filter_output_defs(self, profile, subset_name, families):
"""Return outputs matching input instance families.
Output definitions without families filter are marked as valid.

View file

@ -4,8 +4,8 @@ import os
import shutil
import pyblish.api
import openpype.api
import openpype.lib
from openpype.lib import run_subprocess, get_oiio_tools_path
class ExtractScanlineExr(pyblish.api.InstancePlugin):
@ -45,7 +45,7 @@ class ExtractScanlineExr(pyblish.api.InstancePlugin):
stagingdir = os.path.normpath(repre.get("stagingDir"))
oiio_tool_path = openpype.lib.get_oiio_tools_path()
oiio_tool_path = get_oiio_tools_path()
if not os.path.exists(oiio_tool_path):
self.log.error(
"OIIO tool not found in {}".format(oiio_tool_path))
@ -65,7 +65,7 @@ class ExtractScanlineExr(pyblish.api.InstancePlugin):
subprocess_exr = " ".join(oiio_cmd)
self.log.info(f"running: {subprocess_exr}")
openpype.api.run_subprocess(subprocess_exr, logger=self.log)
run_subprocess(subprocess_exr, logger=self.log)
# raise error if there is no ouptput
if not os.path.exists(os.path.join(stagingdir, original_name)):

View file

@ -34,28 +34,55 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
label = "Extract Thumbnail (from source)"
# Before 'ExtractThumbnail' in global plugins
order = pyblish.api.ExtractorOrder - 0.00001
hosts = ["traypublisher"]
def process(self, instance):
self._create_context_thumbnail(instance.context)
subset_name = instance.data["subset"]
self.log.info(
"Processing instance with subset name {}".format(subset_name)
)
thumbnail_source = instance.data.get("thumbnailSource")
if not thumbnail_source:
self.log.debug("Thumbnail source not filled. Skipping.")
return
elif not os.path.exists(thumbnail_source):
self.log.debug(
"Thumbnail source file was not found {}. Skipping.".format(
thumbnail_source))
# Check if already has thumbnail created
if self._instance_has_thumbnail(instance):
self.log.info("Thumbnail representation already present.")
return
# Check if already has thumbnail created
if self._already_has_thumbnail(instance):
self.log.info("Thumbnail representation already present.")
dst_filepath = self._create_thumbnail(
instance.context, thumbnail_source
)
if not dst_filepath:
return
dst_staging, dst_filename = os.path.split(dst_filepath)
new_repre = {
"name": "thumbnail",
"ext": "jpg",
"files": dst_filename,
"stagingDir": dst_staging,
"thumbnail": True,
"tags": ["thumbnail"]
}
# adding representation
self.log.debug(
"Adding thumbnail representation: {}".format(new_repre)
)
instance.data["representations"].append(new_repre)
def _create_thumbnail(self, context, thumbnail_source):
if not thumbnail_source:
self.log.debug("Thumbnail source not filled. Skipping.")
return
if not os.path.exists(thumbnail_source):
self.log.debug((
"Thumbnail source is set but file was not found {}. Skipping."
).format(thumbnail_source))
return
# Create temp directory for thumbnail
@ -65,7 +92,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
"Create temp directory {} for thumbnail".format(dst_staging)
)
# Store new staging to cleanup paths
instance.context.data["cleanupFullPaths"].append(dst_staging)
context.data["cleanupFullPaths"].append(dst_staging)
thumbnail_created = False
oiio_supported = is_oiio_supported()
@ -97,26 +124,12 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
)
# Skip representation and try next one if wasn't created
if not thumbnail_created:
self.log.warning("Thumbanil has not been created.")
return
if thumbnail_created:
return full_output_path
new_repre = {
"name": "thumbnail",
"ext": "jpg",
"files": dst_filename,
"stagingDir": dst_staging,
"thumbnail": True,
"tags": ["thumbnail"]
}
self.log.warning("Thumbanil has not been created.")
# adding representation
self.log.debug(
"Adding thumbnail representation: {}".format(new_repre)
)
instance.data["representations"].append(new_repre)
def _already_has_thumbnail(self, instance):
def _instance_has_thumbnail(self, instance):
if "representations" not in instance.data:
self.log.warning(
"Instance does not have 'representations' key filled"
@ -171,3 +184,11 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
exc_info=True
)
return False
def _create_context_thumbnail(self, context):
if "thumbnailPath" in context.data:
return
thumbnail_source = context.data.get("thumbnailSource")
thumbnail_path = self._create_thumbnail(context, thumbnail_source)
context.data["thumbnailPath"] = thumbnail_path

View file

@ -13,166 +13,279 @@ import sys
import errno
import shutil
import copy
import collections
import six
import pyblish.api
from openpype.client import get_version_by_id
from openpype.client import get_versions
from openpype.client.operations import OperationsSession, new_thumbnail_doc
InstanceFilterResult = collections.namedtuple(
"InstanceFilterResult",
["instance", "thumbnail_path", "version_id"]
)
class IntegrateThumbnails(pyblish.api.InstancePlugin):
class IntegrateThumbnails(pyblish.api.ContextPlugin):
"""Integrate Thumbnails for Openpype use in Loaders."""
label = "Integrate Thumbnails"
order = pyblish.api.IntegratorOrder + 0.01
families = ["review"]
required_context_keys = [
"project", "asset", "task", "subset", "version"
]
def process(self, instance):
def process(self, context):
# Filter instances which can be used for integration
filtered_instance_items = self._prepare_instances(context)
if not filtered_instance_items:
self.log.info(
"All instances were filtered. Thumbnail integration skipped."
)
return
# Initial validation of available templated and required keys
env_key = "AVALON_THUMBNAIL_ROOT"
thumbnail_root_format_key = "{thumbnail_root}"
thumbnail_root = os.environ.get(env_key) or ""
published_repres = instance.data.get("published_representations")
if not published_repres:
self.log.debug(
"There are no published representations on the instance."
)
return
anatomy = instance.context.data["anatomy"]
anatomy = context.data["anatomy"]
project_name = anatomy.project_name
if "publish" not in anatomy.templates:
self.log.warning("Anatomy is missing the \"publish\" key!")
self.log.warning(
"Anatomy is missing the \"publish\" key. Skipping."
)
return
if "thumbnail" not in anatomy.templates["publish"]:
self.log.warning((
"There is no \"thumbnail\" template set for the project \"{}\""
"There is no \"thumbnail\" template set for the project"
" \"{}\". Skipping."
).format(project_name))
return
thumbnail_template = anatomy.templates["publish"]["thumbnail"]
if not thumbnail_template:
self.log.info("Thumbnail template is not filled. Skipping.")
return
if (
not thumbnail_root
and thumbnail_root_format_key in thumbnail_template
):
self.log.warning((
"{} is not set. Skipping thumbnail integration."
).format(env_key))
self.log.warning(("{} is not set. Skipping.").format(env_key))
return
thumb_repre = None
thumb_repre_anatomy_data = None
for repre_info in published_repres.values():
repre = repre_info["representation"]
if repre["name"].lower() == "thumbnail":
thumb_repre = repre
thumb_repre_anatomy_data = repre_info["anatomy_data"]
# Collect verion ids from all filtered instance
version_ids = {
instance_items.version_id
for instance_items in filtered_instance_items
}
# Query versions
version_docs = get_versions(
project_name,
version_ids=version_ids,
hero=True,
fields=["_id", "type", "name"]
)
# Store version by their id (converted to string)
version_docs_by_str_id = {
str(version_doc["_id"]): version_doc
for version_doc in version_docs
}
self._integrate_thumbnails(
filtered_instance_items,
version_docs_by_str_id,
anatomy,
thumbnail_root
)
def _prepare_instances(self, context):
context_thumbnail_path = context.get("thumbnailPath")
valid_context_thumbnail = False
if context_thumbnail_path and os.path.exists(context_thumbnail_path):
valid_context_thumbnail = True
filtered_instances = []
for instance in context:
instance_label = self._get_instance_label(instance)
# Skip instances without published representations
# - there is no place where to put the thumbnail
published_repres = instance.data.get("published_representations")
if not published_repres:
self.log.debug((
"There are no published representations"
" on the instance {}."
).format(instance_label))
continue
# Find thumbnail path on instance
thumbnail_path = self._get_instance_thumbnail_path(
published_repres)
if thumbnail_path:
self.log.debug((
"Found thumbnail path for instance \"{}\"."
" Thumbnail path: {}"
).format(instance_label, thumbnail_path))
elif valid_context_thumbnail:
# Use context thumbnail path if is available
thumbnail_path = context_thumbnail_path
self.log.debug((
"Using context thumbnail path for instance \"{}\"."
" Thumbnail path: {}"
).format(instance_label, thumbnail_path))
# Skip instance if thumbnail path is not available for it
if not thumbnail_path:
self.log.info((
"Skipping thumbnail integration for instance \"{}\"."
" Instance and context"
" thumbnail paths are not available."
).format(instance_label))
continue
version_id = str(self._get_version_id(published_repres))
filtered_instances.append(
InstanceFilterResult(instance, thumbnail_path, version_id)
)
return filtered_instances
def _get_version_id(self, published_representations):
for repre_info in published_representations.values():
return repre_info["representation"]["parent"]
def _get_instance_thumbnail_path(self, published_representations):
thumb_repre_doc = None
for repre_info in published_representations.values():
repre_doc = repre_info["representation"]
if repre_doc["name"].lower() == "thumbnail":
thumb_repre_doc = repre_doc
break
if not thumb_repre:
if thumb_repre_doc is None:
self.log.debug(
"There is not representation with name \"thumbnail\""
)
return
return None
version = get_version_by_id(project_name, thumb_repre["parent"])
if not version:
raise AssertionError(
"There does not exist version with id {}".format(
str(thumb_repre["parent"])
)
path = thumb_repre_doc["data"]["path"]
if not os.path.exists(path):
self.log.warning(
"Thumbnail file cannot be found. Path: {}".format(path)
)
return None
return os.path.normpath(path)
def _integrate_thumbnails(
self,
filtered_instance_items,
version_docs_by_str_id,
anatomy,
thumbnail_root
):
op_session = OperationsSession()
project_name = anatomy.project_name
for instance_item in filtered_instance_items:
instance, thumbnail_path, version_id = instance_item
instance_label = self._get_instance_label(instance)
version_doc = version_docs_by_str_id.get(version_id)
if not version_doc:
self.log.warning((
"Version entity for instance \"{}\" was not found."
).format(instance_label))
continue
filename, file_extension = os.path.splitext(thumbnail_path)
# Create id for mongo entity now to fill anatomy template
thumbnail_doc = new_thumbnail_doc()
thumbnail_id = thumbnail_doc["_id"]
# Prepare anatomy template fill data
template_data = copy.deepcopy(instance.data["anatomyData"])
template_data.update({
"_id": str(thumbnail_id),
"ext": file_extension[1:],
"name": "thumbnail",
"thumbnail_root": thumbnail_root,
"thumbnail_type": "thumbnail"
})
anatomy_filled = anatomy.format(template_data)
thumbnail_template = anatomy.templates["publish"]["thumbnail"]
template_filled = anatomy_filled["publish"]["thumbnail"]
dst_full_path = os.path.normpath(str(template_filled))
self.log.debug("Copying file .. {} -> {}".format(
thumbnail_path, dst_full_path
))
dirname = os.path.dirname(dst_full_path)
try:
os.makedirs(dirname)
except OSError as e:
if e.errno != errno.EEXIST:
tp, value, tb = sys.exc_info()
six.reraise(tp, value, tb)
shutil.copy(thumbnail_path, dst_full_path)
# Clean template data from keys that are dynamic
for key in ("_id", "thumbnail_root"):
template_data.pop(key, None)
repre_context = template_filled.used_values
for key in self.required_context_keys:
value = template_data.get(key)
if not value:
continue
repre_context[key] = template_data[key]
thumbnail_doc["data"] = {
"template": thumbnail_template,
"template_data": repre_context
}
op_session.create_entity(
project_name, thumbnail_doc["type"], thumbnail_doc
)
# Create thumbnail entity
self.log.debug(
"Creating entity in database {}".format(str(thumbnail_doc))
)
# Get full path to thumbnail file from representation
src_full_path = os.path.normpath(thumb_repre["data"]["path"])
if not os.path.exists(src_full_path):
self.log.warning("Thumbnail file was not found. Path: {}".format(
src_full_path
# Set thumbnail id for version
op_session.update_entity(
project_name,
version_doc["type"],
version_doc["_id"],
{"data.thumbnail_id": thumbnail_id}
)
if version_doc["type"] == "hero_version":
version_name = "Hero"
else:
version_name = version_doc["name"]
self.log.debug("Setting thumbnail for version \"{}\" <{}>".format(
version_name, version_id
))
return
filename, file_extension = os.path.splitext(src_full_path)
# Create id for mongo entity now to fill anatomy template
thumbnail_doc = new_thumbnail_doc()
thumbnail_id = thumbnail_doc["_id"]
# Prepare anatomy template fill data
template_data = copy.deepcopy(thumb_repre_anatomy_data)
template_data.update({
"_id": str(thumbnail_id),
"ext": file_extension[1:],
"thumbnail_root": thumbnail_root,
"thumbnail_type": "thumbnail"
})
anatomy_filled = anatomy.format(template_data)
template_filled = anatomy_filled["publish"]["thumbnail"]
dst_full_path = os.path.normpath(str(template_filled))
self.log.debug(
"Copying file .. {} -> {}".format(src_full_path, dst_full_path)
)
dirname = os.path.dirname(dst_full_path)
try:
os.makedirs(dirname)
except OSError as e:
if e.errno != errno.EEXIST:
tp, value, tb = sys.exc_info()
six.reraise(tp, value, tb)
shutil.copy(src_full_path, dst_full_path)
# Clean template data from keys that are dynamic
for key in ("_id", "thumbnail_root"):
template_data.pop(key, None)
repre_context = template_filled.used_values
for key in self.required_context_keys:
value = template_data.get(key)
if not value:
continue
repre_context[key] = template_data[key]
op_session = OperationsSession()
thumbnail_doc["data"] = {
"template": thumbnail_template,
"template_data": repre_context
}
op_session.create_entity(
project_name, thumbnail_doc["type"], thumbnail_doc
)
# Create thumbnail entity
self.log.debug(
"Creating entity in database {}".format(str(thumbnail_doc))
)
# Set thumbnail id for version
op_session.update_entity(
project_name,
version["type"],
version["_id"],
{"data.thumbnail_id": thumbnail_id}
)
self.log.debug("Setting thumbnail for version \"{}\" <{}>".format(
version["name"], str(version["_id"])
))
asset_entity = instance.data["assetEntity"]
op_session.update_entity(
project_name,
asset_entity["type"],
asset_entity["_id"],
{"data.thumbnail_id": thumbnail_id}
)
self.log.debug("Setting thumbnail for asset \"{}\" <{}>".format(
asset_entity["name"], str(version["_id"])
))
asset_entity = instance.data["assetEntity"]
op_session.update_entity(
project_name,
asset_entity["type"],
asset_entity["_id"],
{"data.thumbnail_id": thumbnail_id}
)
self.log.debug("Setting thumbnail for asset \"{}\" <{}>".format(
asset_entity["name"], version_id
))
op_session.commit()
def _get_instance_label(self, instance):
return (
instance.data.get("label")
or instance.data.get("name")
or "N/A"
)

View file

@ -21,9 +21,8 @@ class PreIntegrateThumbnails(pyblish.api.InstancePlugin):
label = "Override Integrate Thumbnail Representations"
order = pyblish.api.IntegratorOrder - 0.1
families = ["review"]
integrate_profiles = {}
integrate_profiles = []
def process(self, instance):
repres = instance.data.get("representations")

View file

@ -142,7 +142,7 @@
"exr16fpdwaa"
],
"reel_name": "OP_LoadedReel",
"clip_name_template": "{asset}_{subset}<_{output}>"
"clip_name_template": "{batch}_{asset}_{subset}<_{output}>"
}
}
}

View file

@ -53,6 +53,62 @@
"families": [],
"hosts": [],
"outputs": {
"png": {
"ext": "png",
"tags": [
"ftrackreview"
],
"burnins": [],
"ffmpeg_args": {
"video_filters": [],
"audio_filters": [],
"input": [],
"output": []
},
"filter": {
"families": [
"render",
"review",
"ftrack"
],
"subsets": [],
"custom_tags": [],
"single_frame_filter": "single_frame"
},
"overscan_crop": "",
"overscan_color": [
0,
0,
0,
255
],
"width": 1920,
"height": 1080,
"scale_pixel_aspect": true,
"bg_color": [
0,
0,
0,
0
],
"letter_box": {
"enabled": false,
"ratio": 0.0,
"fill_color": [
0,
0,
0,
255
],
"line_thickness": 0,
"line_color": [
255,
0,
0,
255
]
}
},
"h264": {
"ext": "mp4",
"tags": [
@ -79,7 +135,8 @@
"ftrack"
],
"subsets": [],
"custom_tags": []
"custom_tags": [],
"single_frame_filter": "multi_frame"
},
"overscan_crop": "",
"overscan_color": [

View file

@ -192,6 +192,24 @@
]
},
"variants": {
"13-2": {
"use_python_2": false,
"executables": {
"windows": [
"C:\\Program Files\\Nuke13.2v1\\Nuke13.2.exe"
],
"darwin": [],
"linux": [
"/usr/local/Nuke13.2v1/Nuke13.2"
]
},
"arguments": {
"windows": [],
"darwin": [],
"linux": []
},
"environment": {}
},
"13-0": {
"use_python_2": false,
"executables": {
@ -281,6 +299,7 @@
"environment": {}
},
"__dynamic_keys_labels__": {
"13-2": "13.2",
"13-0": "13.0",
"12-2": "12.2",
"12-0": "12.0",
@ -301,6 +320,30 @@
]
},
"variants": {
"13-2": {
"use_python_2": false,
"executables": {
"windows": [
"C:\\Program Files\\Nuke13.2v1\\Nuke13.2.exe"
],
"darwin": [],
"linux": [
"/usr/local/Nuke13.2v1/Nuke13.2"
]
},
"arguments": {
"windows": [
"--nukex"
],
"darwin": [
"--nukex"
],
"linux": [
"--nukex"
]
},
"environment": {}
},
"13-0": {
"use_python_2": false,
"executables": {
@ -420,6 +463,7 @@
"environment": {}
},
"__dynamic_keys_labels__": {
"13-2": "13.2",
"13-0": "13.0",
"12-2": "12.2",
"12-0": "12.0",
@ -438,6 +482,30 @@
"TAG_ASSETBUILD_STARTUP": "0"
},
"variants": {
"13-2": {
"use_python_2": false,
"executables": {
"windows": [
"C:\\Program Files\\Nuke13.2v1\\Nuke13.2.exe"
],
"darwin": [],
"linux": [
"/usr/local/Nuke13.2v1/Nuke13.2"
]
},
"arguments": {
"windows": [
"--studio"
],
"darwin": [
"--studio"
],
"linux": [
"--studio"
]
},
"environment": {}
},
"13-0": {
"use_python_2": false,
"executables": {
@ -555,6 +623,7 @@
"environment": {}
},
"__dynamic_keys_labels__": {
"13-2": "13.2",
"13-0": "13.0",
"12-2": "12.2",
"12-0": "12.0",
@ -573,6 +642,30 @@
"TAG_ASSETBUILD_STARTUP": "0"
},
"variants": {
"13-2": {
"use_python_2": false,
"executables": {
"windows": [
"C:\\Program Files\\Nuke13.2v1\\Nuke13.2.exe"
],
"darwin": [],
"linux": [
"/usr/local/Nuke13.2v1/Nuke13.2"
]
},
"arguments": {
"windows": [
"--hiero"
],
"darwin": [
"--hiero"
],
"linux": [
"--hiero"
]
},
"environment": {}
},
"13-0": {
"use_python_2": false,
"executables": {
@ -692,6 +785,7 @@
"environment": {}
},
"__dynamic_keys_labels__": {
"13-2": "13.2",
"13-0": "13.0",
"12-2": "12.2",
"12-0": "12.0",

View file

@ -304,6 +304,20 @@
"label": "Custom Tags",
"type": "list",
"object_type": "text"
},
{
"type": "label",
"label": "Use output <b>always</b> / only if input <b>is 1 frame</b> image / only if has <b>2+ frames</b> or <b>is video</b>"
},
{
"type": "enum",
"key": "single_frame_filter",
"default": "everytime",
"enum_items": [
{"everytime": "Always"},
{"single_frame": "Only if input has 1 image frame"},
{"multi_frame": "Only if input is video or sequence of frames"}
]
}
]
},

View file

@ -138,8 +138,7 @@ def save_studio_settings(data):
SaveWarningExc: If any module raises the exception.
"""
# Notify Pype modules
from openpype.modules import ModulesManager
from openpype_interfaces import ISettingsChangeListener
from openpype.modules import ModulesManager, ISettingsChangeListener
old_data = get_system_settings()
default_values = get_default_settings()[SYSTEM_SETTINGS_KEY]
@ -186,8 +185,7 @@ def save_project_settings(project_name, overrides):
SaveWarningExc: If any module raises the exception.
"""
# Notify Pype modules
from openpype.modules import ModulesManager
from openpype_interfaces import ISettingsChangeListener
from openpype.modules import ModulesManager, ISettingsChangeListener
default_values = get_default_settings()[PROJECT_SETTINGS_KEY]
if project_name:
@ -248,8 +246,7 @@ def save_project_anatomy(project_name, anatomy_data):
SaveWarningExc: If any module raises the exception.
"""
# Notify Pype modules
from openpype.modules import ModulesManager
from openpype_interfaces import ISettingsChangeListener
from openpype.modules import ModulesManager, ISettingsChangeListener
default_values = get_default_settings()[PROJECT_ANATOMY_KEY]
if project_name:

View file

@ -27,7 +27,7 @@
"bg": "#2C313A",
"bg-inputs": "#21252B",
"bg-buttons": "#434a56",
"bg-button-hover": "rgba(168, 175, 189, 0.3)",
"bg-button-hover": "rgb(81, 86, 97)",
"bg-inputs-disabled": "#2C313A",
"bg-buttons-disabled": "#434a56",

View file

@ -884,6 +884,26 @@ PublisherTabBtn[active="1"]:hover {
background: {color:bg};
}
PixmapButton{
border: 0px solid transparent;
border-radius: 0.2em;
background: {color:bg-buttons};
}
PixmapButton:hover {
background: {color:bg-button-hover};
}
PixmapButton:disabled {
background: {color:bg-buttons-disabled};
}
#ThumbnailPixmapHoverButton {
font-size: 11pt;
background: {color:bg-view};
}
#ThumbnailPixmapHoverButton:hover {
background: {color:bg-button-hover};
}
#CreatorDetailedDescription {
padding-left: 5px;
padding-right: 5px;
@ -911,11 +931,11 @@ PublisherTabBtn[active="1"]:hover {
#PublishLogConsole {
font-family: "Noto Sans Mono";
}
VariantInputsWidget QLineEdit {
#VariantInputsWidget QLineEdit {
border-bottom-right-radius: 0px;
border-top-right-radius: 0px;
}
VariantInputsWidget QToolButton {
#VariantInputsWidget QToolButton {
border-bottom-left-radius: 0px;
border-top-left-radius: 0px;
padding-top: 0.5em;

View file

@ -3,8 +3,14 @@ from .widgets import (
AttributeDefinitionsWidget,
)
from .dialog import (
AttributeDefinitionsDialog,
)
__all__ = (
"create_widget_for_attr_def",
"AttributeDefinitionsWidget",
"AttributeDefinitionsDialog",
)

View file

@ -0,0 +1,33 @@
from Qt import QtWidgets
from .widgets import AttributeDefinitionsWidget
class AttributeDefinitionsDialog(QtWidgets.QDialog):
def __init__(self, attr_defs, parent=None):
super(AttributeDefinitionsDialog, self).__init__(parent)
attrs_widget = AttributeDefinitionsWidget(attr_defs, self)
btns_widget = QtWidgets.QWidget(self)
ok_btn = QtWidgets.QPushButton("OK", btns_widget)
cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget)
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
btns_layout.setContentsMargins(0, 0, 0, 0)
btns_layout.addStretch(1)
btns_layout.addWidget(ok_btn, 0)
btns_layout.addWidget(cancel_btn, 0)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(attrs_widget, 0)
main_layout.addStretch(1)
main_layout.addWidget(btns_widget, 0)
ok_btn.clicked.connect(self.accept)
cancel_btn.clicked.connect(self.reject)
self._attrs_widget = attrs_widget
def get_values(self):
return self._attrs_widget.current_value()

View file

@ -164,9 +164,9 @@ class ExperimentalTools:
def _show_publisher(self):
if self._publisher_tool is None:
from openpype.tools import publisher
from openpype.tools.publisher.window import PublisherWindow
self._publisher_tool = publisher.PublisherWindow(
self._publisher_tool = PublisherWindow(
parent=self._parent_widget
)

View file

@ -2,6 +2,8 @@ import inspect
from Qt import QtGui
import qtawesome
from openpype.lib.attribute_definitions import AbtractAttrDef
from openpype.tools.attribute_defs import AttributeDefinitionsDialog
from openpype.tools.utils.widgets import (
OptionalAction,
OptionDialog
@ -34,21 +36,30 @@ def get_options(action, loader, parent, repre_contexts):
None when dialog was closed or cancelled, in all other cases {}
if no options
"""
# Pop option dialog
options = {}
loader_options = loader.get_options(repre_contexts)
if getattr(action, "optioned", False) and loader_options:
if not getattr(action, "optioned", False) or not loader_options:
return options
if isinstance(loader_options[0], AbtractAttrDef):
qargparse_options = False
dialog = AttributeDefinitionsDialog(loader_options, parent)
else:
qargparse_options = True
dialog = OptionDialog(parent)
dialog.setWindowTitle(action.label + " Options")
dialog.create(loader_options)
if not dialog.exec_():
return None
dialog.setWindowTitle(action.label + " Options")
# Get option
options = dialog.parse()
if not dialog.exec_():
return None
return options
# Get option
if qargparse_options:
return dialog.parse()
return dialog.get_values()
def add_representation_loaders_to_menu(loaders, menu, repre_contexts):

View file

@ -515,7 +515,7 @@ class SubsetWidget(QtWidgets.QWidget):
if not one_item_selected:
# Filter loaders from first subset by intersected combinations
for repre, loader in first_loaders:
if (repre["name"], loader) not in found_combinations:
if (repre["name"].lower(), loader) not in found_combinations:
continue
loaders.append((repre, loader))

View file

@ -20,9 +20,10 @@ INSTANCE_ID_ROLE = QtCore.Qt.UserRole + 1
SORT_VALUE_ROLE = QtCore.Qt.UserRole + 2
IS_GROUP_ROLE = QtCore.Qt.UserRole + 3
CREATOR_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 4
FAMILY_ROLE = QtCore.Qt.UserRole + 5
GROUP_ROLE = QtCore.Qt.UserRole + 6
CONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 7
CREATOR_THUMBNAIL_ENABLED_ROLE = QtCore.Qt.UserRole + 5
FAMILY_ROLE = QtCore.Qt.UserRole + 6
GROUP_ROLE = QtCore.Qt.UserRole + 7
CONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 8
__all__ = (

View file

@ -4,6 +4,8 @@ import logging
import traceback
import collections
import uuid
import tempfile
import shutil
from abc import ABCMeta, abstractmethod, abstractproperty
import six
@ -24,6 +26,7 @@ from openpype.pipeline import (
KnownPublishError,
registered_host,
legacy_io,
get_process_id,
)
from openpype.pipeline.create import (
CreateContext,
@ -87,9 +90,9 @@ class AssetDocsCache:
return
project_name = self._controller.project_name
asset_docs = get_assets(
asset_docs = list(get_assets(
project_name, fields=self.projection.keys()
)
))
asset_docs_by_name = {}
task_names_by_asset_name = {}
for asset_doc in asset_docs:
@ -825,6 +828,7 @@ class CreatorItem:
default_variant,
default_variants,
create_allow_context_change,
create_allow_thumbnail,
pre_create_attributes_defs
):
self.identifier = identifier
@ -838,6 +842,7 @@ class CreatorItem:
self.default_variant = default_variant
self.default_variants = default_variants
self.create_allow_context_change = create_allow_context_change
self.create_allow_thumbnail = create_allow_thumbnail
self.instance_attributes_defs = instance_attributes_defs
self.pre_create_attributes_defs = pre_create_attributes_defs
@ -864,6 +869,7 @@ class CreatorItem:
default_variants = None
pre_create_attr_defs = None
create_allow_context_change = None
create_allow_thumbnail = None
if creator_type is CreatorTypes.artist:
description = creator.get_description()
detail_description = creator.get_detail_description()
@ -871,6 +877,7 @@ class CreatorItem:
default_variants = creator.get_default_variants()
pre_create_attr_defs = creator.get_pre_create_attr_defs()
create_allow_context_change = creator.create_allow_context_change
create_allow_thumbnail = creator.create_allow_thumbnail
identifier = creator.identifier
return cls(
@ -886,6 +893,7 @@ class CreatorItem:
default_variant,
default_variants,
create_allow_context_change,
create_allow_thumbnail,
pre_create_attr_defs
)
@ -914,6 +922,7 @@ class CreatorItem:
"default_variant": self.default_variant,
"default_variants": self.default_variants,
"create_allow_context_change": self.create_allow_context_change,
"create_allow_thumbnail": self.create_allow_thumbnail,
"instance_attributes_defs": instance_attributes_defs,
"pre_create_attributes_defs": pre_create_attributes_defs,
}
@ -1115,11 +1124,13 @@ class AbstractPublisherController(object):
pass
@abstractmethod
def save_changes(self):
"""Save changes in create context."""
pass
@abstractmethod
def remove_instances(self, instance_ids):
"""Remove list of instances from create context."""
# TODO expect instance ids
@ -1256,6 +1267,14 @@ class AbstractPublisherController(object):
def trigger_convertor_items(self, convertor_identifiers):
pass
@abstractmethod
def get_thumbnail_paths_for_instances(self, instance_ids):
pass
@abstractmethod
def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping):
pass
@abstractmethod
def set_comment(self, comment):
"""Set comment on pyblish context.
@ -1283,6 +1302,22 @@ class AbstractPublisherController(object):
pass
@abstractmethod
def get_thumbnail_temp_dir_path(self):
"""Return path to directory where thumbnails can be temporary stored.
Returns:
str: Path to a directory.
"""
pass
@abstractmethod
def clear_thumbnail_temp_dir_path(self):
"""Remove content of thumbnail temp directory."""
pass
class BasePublisherController(AbstractPublisherController):
"""Implement common logic for controllers.
@ -1523,6 +1558,26 @@ class BasePublisherController(AbstractPublisherController):
return creator_item.icon
return None
def get_thumbnail_temp_dir_path(self):
"""Return path to directory where thumbnails can be temporary stored.
Returns:
str: Path to a directory.
"""
return os.path.join(
tempfile.gettempdir(),
"publisher_thumbnails",
get_process_id()
)
def clear_thumbnail_temp_dir_path(self):
"""Remove content of thumbnail temp directory."""
dirpath = self.get_thumbnail_temp_dir_path()
if os.path.exists(dirpath):
shutil.rmtree(dirpath)
class PublisherController(BasePublisherController):
"""Middleware between UI, CreateContext and publish Context.
@ -1778,6 +1833,29 @@ class PublisherController(BasePublisherController):
self._on_create_instance_change()
def get_thumbnail_paths_for_instances(self, instance_ids):
thumbnail_paths_by_instance_id = (
self._create_context.thumbnail_paths_by_instance_id
)
return {
instance_id: thumbnail_paths_by_instance_id.get(instance_id)
for instance_id in instance_ids
}
def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping):
thumbnail_paths_by_instance_id = (
self._create_context.thumbnail_paths_by_instance_id
)
for instance_id, thumbnail_path in thumbnail_path_mapping.items():
thumbnail_paths_by_instance_id[instance_id] = thumbnail_path
self._emit_event(
"instance.thumbnail.changed",
{
"mapping": thumbnail_path_mapping
}
)
def emit_card_message(
self, message, message_type=CardMessageTypes.standard
):

View file

@ -115,6 +115,11 @@ class QtRemotePublishController(BasePublisherController):
super().__init__(*args, **kwargs)
self._created_instances = {}
self._thumbnail_paths_by_instance_id = None
def _reset_attributes(self):
super()._reset_attributes()
self._thumbnail_paths_by_instance_id = None
@abstractmethod
def _get_serialized_instances(self):
@ -180,6 +185,11 @@ class QtRemotePublishController(BasePublisherController):
self.host_is_valid = event["value"]
return
# Don't skip because UI want know about it too
if event.topic == "instance.thumbnail.changed":
for instance_id, path in event["mapping"].items():
self.thumbnail_paths_by_instance_id[instance_id] = path
# Topics that can be just passed by because are not affecting
# controller itself
# - "show.card.message"
@ -256,6 +266,42 @@ class QtRemotePublishController(BasePublisherController):
def get_existing_subset_names(self, asset_name):
pass
@property
def thumbnail_paths_by_instance_id(self):
if self._thumbnail_paths_by_instance_id is None:
self._thumbnail_paths_by_instance_id = (
self._collect_thumbnail_paths_by_instance_id()
)
return self._thumbnail_paths_by_instance_id
def get_thumbnail_path_for_instance(self, instance_id):
return self.thumbnail_paths_by_instance_id.get(instance_id)
def set_thumbnail_path_for_instance(self, instance_id, thumbnail_path):
self._set_thumbnail_path_on_context(self, instance_id, thumbnail_path)
@abstractmethod
def _collect_thumbnail_paths_by_instance_id(self):
"""Collect thumbnail paths by instance id in remote controller.
These should be collected from 'CreatedContext' there.
Returns:
Dict[str, str]: Mapping of thumbnail path by instance id.
"""
pass
@abstractmethod
def _set_thumbnail_path_on_context(self, instance_id, thumbnail_path):
"""Send change of thumbnail path in remote controller.
That should trigger event 'instance.thumbnail.changed' which is
captured and handled in default implementation in this class.
"""
pass
@abstractmethod
def get_subset_name(
self,

View file

@ -1,11 +1,12 @@
import os
import json
import six
import uuid
import appdirs
from Qt import QtWidgets, QtCore, QtGui
from openpype import style
from openpype.lib import JSONSettingRegistry
from openpype.resources import get_openpype_icon_filepath
from openpype.tools import resources
from openpype.tools.utils import (
@ -23,38 +24,198 @@ else:
from report_items import PublishReport
FILEPATH_ROLE = QtCore.Qt.UserRole + 1
MODIFIED_ROLE = QtCore.Qt.UserRole + 2
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
class PublisherReportRegistry(JSONSettingRegistry):
"""Class handling storing publish report tool.
Attributes:
vendor (str): Name used for path construction.
product (str): Additional name used for path construction.
def get_reports_dir():
"""Root directory where publish reports are stored for next session.
Returns:
str: Path to directory where reports are stored.
"""
report_dir = os.path.join(
appdirs.user_data_dir("openpype", "pypeclub"),
"publish_report_viewer"
)
if not os.path.exists(report_dir):
os.makedirs(report_dir)
return report_dir
class PublishReportItem:
"""Report item representing one file in report directory."""
def __init__(self, content):
item_id = content.get("id")
changed = False
if not item_id:
item_id = str(uuid.uuid4())
changed = True
content["id"] = item_id
if not content.get("report_version"):
changed = True
content["report_version"] = "0.0.1"
report_path = os.path.join(get_reports_dir(), item_id)
file_modified = None
if os.path.exists(report_path):
file_modified = os.path.getmtime(report_path)
self.content = content
self.report_path = report_path
self.file_modified = file_modified
self._loaded_label = content.get("label")
self._changed = changed
self.publish_report = PublishReport(content)
@property
def version(self):
return self.content["report_version"]
@property
def id(self):
return self.content["id"]
def get_label(self):
return self.content.get("label") or "Unfilled label"
def set_label(self, label):
if not label:
self.content.pop("label", None)
self.content["label"] = label
label = property(get_label, set_label)
def save(self):
save = False
if (
self._changed
or self._loaded_label != self.label
or not os.path.exists(self.report_path)
or self.file_modified != os.path.getmtime(self.report_path)
):
save = True
if not save:
return
with open(self.report_path, "w") as stream:
json.dump(self.content, stream)
self._loaded_label = self.content.get("label")
self._changed = False
self.file_modified = os.path.getmtime(self.report_path)
@classmethod
def from_filepath(cls, filepath):
if not os.path.exists(filepath):
return None
try:
with open(filepath, "r") as stream:
content = json.load(stream)
return cls(content)
except Exception:
return None
def remove_file(self):
if os.path.exists(self.report_path):
os.remove(self.report_path)
def update_file_content(self):
if not os.path.exists(self.report_path):
return
file_modified = os.path.getmtime(self.report_path)
if file_modified == self.file_modified:
return
with open(self.report_path, "r") as stream:
content = json.load(self.content, stream)
item_id = content.get("id")
version = content.get("report_version")
if not item_id:
item_id = str(uuid.uuid4())
content["id"] = item_id
if not version:
version = "0.0.1"
content["report_version"] = version
self.content = content
self.file_modified = file_modified
class PublisherReportHandler:
"""Class handling storing publish report tool."""
def __init__(self):
self.vendor = "pypeclub"
self.product = "openpype"
name = "publish_report_viewer"
path = appdirs.user_data_dir(self.product, self.vendor)
super(PublisherReportRegistry, self).__init__(name, path)
self._reports = None
self._reports_by_id = {}
def reset(self):
self._reports = None
self._reports_by_id = {}
def list_reports(self):
if self._reports is not None:
return self._reports
reports = []
reports_by_id = {}
report_dir = get_reports_dir()
for filename in os.listdir(report_dir):
ext = os.path.splitext(filename)[-1]
if ext == ".json":
continue
filepath = os.path.join(report_dir, filename)
item = PublishReportItem.from_filepath(filepath)
reports.append(item)
reports_by_id[item.id] = item
self._reports = reports
self._reports_by_id = reports_by_id
return reports
def remove_report_items(self, item_id):
item = self._reports_by_id.get(item_id)
if item:
try:
item.remove_file()
self._reports_by_id.get(item_id)
except Exception:
pass
class LoadedFilesMopdel(QtGui.QStandardItemModel):
class LoadedFilesModel(QtGui.QStandardItemModel):
def __init__(self, *args, **kwargs):
super(LoadedFilesMopdel, self).__init__(*args, **kwargs)
self.setColumnCount(2)
self._items_by_filepath = {}
self._reports_by_filepath = {}
super(LoadedFilesModel, self).__init__(*args, **kwargs)
self._registry = PublisherReportRegistry()
self._items_by_id = {}
self._report_items_by_id = {}
self._handler = PublisherReportHandler()
self._loading_registry = False
self._load_registry()
def refresh(self):
self._handler.reset()
self._items_by_id = {}
self._report_items_by_id = {}
new_items = []
for report_item in self._handler.list_reports():
item = self._create_item(report_item)
self._report_items_by_id[report_item.id] = report_item
self._items_by_id[report_item.id] = item
new_items.append(item)
if new_items:
root_item = self.invisibleRootItem()
root_item.appendRows(new_items)
def headerData(self, section, orientation, role):
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
@ -63,22 +224,7 @@ class LoadedFilesMopdel(QtGui.QStandardItemModel):
if section == 1:
return "Modified"
return ""
super(LoadedFilesMopdel, self).headerData(section, orientation, role)
def _load_registry(self):
self._loading_registry = True
try:
filepaths = self._registry.get_item("filepaths")
self.add_filepaths(filepaths)
except ValueError:
pass
self._loading_registry = False
def _store_registry(self):
if self._loading_registry:
return
filepaths = list(self._items_by_filepath.keys())
self._registry.set_item("filepaths", filepaths)
super(LoadedFilesModel, self).headerData(section, orientation, role)
def data(self, index, role=None):
if role is None:
@ -88,17 +234,28 @@ class LoadedFilesMopdel(QtGui.QStandardItemModel):
if col != 0:
index = self.index(index.row(), 0, index.parent())
if role == QtCore.Qt.ToolTipRole:
if col == 0:
role = FILEPATH_ROLE
elif col == 1:
return "File modified"
return super(LoadedFilesModel, self).data(index, role)
def setData(self, index, value, role):
if role == QtCore.Qt.EditRole:
item_id = index.data(ITEM_ID_ROLE)
report_item = self._report_items_by_id.get(item_id)
if report_item is not None:
report_item.label = value
report_item.save()
value = report_item.label
return super(LoadedFilesModel, self).setData(index, value, role)
def _create_item(self, report_item):
if report_item.id in self._items_by_id:
return None
elif role == QtCore.Qt.DisplayRole:
if col == 1:
role = MODIFIED_ROLE
return super(LoadedFilesMopdel, self).data(index, role)
item = QtGui.QStandardItem(report_item.label)
item.setColumnCount(self.columnCount())
item.setData(report_item.id, ITEM_ID_ROLE)
return item
def add_filepaths(self, filepaths):
if not filepaths:
@ -110,9 +267,6 @@ class LoadedFilesMopdel(QtGui.QStandardItemModel):
filtered_paths = []
for filepath in filepaths:
normalized_path = os.path.normpath(filepath)
if normalized_path in self._items_by_filepath:
continue
if (
os.path.exists(normalized_path)
and normalized_path not in filtered_paths
@ -127,54 +281,46 @@ class LoadedFilesMopdel(QtGui.QStandardItemModel):
try:
with open(normalized_path, "r") as stream:
data = json.load(stream)
report = PublishReport(data)
report_item = PublishReportItem(data)
except Exception:
# TODO handle errors
continue
modified = os.path.getmtime(normalized_path)
item = QtGui.QStandardItem(os.path.basename(normalized_path))
item.setColumnCount(self.columnCount())
item.setData(normalized_path, FILEPATH_ROLE)
item.setData(modified, MODIFIED_ROLE)
label = data.get("label")
if not label:
report_item.label = (
os.path.splitext(os.path.basename(filepath))[0]
)
item = self._create_item(report_item)
if item is None:
continue
new_items.append(item)
self._items_by_filepath[normalized_path] = item
self._reports_by_filepath[normalized_path] = report
report_item.save()
self._items_by_id[report_item.id] = item
self._report_items_by_id[report_item.id] = report_item
if not new_items:
if new_items:
root_item = self.invisibleRootItem()
root_item.appendRows(new_items)
def remove_item_by_id(self, item_id):
report_item = self._report_items_by_id.get(item_id)
if not report_item:
return
self._handler.remove_report_items(item_id)
item = self._items_by_id.get(item_id)
parent = self.invisibleRootItem()
parent.appendRows(new_items)
parent.removeRow(item.row())
self._store_registry()
def remove_filepaths(self, filepaths):
if not filepaths:
return
if isinstance(filepaths, six.string_types):
filepaths = [filepaths]
filtered_paths = []
for filepath in filepaths:
normalized_path = os.path.normpath(filepath)
if normalized_path in self._items_by_filepath:
filtered_paths.append(normalized_path)
if not filtered_paths:
return
parent = self.invisibleRootItem()
for filepath in filtered_paths:
self._reports_by_filepath.pop(normalized_path)
item = self._items_by_filepath.pop(filepath)
parent.removeRow(item.row())
self._store_registry()
def get_report_by_filepath(self, filepath):
return self._reports_by_filepath.get(filepath)
def get_report_by_id(self, item_id):
report_item = self._report_items_by_id.get(item_id)
if report_item:
return report_item.publish_report
return None
class LoadedFilesView(QtWidgets.QTreeView):
@ -182,11 +328,13 @@ class LoadedFilesView(QtWidgets.QTreeView):
def __init__(self, *args, **kwargs):
super(LoadedFilesView, self).__init__(*args, **kwargs)
self.setEditTriggers(self.NoEditTriggers)
self.setEditTriggers(
self.EditKeyPressed | self.SelectedClicked | self.DoubleClicked
)
self.setIndentation(0)
self.setAlternatingRowColors(True)
model = LoadedFilesMopdel()
model = LoadedFilesModel()
self.setModel(model)
time_delegate = PrettyTimeDelegate()
@ -226,9 +374,10 @@ class LoadedFilesView(QtWidgets.QTreeView):
def showEvent(self, event):
super(LoadedFilesView, self).showEvent(event)
self._update_remove_btn()
self._model.refresh()
header = self.header()
header.resizeSections(header.ResizeToContents)
self._update_remove_btn()
def _on_selection_change(self):
self.selection_changed.emit()
@ -237,14 +386,14 @@ class LoadedFilesView(QtWidgets.QTreeView):
self._model.add_filepaths(filepaths)
self._fill_selection()
def remove_filepaths(self, filepaths):
self._model.remove_filepaths(filepaths)
def remove_item_by_id(self, item_id):
self._model.remove_item_by_id(item_id)
self._fill_selection()
def _on_remove_clicked(self):
index = self.currentIndex()
filepath = index.data(FILEPATH_ROLE)
self.remove_filepaths(filepath)
item_id = index.data(ITEM_ID_ROLE)
self.remove_item_by_id(item_id)
def _fill_selection(self):
index = self.currentIndex()
@ -257,8 +406,8 @@ class LoadedFilesView(QtWidgets.QTreeView):
def get_current_report(self):
index = self.currentIndex()
filepath = index.data(FILEPATH_ROLE)
return self._model.get_report_by_filepath(filepath)
item_id = index.data(ITEM_ID_ROLE)
return self._model.get_report_by_id(item_id)
class LoadedFilesWidget(QtWidgets.QWidget):

View file

@ -1,15 +1,14 @@
import sys
import re
import traceback
from Qt import QtWidgets, QtCore, QtGui
from openpype.pipeline.create import (
CreatorError,
SUBSET_NAME_ALLOWED_SYMBOLS,
PRE_CREATE_THUMBNAIL_KEY,
TaskNotSetError,
)
from .thumbnail_widget import ThumbnailWidget
from .widgets import (
IconValuePixmapLabel,
CreateBtn,
@ -20,17 +19,18 @@ from .precreate_widget import PreCreateWidget
from ..constants import (
VARIANT_TOOLTIP,
CREATOR_IDENTIFIER_ROLE,
FAMILY_ROLE
FAMILY_ROLE,
CREATOR_THUMBNAIL_ENABLED_ROLE,
)
SEPARATORS = ("---separator---", "---")
class VariantInputsWidget(QtWidgets.QWidget):
class ResizeControlWidget(QtWidgets.QWidget):
resized = QtCore.Signal()
def resizeEvent(self, event):
super(VariantInputsWidget, self).resizeEvent(event)
super(ResizeControlWidget, self).resizeEvent(event)
self.resized.emit()
@ -153,13 +153,20 @@ class CreateWidget(QtWidgets.QWidget):
# --- Creator attr defs ---
creators_attrs_widget = QtWidgets.QWidget(creators_splitter)
# Top part - variant / subset name + thumbnail
creators_attrs_top = QtWidgets.QWidget(creators_attrs_widget)
# Basics - variant / subset name
creator_basics_widget = ResizeControlWidget(creators_attrs_top)
variant_subset_label = QtWidgets.QLabel(
"Create options", creators_attrs_widget
"Create options", creator_basics_widget
)
variant_subset_widget = QtWidgets.QWidget(creators_attrs_widget)
variant_subset_widget = QtWidgets.QWidget(creator_basics_widget)
# Variant and subset input
variant_widget = VariantInputsWidget(creators_attrs_widget)
variant_widget = ResizeControlWidget(variant_subset_widget)
variant_widget.setObjectName("VariantInputsWidget")
variant_input = QtWidgets.QLineEdit(variant_widget)
variant_input.setObjectName("VariantInput")
@ -186,6 +193,18 @@ class CreateWidget(QtWidgets.QWidget):
variant_subset_layout.addRow("Variant", variant_widget)
variant_subset_layout.addRow("Subset", subset_name_input)
creator_basics_layout = QtWidgets.QVBoxLayout(creator_basics_widget)
creator_basics_layout.setContentsMargins(0, 0, 0, 0)
creator_basics_layout.addWidget(variant_subset_label, 0)
creator_basics_layout.addWidget(variant_subset_widget, 0)
thumbnail_widget = ThumbnailWidget(controller, creators_attrs_top)
creators_attrs_top_layout = QtWidgets.QHBoxLayout(creators_attrs_top)
creators_attrs_top_layout.setContentsMargins(0, 0, 0, 0)
creators_attrs_top_layout.addWidget(creator_basics_widget, 1)
creators_attrs_top_layout.addWidget(thumbnail_widget, 0)
# Precreate attributes widget
pre_create_widget = PreCreateWidget(creators_attrs_widget)
@ -201,8 +220,7 @@ class CreateWidget(QtWidgets.QWidget):
creators_attrs_layout = QtWidgets.QVBoxLayout(creators_attrs_widget)
creators_attrs_layout.setContentsMargins(0, 0, 0, 0)
creators_attrs_layout.addWidget(variant_subset_label, 0)
creators_attrs_layout.addWidget(variant_subset_widget, 0)
creators_attrs_layout.addWidget(creators_attrs_top, 0)
creators_attrs_layout.addWidget(pre_create_widget, 1)
creators_attrs_layout.addWidget(create_btn_wrapper, 0)
@ -240,6 +258,7 @@ class CreateWidget(QtWidgets.QWidget):
create_btn.clicked.connect(self._on_create)
variant_widget.resized.connect(self._on_variant_widget_resize)
creator_basics_widget.resized.connect(self._on_creator_basics_resize)
variant_input.returnPressed.connect(self._on_create)
variant_input.textChanged.connect(self._on_variant_change)
creators_view.selectionModel().currentChanged.connect(
@ -252,6 +271,8 @@ class CreateWidget(QtWidgets.QWidget):
self._on_current_session_context_request
)
tasks_widget.task_changed.connect(self._on_task_change)
thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)
thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear)
controller.event_system.add_callback(
"plugins.refresh.finished", self._on_plugins_refresh
@ -278,11 +299,14 @@ class CreateWidget(QtWidgets.QWidget):
self._create_btn = create_btn
self._creator_short_desc_widget = creator_short_desc_widget
self._creator_basics_widget = creator_basics_widget
self._thumbnail_widget = thumbnail_widget
self._pre_create_widget = pre_create_widget
self._attr_separator_widget = attr_separator_widget
self._prereq_timer = prereq_timer
self._first_show = True
self._last_thumbnail_path = None
@property
def current_asset_name(self):
@ -434,6 +458,10 @@ class CreateWidget(QtWidgets.QWidget):
item.setData(creator_item.label, QtCore.Qt.DisplayRole)
item.setData(identifier, CREATOR_IDENTIFIER_ROLE)
item.setData(
creator_item.create_allow_thumbnail,
CREATOR_THUMBNAIL_ENABLED_ROLE
)
item.setData(creator_item.family, FAMILY_ROLE)
# Remove families that are no more available
@ -473,6 +501,13 @@ class CreateWidget(QtWidgets.QWidget):
if self._context_change_is_enabled():
self._invalidate_prereq_deffered()
def _on_thumbnail_create(self, thumbnail_path):
self._last_thumbnail_path = thumbnail_path
self._thumbnail_widget.set_current_thumbnails([thumbnail_path])
def _on_thumbnail_clear(self):
self._last_thumbnail_path = None
def _on_current_session_context_request(self):
self._assets_widget.set_current_session_asset()
task_name = self.current_task_name
@ -527,6 +562,10 @@ class CreateWidget(QtWidgets.QWidget):
self._set_context_enabled(creator_item.create_allow_context_change)
self._refresh_asset()
self._thumbnail_widget.setVisible(
creator_item.create_allow_thumbnail
)
default_variants = creator_item.default_variants
if not default_variants:
default_variants = ["Main"]
@ -684,6 +723,11 @@ class CreateWidget(QtWidgets.QWidget):
self._first_show = False
self._on_first_show()
def _on_creator_basics_resize(self):
self._thumbnail_widget.set_height(
self._creator_basics_widget.sizeHint().height()
)
def _on_create(self):
indexes = self._creators_view.selectedIndexes()
if not indexes or len(indexes) > 1:
@ -706,6 +750,11 @@ class CreateWidget(QtWidgets.QWidget):
task_name = self._get_task_name()
pre_create_data = self._pre_create_widget.current_value()
if index.data(CREATOR_THUMBNAIL_ENABLED_ROLE):
pre_create_data[PRE_CREATE_THUMBNAIL_KEY] = (
self._last_thumbnail_path
)
# Where to define these data?
# - what data show be stored?
instance_data = {
@ -725,3 +774,5 @@ class CreateWidget(QtWidgets.QWidget):
if success:
self._set_creator(self._selected_creator)
self._controller.emit_card_message("Creation finished...")
self._last_thumbnail_path = None
self._thumbnail_widget.set_current_thumbnails()

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,6 +1,6 @@
from Qt import QtWidgets, QtCore
from openpype.widgets.attribute_defs import create_widget_for_attr_def
from openpype.tools.attribute_defs import create_widget_for_attr_def
class PreCreateWidget(QtWidgets.QWidget):

Some files were not shown because too many files have changed in this diff Show more