Merge branch 'main' into workflow/main-milestones

This commit is contained in:
Jakub Jezek 2022-11-10 17:50:25 +01:00
commit a70a9a3ea6
No known key found for this signature in database
GPG key ID: 730D7C02726179A7
117 changed files with 3408 additions and 1075 deletions

View file

@ -2,7 +2,7 @@ name: Milestone - assign to PRs
on:
pull_request_target:
types: [opened, reopened, edited]
types: [opened, reopened, edited, synchronize]
jobs:
run_if_release:

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

@ -815,6 +815,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

@ -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,3 +1,4 @@
import os
import nuke
import qargparse
@ -84,6 +85,16 @@ class LoadClip(plugin.NukeLoader):
+ plugin.get_review_presets_config()
)
def _fix_path_for_knob(self, filepath, repre_cont):
basename = os.path.basename(filepath)
dirname = os.path.dirname(filepath)
frame = repre_cont.get("frame")
assert frame, "Representation is not sequence"
padding = len(str(frame))
basename = basename.replace(frame, "#" * padding)
return os.path.join(dirname, basename).replace("\\", "/")
def load(self, context, name, namespace, options):
repre = context["representation"]
# reste container id so it is always unique for each instance
@ -91,7 +102,7 @@ class LoadClip(plugin.NukeLoader):
is_sequence = len(repre["files"]) > 1
file = self.fname.replace("\\", "/")
filepath = self.fname.replace("\\", "/")
start_at_workfile = options.get(
"start_at_workfile", self.options_defaults["start_at_workfile"])
@ -121,18 +132,14 @@ 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)
elif "#" not in filepath:
filepath = self._fix_path_for_knob(filepath, repre_cont)
# 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
@ -147,7 +154,7 @@ 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"])
@ -218,7 +225,7 @@ class LoadClip(plugin.NukeLoader):
is_sequence = len(representation["files"]) > 1
read_node = nuke.toNode(container['objectName'])
file = get_representation_path(representation).replace("\\", "/")
filepath = get_representation_path(representation).replace("\\", "/")
start_at_workfile = "start at" in read_node['frame_mode'].value()
@ -251,14 +258,10 @@ 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"
elif "#" not in filepath:
filepath = self._fix_path_for_knob(filepath, repre_cont)
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 +269,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)

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

@ -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

@ -24,6 +24,7 @@ from .creator_plugins import (
Creator,
AutoCreator,
discover_creator_plugins,
discover_convertor_plugins,
CreatorError,
)
@ -70,6 +71,41 @@ class HostMissRequiredMethod(Exception):
super(HostMissRequiredMethod, self).__init__(msg)
class ConvertorsOperationFailed(Exception):
def __init__(self, msg, failed_info):
super(ConvertorsOperationFailed, self).__init__(msg)
self.failed_info = failed_info
class ConvertorsFindFailed(ConvertorsOperationFailed):
def __init__(self, failed_info):
msg = "Failed to find incompatible subsets"
super(ConvertorsFindFailed, self).__init__(
msg, failed_info
)
class ConvertorsConversionFailed(ConvertorsOperationFailed):
def __init__(self, failed_info):
msg = "Failed to convert incompatible subsets"
super(ConvertorsConversionFailed, self).__init__(
msg, failed_info
)
def prepare_failed_convertor_operation_info(identifier, exc_info):
exc_type, exc_value, exc_traceback = exc_info
formatted_traceback = "".join(traceback.format_exception(
exc_type, exc_value, exc_traceback
))
return {
"convertor_identifier": identifier,
"message": str(exc_value),
"traceback": formatted_traceback
}
class CreatorsOperationFailed(Exception):
"""Raised when a creator process crashes in 'CreateContext'.
@ -926,6 +962,37 @@ class CreatedInstance:
self[key] = new_value
class ConvertorItem(object):
"""Item representing convertor plugin.
Args:
identifier (str): Identifier of convertor.
label (str): Label which will be shown in UI.
"""
def __init__(self, identifier, label):
self._id = str(uuid4())
self.identifier = identifier
self.label = label
@property
def id(self):
return self._id
def to_data(self):
return {
"id": self.id,
"identifier": self.identifier,
"label": self.label
}
@classmethod
def from_data(cls, data):
obj = cls(data["identifier"], data["label"])
obj._id = data["id"]
return obj
class CreateContext:
"""Context of instance creation.
@ -991,6 +1058,9 @@ class CreateContext:
# Manual creators
self.manual_creators = {}
self.convertors_plugins = {}
self.convertor_items_by_id = {}
self.publish_discover_result = None
self.publish_plugins_mismatch_targets = []
self.publish_plugins = []
@ -1007,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)
@ -1071,10 +1143,34 @@ class CreateContext:
with self.bulk_instances_collection():
self.reset_instances()
self.find_convertor_items()
self.execute_autocreators()
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."""
@ -1086,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.
@ -1125,6 +1222,12 @@ class CreateContext:
Reloads creators from preregistered paths and can load publish plugins
if it's enabled on context.
"""
self._reset_publish_plugins(discover_publish_plugins)
self._reset_creator_plugins()
self._reset_convertor_plugins()
def _reset_publish_plugins(self, discover_publish_plugins):
import pyblish.logic
from openpype.pipeline import OpenPypePyblishPluginMixin
@ -1166,6 +1269,7 @@ class CreateContext:
self.publish_plugins = plugins_by_targets
self.plugins_with_defs = plugins_with_defs
def _reset_creator_plugins(self):
# Prepare settings
system_settings = get_system_settings()
project_settings = get_project_settings(self.project_name)
@ -1217,6 +1321,27 @@ class CreateContext:
self.creators = creators
def _reset_convertor_plugins(self):
convertors_plugins = {}
for convertor_class in discover_convertor_plugins():
if inspect.isabstract(convertor_class):
self.log.info(
"Skipping abstract Creator {}".format(str(convertor_class))
)
continue
convertor_identifier = convertor_class.identifier
if convertor_identifier in convertors_plugins:
self.log.warning((
"Duplicated Converter identifier. "
"Using first and skipping following"
))
continue
convertors_plugins[convertor_identifier] = convertor_class(self)
self.convertors_plugins = convertors_plugins
def reset_context_data(self):
"""Reload context data using host implementation.
@ -1346,6 +1471,14 @@ class CreateContext:
self._instances_by_id.pop(instance.id, None)
def add_convertor_item(self, convertor_identifier, label):
self.convertor_items_by_id[convertor_identifier] = ConvertorItem(
convertor_identifier, label
)
def remove_convertor_item(self, convertor_identifier):
self.convertor_items_by_id.pop(convertor_identifier, None)
@contextmanager
def bulk_instances_collection(self):
"""Validate context of instances in bulk.
@ -1413,6 +1546,37 @@ class CreateContext:
if failed_info:
raise CreatorsCollectionFailed(failed_info)
def find_convertor_items(self):
"""Go through convertor plugins to look for items to convert.
Raises:
ConvertorsFindFailed: When one or more convertors fails during
finding.
"""
self.convertor_items_by_id = {}
failed_info = []
for convertor in self.convertors_plugins.values():
try:
convertor.find_instances()
except:
failed_info.append(
prepare_failed_convertor_operation_info(
convertor.identifier, sys.exc_info()
)
)
self.log.warning(
"Failed to find instances of convertor \"{}\"".format(
convertor.identifier
),
exc_info=True
)
if failed_info:
raise ConvertorsFindFailed(failed_info)
def execute_autocreators(self):
"""Execute discovered AutoCreator plugins.
@ -1668,3 +1832,51 @@ class CreateContext:
"Accessed Collection shared data out of collection phase"
)
return self._collection_shared_data
def run_convertor(self, convertor_identifier):
"""Run convertor plugin by it's idenfitifier.
Conversion is skipped if convertor is not available.
Args:
convertor_identifier (str): Identifier of convertor.
"""
convertor = self.convertors_plugins.get(convertor_identifier)
if convertor is not None:
convertor.convert()
def run_convertors(self, convertor_identifiers):
"""Run convertor plugins by idenfitifiers.
Conversion is skipped if convertor is not available. It is recommended
to trigger reset after conversion to reload instances.
Args:
convertor_identifiers (Iterator[str]): Identifiers of convertors
to run.
Raises:
ConvertorsConversionFailed: When one or more convertors fails.
"""
failed_info = []
for convertor_identifier in convertor_identifiers:
try:
self.run_convertor(convertor_identifier)
except:
failed_info.append(
prepare_failed_convertor_operation_info(
convertor_identifier, sys.exc_info()
)
)
self.log.warning(
"Failed to convert instances of convertor \"{}\"".format(
convertor_identifier
),
exc_info=True
)
if failed_info:
raise ConvertorsConversionFailed(failed_info)

View file

@ -1,5 +1,6 @@
import os
import copy
import collections
from abc import (
ABCMeta,
@ -33,6 +34,111 @@ class CreatorError(Exception):
super(CreatorError, self).__init__(message)
@six.add_metaclass(ABCMeta)
class SubsetConvertorPlugin(object):
"""Helper for conversion of instances created using legacy creators.
Conversion from legacy creators would mean to loose legacy instances,
convert them automatically or write a script which must user run. All of
these solutions are workign but will happen without asking or user must
know about them. This plugin can be used to show legacy instances in
Publisher and give user ability to run conversion script.
Convertor logic should be very simple. Method 'find_instances' is to
look for legacy instances in scene a possibly call
pre-implemented 'add_convertor_item'.
User will have ability to trigger conversion which is executed by calling
'convert' which should call 'remove_convertor_item' when is done.
It does make sense to add only one or none legacy item to create context
for convertor as it's not possible to choose which instace are converted
and which are not.
Convertor can use 'collection_shared_data' property like creators. Also
can store any information to it's object for conversion purposes.
Args:
create_context
"""
_log = None
def __init__(self, create_context):
self._create_context = create_context
@property
def log(self):
"""Logger of the plugin.
Returns:
logging.Logger: Logger with name of the plugin.
"""
if self._log is None:
self._log = Logger.get_logger(self.__class__.__name__)
return self._log
@abstractproperty
def identifier(self):
"""Converted identifier.
Returns:
str: Converted identifier unique for all converters in host.
"""
pass
@abstractmethod
def find_instances(self):
"""Look for legacy instances in the scene.
Should call 'add_convertor_item' if there is at least one instance to
convert.
"""
pass
@abstractmethod
def convert(self):
"""Conversion code."""
pass
@property
def create_context(self):
"""Quick access to create context."""
return self._create_context
@property
def collection_shared_data(self):
"""Access to shared data that can be used during 'find_instances'.
Retruns:
Dict[str, Any]: Shared data.
Raises:
UnavailableSharedData: When called out of collection phase.
"""
return self._create_context.collection_shared_data
def add_convertor_item(self, label):
"""Add item to CreateContext.
Args:
label (str): Label of item which will show in UI.
"""
self._create_context.add_convertor_item(self.identifier, label)
def remove_convertor_item(self):
"""Remove legacy item from create context when conversion finished."""
self._create_context.remove_convertor_item(self.identifier)
@six.add_metaclass(ABCMeta)
class BaseCreator:
"""Plugin that create and modify instance data before publishing process.
@ -337,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.
@ -363,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
@ -469,6 +589,10 @@ def discover_creator_plugins():
return discover(BaseCreator)
def discover_convertor_plugins():
return discover(SubsetConvertorPlugin)
def discover_legacy_creator_plugins():
from openpype.lib import Logger
@ -526,6 +650,9 @@ def register_creator_plugin(plugin):
elif issubclass(plugin, LegacyCreator):
register_plugin(LegacyCreator, plugin)
elif issubclass(plugin, SubsetConvertorPlugin):
register_plugin(SubsetConvertorPlugin, plugin)
def deregister_creator_plugin(plugin):
if issubclass(plugin, BaseCreator):
@ -534,12 +661,48 @@ def deregister_creator_plugin(plugin):
elif issubclass(plugin, LegacyCreator):
deregister_plugin(LegacyCreator, plugin)
elif issubclass(plugin, SubsetConvertorPlugin):
deregister_plugin(SubsetConvertorPlugin, plugin)
def register_creator_plugin_path(path):
register_plugin_path(BaseCreator, path)
register_plugin_path(LegacyCreator, path)
register_plugin_path(SubsetConvertorPlugin, path)
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

@ -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",
@ -64,7 +64,9 @@
"overlay-messages": {
"close-btn": "#D3D8DE",
"bg-success": "#458056",
"bg-success-hover": "#55a066"
"bg-success-hover": "#55a066",
"bg-error": "#AD2E2E",
"bg-error-hover": "#C93636"
},
"tab-widget": {
"bg": "#21252B",

View file

@ -688,22 +688,23 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
}
/* Messages overlay */
#OverlayMessageWidget {
OverlayMessageWidget {
border-radius: 0.2em;
background: {color:bg-buttons};
}
#OverlayMessageWidget:hover {
background: {color:bg-button-hover};
}
#OverlayMessageWidget {
background: {color:overlay-messages:bg-success};
}
#OverlayMessageWidget:hover {
OverlayMessageWidget:hover {
background: {color:overlay-messages:bg-success-hover};
}
#OverlayMessageWidget QWidget {
OverlayMessageWidget[type="error"] {
background: {color:overlay-messages:bg-error};
}
OverlayMessageWidget[type="error"]:hover {
background: {color:overlay-messages:bg-error-hover};
}
OverlayMessageWidget QWidget {
background: transparent;
}
@ -883,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;
@ -910,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

@ -3,6 +3,10 @@ from Qt import QtCore
# ID of context item in instance view
CONTEXT_ID = "context"
CONTEXT_LABEL = "Options"
# Not showed anywhere - used as identifier
CONTEXT_GROUP = "__ContextGroup__"
CONVERTOR_ITEM_GROUP = "Incompatible subsets"
# Allowed symbols for subset name (and variant)
# - characters, numbers, unsercore and dash
@ -16,7 +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
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,
@ -33,12 +36,18 @@ from openpype.pipeline.create import (
)
from openpype.pipeline.create.context import (
CreatorsOperationFailed,
ConvertorsOperationFailed,
)
# Define constant for plugin orders offset
PLUGIN_ORDER_OFFSET = 0.5
class CardMessageTypes:
standard = None
error = "error"
class MainThreadItem:
"""Callback with args and kwargs."""
@ -81,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:
@ -819,6 +828,7 @@ class CreatorItem:
default_variant,
default_variants,
create_allow_context_change,
create_allow_thumbnail,
pre_create_attributes_defs
):
self.identifier = identifier
@ -832,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
@ -858,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()
@ -865,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(
@ -880,6 +893,7 @@ class CreatorItem:
default_variant,
default_variants,
create_allow_context_change,
create_allow_thumbnail,
pre_create_attr_defs
)
@ -908,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,
}
@ -1109,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
@ -1242,6 +1259,22 @@ class AbstractPublisherController(object):
pass
@abstractproperty
def convertor_items(self):
pass
@abstractmethod
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.
@ -1255,7 +1288,9 @@ class AbstractPublisherController(object):
pass
@abstractmethod
def emit_card_message(self, message):
def emit_card_message(
self, message, message_type=CardMessageTypes.standard
):
"""Emit a card message which can have a lifetime.
This is for UI purposes. Method can be extended to more arguments
@ -1267,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.
@ -1507,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.
@ -1606,6 +1677,10 @@ class PublisherController(BasePublisherController):
"""Current instances in create context."""
return self._create_context.instances_by_id
@property
def convertor_items(self):
return self._create_context.convertor_items_by_id
@property
def _creators(self):
"""All creators loaded in create context."""
@ -1731,6 +1806,17 @@ class PublisherController(BasePublisherController):
}
)
try:
self._create_context.find_convertor_items()
except ConvertorsOperationFailed as exc:
self._emit_event(
"convertors.find.failed",
{
"title": "Collection of unsupported subset failed",
"failed_info": exc.failed_info
}
)
try:
self._create_context.execute_autocreators()
@ -1747,8 +1833,39 @@ class PublisherController(BasePublisherController):
self._on_create_instance_change()
def emit_card_message(self, message):
self._emit_event("show.card.message", {"message": message})
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
):
self._emit_event(
"show.card.message",
{
"message": message,
"message_type": message_type
}
)
def get_creator_attribute_definitions(self, instances):
"""Collect creator attribute definitions for multuple instances.
@ -1866,6 +1983,30 @@ class PublisherController(BasePublisherController):
variant, task_name, asset_doc, project_name, instance=instance
)
def trigger_convertor_items(self, convertor_identifiers):
self.save_changes()
success = True
try:
self._create_context.run_convertors(convertor_identifiers)
except ConvertorsOperationFailed as exc:
success = False
self._emit_event(
"convertors.convert.failed",
{
"title": "Conversion failed",
"failed_info": exc.failed_info
}
)
if success:
self.emit_card_message("Conversion finished")
else:
self.emit_card_message("Conversion failed", CardMessageTypes.error)
self.reset()
def create(
self, creator_identifier, subset_name, instance_data, options
):
@ -1912,7 +2053,6 @@ class PublisherController(BasePublisherController):
Args:
instance_ids (List[str]): List of instance ids to remove.
"""
# TODO expect instance ids instead of instances
# QUESTION Expect that instances are really removed? In that case save
# reset is not required and save changes too.
self.save_changes()

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

@ -37,7 +37,9 @@ from .widgets import (
)
from ..constants import (
CONTEXT_ID,
CONTEXT_LABEL
CONTEXT_LABEL,
CONTEXT_GROUP,
CONVERTOR_ITEM_GROUP,
)
@ -57,15 +59,12 @@ class SelectionTypes:
extend_to = SelectionType("extend_to")
class GroupWidget(QtWidgets.QWidget):
"""Widget wrapping instances under group."""
class BaseGroupWidget(QtWidgets.QWidget):
selected = QtCore.Signal(str, str, SelectionType)
active_changed = QtCore.Signal()
removed_selected = QtCore.Signal()
def __init__(self, group_name, group_icons, parent):
super(GroupWidget, self).__init__(parent)
def __init__(self, group_name, parent):
super(BaseGroupWidget, self).__init__(parent)
label_widget = QtWidgets.QLabel(group_name, self)
@ -86,10 +85,9 @@ class GroupWidget(QtWidgets.QWidget):
layout.addLayout(label_layout, 0)
self._group = group_name
self._group_icons = group_icons
self._widgets_by_id = {}
self._ordered_instance_ids = []
self._ordered_item_ids = []
self._label_widget = label_widget
self._content_layout = layout
@ -104,7 +102,12 @@ class GroupWidget(QtWidgets.QWidget):
return self._group
def get_selected_instance_ids(self):
def get_widget_by_item_id(self, item_id):
"""Get instance widget by it's id."""
return self._widgets_by_id.get(item_id)
def get_selected_item_ids(self):
"""Selected instance ids.
Returns:
@ -139,13 +142,80 @@ class GroupWidget(QtWidgets.QWidget):
return [
self._widgets_by_id[instance_id]
for instance_id in self._ordered_instance_ids
for instance_id in self._ordered_item_ids
]
def get_widget_by_instance_id(self, instance_id):
"""Get instance widget by it's id."""
def _remove_all_except(self, item_ids):
item_ids = set(item_ids)
# Remove instance widgets that are not in passed instances
for item_id in tuple(self._widgets_by_id.keys()):
if item_id in item_ids:
continue
return self._widgets_by_id.get(instance_id)
widget = self._widgets_by_id.pop(item_id)
if widget.is_selected:
self.removed_selected.emit()
widget.setVisible(False)
self._content_layout.removeWidget(widget)
widget.deleteLater()
def _update_ordered_item_ids(self):
ordered_item_ids = []
for idx in range(self._content_layout.count()):
if idx > 0:
item = self._content_layout.itemAt(idx)
widget = item.widget()
if widget is not None:
ordered_item_ids.append(widget.id)
self._ordered_item_ids = ordered_item_ids
def _on_widget_selection(self, instance_id, group_id, selection_type):
self.selected.emit(instance_id, group_id, selection_type)
class ConvertorItemsGroupWidget(BaseGroupWidget):
def update_items(self, items_by_id):
items_by_label = collections.defaultdict(list)
for item in items_by_id.values():
items_by_label[item.label].append(item)
# Remove instance widgets that are not in passed instances
self._remove_all_except(items_by_id.keys())
# Sort instances by subset name
sorted_labels = list(sorted(items_by_label.keys()))
# Add new instances to widget
widget_idx = 1
for label in sorted_labels:
for item in items_by_label[label]:
if item.id in self._widgets_by_id:
widget = self._widgets_by_id[item.id]
widget.update_item(item)
else:
widget = ConvertorItemCardWidget(item, self)
widget.selected.connect(self._on_widget_selection)
self._widgets_by_id[item.id] = widget
self._content_layout.insertWidget(widget_idx, widget)
widget_idx += 1
self._update_ordered_item_ids()
class InstanceGroupWidget(BaseGroupWidget):
"""Widget wrapping instances under group."""
active_changed = QtCore.Signal()
def __init__(self, group_icons, *args, **kwargs):
super(InstanceGroupWidget, self).__init__(*args, **kwargs)
self._group_icons = group_icons
def update_icons(self, group_icons):
self._group_icons = group_icons
def update_instance_values(self):
"""Trigger update on instance widgets."""
@ -153,14 +223,6 @@ class GroupWidget(QtWidgets.QWidget):
for widget in self._widgets_by_id.values():
widget.update_instance_values()
def confirm_remove_instance_id(self, instance_id):
"""Delete widget by instance id."""
widget = self._widgets_by_id.pop(instance_id)
widget.setVisible(False)
self._content_layout.removeWidget(widget)
widget.deleteLater()
def update_instances(self, instances):
"""Update instances for the group.
@ -178,17 +240,7 @@ class GroupWidget(QtWidgets.QWidget):
instances_by_subset_name[subset_name].append(instance)
# Remove instance widgets that are not in passed instances
for instance_id in tuple(self._widgets_by_id.keys()):
if instance_id in instances_by_id:
continue
widget = self._widgets_by_id.pop(instance_id)
if widget.is_selected:
self.removed_selected.emit()
widget.setVisible(False)
self._content_layout.removeWidget(widget)
widget.deleteLater()
self._remove_all_except(instances_by_id.keys())
# Sort instances by subset name
sorted_subset_names = list(sorted(instances_by_subset_name.keys()))
@ -211,18 +263,7 @@ class GroupWidget(QtWidgets.QWidget):
self._content_layout.insertWidget(widget_idx, widget)
widget_idx += 1
ordered_instance_ids = []
for idx in range(self._content_layout.count()):
if idx > 0:
item = self._content_layout.itemAt(idx)
widget = item.widget()
if widget is not None:
ordered_instance_ids.append(widget.id)
self._ordered_instance_ids = ordered_instance_ids
def _on_widget_selection(self, instance_id, group_id, selection_type):
self.selected.emit(instance_id, group_id, selection_type)
self._update_ordered_item_ids()
class CardWidget(BaseClickableFrame):
@ -284,7 +325,7 @@ class ContextCardWidget(CardWidget):
super(ContextCardWidget, self).__init__(parent)
self._id = CONTEXT_ID
self._group_identifier = ""
self._group_identifier = CONTEXT_GROUP
icon_widget = PublishPixmapLabel(None, self)
icon_widget.setObjectName("FamilyIconLabel")
@ -304,6 +345,40 @@ class ContextCardWidget(CardWidget):
self._label_widget = label_widget
class ConvertorItemCardWidget(CardWidget):
"""Card for global context.
Is not visually under group widget and is always at the top of card view.
"""
def __init__(self, item, parent):
super(ConvertorItemCardWidget, self).__init__(parent)
self._id = item.id
self.identifier = item.identifier
self._group_identifier = CONVERTOR_ITEM_GROUP
icon_widget = IconValuePixmapLabel("fa.magic", self)
icon_widget.setObjectName("FamilyIconLabel")
label_widget = QtWidgets.QLabel(item.label, self)
icon_layout = QtWidgets.QHBoxLayout()
icon_layout.setContentsMargins(10, 5, 5, 5)
icon_layout.addWidget(icon_widget)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 5, 10, 5)
layout.addLayout(icon_layout, 0)
layout.addWidget(label_widget, 1)
self._icon_widget = icon_widget
self._label_widget = label_widget
def update_instance_values(self):
pass
class InstanceCardWidget(CardWidget):
"""Card widget representing instance."""
@ -481,6 +556,7 @@ class InstanceCardView(AbstractInstanceView):
self._content_widget = content_widget
self._context_widget = None
self._convertor_items_group = None
self._widgets_by_group = {}
self._ordered_groups = []
@ -513,6 +589,9 @@ class InstanceCardView(AbstractInstanceView):
):
output.append(self._context_widget)
if self._convertor_items_group is not None:
output.extend(self._convertor_items_group.get_selected_widgets())
for group_widget in self._widgets_by_group.values():
for widget in group_widget.get_selected_widgets():
output.append(widget)
@ -526,23 +605,19 @@ class InstanceCardView(AbstractInstanceView):
):
output.append(CONTEXT_ID)
if self._convertor_items_group is not None:
output.extend(self._convertor_items_group.get_selected_item_ids())
for group_widget in self._widgets_by_group.values():
output.extend(group_widget.get_selected_instance_ids())
output.extend(group_widget.get_selected_item_ids())
return output
def refresh(self):
"""Refresh instances in view based on CreatedContext."""
# Create context item if is not already existing
# - this must be as first thing to do as context item should be at the
# top
if self._context_widget is None:
widget = ContextCardWidget(self._content_widget)
widget.selected.connect(self._on_widget_selection)
self._context_widget = widget
self._make_sure_context_widget_exists()
self.selection_changed.emit()
self._content_layout.insertWidget(0, widget)
self._update_convertor_items_group()
# Prepare instances by group and identifiers by group
instances_by_group = collections.defaultdict(list)
@ -573,17 +648,21 @@ class InstanceCardView(AbstractInstanceView):
# Keep track of widget indexes
# - we start with 1 because Context item as at the top
widget_idx = 1
if self._convertor_items_group is not None:
widget_idx += 1
for group_name in sorted_group_names:
group_icons = {
idenfier: self._controller.get_creator_icon(idenfier)
for idenfier in identifiers_by_group[group_name]
}
if group_name in self._widgets_by_group:
group_widget = self._widgets_by_group[group_name]
else:
group_icons = {
idenfier: self._controller.get_creator_icon(idenfier)
for idenfier in identifiers_by_group[group_name]
}
group_widget.update_icons(group_icons)
group_widget = GroupWidget(
group_name, group_icons, self._content_widget
else:
group_widget = InstanceGroupWidget(
group_icons, group_name, self._content_widget
)
group_widget.active_changed.connect(self._on_active_changed)
group_widget.selected.connect(self._on_widget_selection)
@ -595,7 +674,10 @@ class InstanceCardView(AbstractInstanceView):
instances_by_group[group_name]
)
ordered_group_names = [""]
self._update_ordered_group_nameS()
def _update_ordered_group_nameS(self):
ordered_group_names = [CONTEXT_GROUP]
for idx in range(self._content_layout.count()):
if idx > 0:
item = self._content_layout.itemAt(idx)
@ -605,6 +687,43 @@ class InstanceCardView(AbstractInstanceView):
self._ordered_groups = ordered_group_names
def _make_sure_context_widget_exists(self):
# Create context item if is not already existing
# - this must be as first thing to do as context item should be at the
# top
if self._context_widget is not None:
return
widget = ContextCardWidget(self._content_widget)
widget.selected.connect(self._on_widget_selection)
self._context_widget = widget
self.selection_changed.emit()
self._content_layout.insertWidget(0, widget)
def _update_convertor_items_group(self):
convertor_items = self._controller.convertor_items
if not convertor_items and self._convertor_items_group is None:
return
if not convertor_items:
self._convertor_items_group.setVisible(False)
self._content_layout.removeWidget(self._convertor_items_group)
self._convertor_items_group.deleteLater()
self._convertor_items_group = None
return
if self._convertor_items_group is None:
group_widget = ConvertorItemsGroupWidget(
CONVERTOR_ITEM_GROUP, self._content_widget
)
group_widget.selected.connect(self._on_widget_selection)
self._content_layout.insertWidget(1, group_widget)
self._convertor_items_group = group_widget
self._convertor_items_group.update_items(convertor_items)
def refresh_instance_states(self):
"""Trigger update of instances on group widgets."""
for widget in self._widgets_by_group.values():
@ -621,9 +740,13 @@ class InstanceCardView(AbstractInstanceView):
"""
if instance_id == CONTEXT_ID:
new_widget = self._context_widget
else:
group_widget = self._widgets_by_group[group_name]
new_widget = group_widget.get_widget_by_instance_id(instance_id)
if group_name == CONVERTOR_ITEM_GROUP:
group_widget = self._convertor_items_group
else:
group_widget = self._widgets_by_group[group_name]
new_widget = group_widget.get_widget_by_item_id(instance_id)
if selection_type is SelectionTypes.clear:
self._select_item_clear(instance_id, group_name, new_widget)
@ -668,7 +791,10 @@ class InstanceCardView(AbstractInstanceView):
if instance_id == CONTEXT_ID:
remove_group = True
else:
group_widget = self._widgets_by_group[group_name]
if group_name == CONVERTOR_ITEM_GROUP:
group_widget = self._convertor_items_group
else:
group_widget = self._widgets_by_group[group_name]
if not group_widget.get_selected_widgets():
remove_group = True
@ -749,7 +875,7 @@ class InstanceCardView(AbstractInstanceView):
# If start group is not set then use context item group name
if start_group is None:
start_group = ""
start_group = CONTEXT_GROUP
# If start instance id is not filled then use context id (similar to
# group)
@ -777,10 +903,13 @@ class InstanceCardView(AbstractInstanceView):
# Go through ordered groups (from top to bottom) and change selection
for name in self._ordered_groups:
# Prepare sorted instance widgets
if name == "":
if name == CONTEXT_GROUP:
sorted_widgets = [self._context_widget]
else:
group_widget = self._widgets_by_group[name]
if name == CONVERTOR_ITEM_GROUP:
group_widget = self._convertor_items_group
else:
group_widget = self._widgets_by_group[name]
sorted_widgets = group_widget.get_ordered_widgets()
# Change selection based on explicit selection if start group
@ -892,6 +1021,8 @@ class InstanceCardView(AbstractInstanceView):
def get_selected_items(self):
"""Get selected instance ids and context."""
convertor_identifiers = []
instances = []
selected_widgets = self._get_selected_widgets()
@ -899,37 +1030,56 @@ class InstanceCardView(AbstractInstanceView):
for widget in selected_widgets:
if widget is self._context_widget:
context_selected = True
else:
elif isinstance(widget, InstanceCardWidget):
instances.append(widget.id)
return instances, context_selected
elif isinstance(widget, ConvertorItemCardWidget):
convertor_identifiers.append(widget.identifier)
def set_selected_items(self, instance_ids, context_selected):
return instances, context_selected, convertor_identifiers
def set_selected_items(
self, instance_ids, context_selected, convertor_identifiers
):
s_instance_ids = set(instance_ids)
cur_ids, cur_context = self.get_selected_items()
s_convertor_identifiers = set(convertor_identifiers)
cur_ids, cur_context, cur_convertor_identifiers = (
self.get_selected_items()
)
if (
set(cur_ids) == s_instance_ids
and cur_context == context_selected
and set(cur_convertor_identifiers) == s_convertor_identifiers
):
return
selected_groups = []
selected_instances = []
if context_selected:
selected_groups.append("")
selected_groups.append(CONTEXT_GROUP)
selected_instances.append(CONTEXT_ID)
self._context_widget.set_selected(context_selected)
for group_name in self._ordered_groups:
if group_name == "":
if group_name == CONTEXT_GROUP:
continue
group_widget = self._widgets_by_group[group_name]
is_convertor_group = group_name == CONVERTOR_ITEM_GROUP
if is_convertor_group:
group_widget = self._convertor_items_group
else:
group_widget = self._widgets_by_group[group_name]
group_selected = False
for widget in group_widget.get_ordered_widgets():
select = False
if widget.id in s_instance_ids:
if is_convertor_group:
is_in = widget.identifier in s_convertor_identifiers
else:
is_in = widget.id in s_instance_ids
if is_in:
selected_instances.append(widget.id)
group_selected = True
select = True

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

@ -35,7 +35,10 @@ from ..constants import (
SORT_VALUE_ROLE,
IS_GROUP_ROLE,
CONTEXT_ID,
CONTEXT_LABEL
CONTEXT_LABEL,
GROUP_ROLE,
CONVERTER_IDENTIFIER_ROLE,
CONVERTOR_ITEM_GROUP,
)
@ -330,6 +333,9 @@ class InstanceTreeView(QtWidgets.QTreeView):
"""Ids of selected instances."""
instance_ids = set()
for index in self.selectionModel().selectedIndexes():
if index.data(CONVERTER_IDENTIFIER_ROLE) is not None:
continue
instance_id = index.data(INSTANCE_ID_ROLE)
if instance_id is not None:
instance_ids.add(instance_id)
@ -439,26 +445,35 @@ class InstanceListView(AbstractInstanceView):
self._group_items = {}
self._group_widgets = {}
self._widgets_by_id = {}
# Group by instance id for handling of active state
self._group_by_instance_id = {}
self._context_item = None
self._context_widget = None
self._convertor_group_item = None
self._convertor_group_widget = None
self._convertor_items_by_id = {}
self._instance_view = instance_view
self._instance_delegate = instance_delegate
self._instance_model = instance_model
self._proxy_model = proxy_model
def _on_expand(self, index):
group_name = index.data(SORT_VALUE_ROLE)
group_widget = self._group_widgets.get(group_name)
if group_widget:
group_widget.set_expanded(True)
self._update_widget_expand_state(index, True)
def _on_collapse(self, index):
group_name = index.data(SORT_VALUE_ROLE)
group_widget = self._group_widgets.get(group_name)
self._update_widget_expand_state(index, False)
def _update_widget_expand_state(self, index, expanded):
group_name = index.data(GROUP_ROLE)
if group_name == CONVERTOR_ITEM_GROUP:
group_widget = self._convertor_group_widget
else:
group_widget = self._group_widgets.get(group_name)
if group_widget:
group_widget.set_expanded(False)
group_widget.set_expanded(expanded)
def _on_toggle_request(self, toggle):
selected_instance_ids = self._instance_view.get_selected_instance_ids()
@ -517,6 +532,16 @@ class InstanceListView(AbstractInstanceView):
def refresh(self):
"""Refresh instances in the view."""
# Sort view at the end of refresh
# - is turned off until any change in view happens
sort_at_the_end = False
# Create or use already existing context item
# - context widget does not change so we don't have to update anything
if self._make_sure_context_item_exists():
sort_at_the_end = True
self._update_convertor_items_group()
# Prepare instances by their groups
instances_by_group_name = collections.defaultdict(list)
group_names = set()
@ -525,75 +550,12 @@ class InstanceListView(AbstractInstanceView):
group_names.add(group_label)
instances_by_group_name[group_label].append(instance)
# Sort view at the end of refresh
# - is turned off until any change in view happens
sort_at_the_end = False
# Access to root item of main model
root_item = self._instance_model.invisibleRootItem()
# Create or use already existing context item
# - context widget does not change so we don't have to update anything
context_item = None
if self._context_item is None:
sort_at_the_end = True
context_item = QtGui.QStandardItem()
context_item.setData(0, SORT_VALUE_ROLE)
context_item.setData(CONTEXT_ID, INSTANCE_ID_ROLE)
root_item.appendRow(context_item)
index = self._instance_model.index(
context_item.row(), context_item.column()
)
proxy_index = self._proxy_model.mapFromSource(index)
widget = ListContextWidget(self._instance_view)
self._instance_view.setIndexWidget(proxy_index, widget)
self._context_widget = widget
self._context_item = context_item
# Create new groups based on prepared `instances_by_group_name`
new_group_items = []
for group_name in group_names:
if group_name in self._group_items:
continue
group_item = QtGui.QStandardItem()
group_item.setData(group_name, SORT_VALUE_ROLE)
group_item.setData(True, IS_GROUP_ROLE)
group_item.setFlags(QtCore.Qt.ItemIsEnabled)
self._group_items[group_name] = group_item
new_group_items.append(group_item)
# Add new group items to root item if there are any
if new_group_items:
# Trigger sort at the end
if self._make_sure_groups_exists(group_names):
sort_at_the_end = True
root_item.appendRows(new_group_items)
# Create widget for each new group item and store it for future usage
for group_item in new_group_items:
index = self._instance_model.index(
group_item.row(), group_item.column()
)
proxy_index = self._proxy_model.mapFromSource(index)
group_name = group_item.data(SORT_VALUE_ROLE)
widget = InstanceListGroupWidget(group_name, self._instance_view)
widget.expand_changed.connect(self._on_group_expand_request)
widget.toggle_requested.connect(self._on_group_toggle_request)
self._group_widgets[group_name] = widget
self._instance_view.setIndexWidget(proxy_index, widget)
# Remove groups that are not available anymore
for group_name in tuple(self._group_items.keys()):
if group_name in group_names:
continue
group_item = self._group_items.pop(group_name)
root_item.removeRow(group_item.row())
widget = self._group_widgets.pop(group_name)
widget.deleteLater()
self._remove_groups_except(group_names)
# Store which groups should be expanded at the end
expand_groups = set()
@ -652,6 +614,7 @@ class InstanceListView(AbstractInstanceView):
# Create new item and store it as new
item = QtGui.QStandardItem()
item.setData(instance["subset"], SORT_VALUE_ROLE)
item.setData(instance["subset"], GROUP_ROLE)
item.setData(instance_id, INSTANCE_ID_ROLE)
new_items.append(item)
new_items_with_instance.append((item, instance))
@ -717,13 +680,152 @@ class InstanceListView(AbstractInstanceView):
self._instance_view.expand(proxy_index)
def _make_sure_context_item_exists(self):
if self._context_item is not None:
return False
root_item = self._instance_model.invisibleRootItem()
context_item = QtGui.QStandardItem()
context_item.setData(0, SORT_VALUE_ROLE)
context_item.setData(CONTEXT_ID, INSTANCE_ID_ROLE)
root_item.appendRow(context_item)
index = self._instance_model.index(
context_item.row(), context_item.column()
)
proxy_index = self._proxy_model.mapFromSource(index)
widget = ListContextWidget(self._instance_view)
self._instance_view.setIndexWidget(proxy_index, widget)
self._context_widget = widget
self._context_item = context_item
return True
def _update_convertor_items_group(self):
created_new_items = False
convertor_items_by_id = self._controller.convertor_items
group_item = self._convertor_group_item
if not convertor_items_by_id and group_item is None:
return created_new_items
root_item = self._instance_model.invisibleRootItem()
if not convertor_items_by_id:
root_item.removeRow(group_item.row())
self._convertor_group_widget.deleteLater()
self._convertor_group_widget = None
self._convertor_items_by_id = {}
return created_new_items
if group_item is None:
created_new_items = True
group_item = QtGui.QStandardItem()
group_item.setData(CONVERTOR_ITEM_GROUP, GROUP_ROLE)
group_item.setData(1, SORT_VALUE_ROLE)
group_item.setData(True, IS_GROUP_ROLE)
group_item.setFlags(QtCore.Qt.ItemIsEnabled)
root_item.appendRow(group_item)
index = self._instance_model.index(
group_item.row(), group_item.column()
)
proxy_index = self._proxy_model.mapFromSource(index)
widget = InstanceListGroupWidget(
CONVERTOR_ITEM_GROUP, self._instance_view
)
widget.toggle_checkbox.setVisible(False)
widget.expand_changed.connect(
self._on_convertor_group_expand_request
)
self._instance_view.setIndexWidget(proxy_index, widget)
self._convertor_group_item = group_item
self._convertor_group_widget = widget
for row in reversed(range(group_item.rowCount())):
child_item = group_item.child(row)
child_identifier = child_item.data(CONVERTER_IDENTIFIER_ROLE)
if child_identifier not in convertor_items_by_id:
self._convertor_items_by_id.pop(child_identifier, None)
group_item.removeRows(row, 1)
new_items = []
for identifier, convertor_item in convertor_items_by_id.items():
item = self._convertor_items_by_id.get(identifier)
if item is None:
created_new_items = True
item = QtGui.QStandardItem(convertor_item.label)
new_items.append(item)
item.setData(convertor_item.id, INSTANCE_ID_ROLE)
item.setData(convertor_item.label, SORT_VALUE_ROLE)
item.setData(CONVERTOR_ITEM_GROUP, GROUP_ROLE)
item.setData(
convertor_item.identifier, CONVERTER_IDENTIFIER_ROLE
)
self._convertor_items_by_id[identifier] = item
if new_items:
group_item.appendRows(new_items)
return created_new_items
def _make_sure_groups_exists(self, group_names):
new_group_items = []
for group_name in group_names:
if group_name in self._group_items:
continue
group_item = QtGui.QStandardItem()
group_item.setData(group_name, GROUP_ROLE)
group_item.setData(group_name, SORT_VALUE_ROLE)
group_item.setData(True, IS_GROUP_ROLE)
group_item.setFlags(QtCore.Qt.ItemIsEnabled)
self._group_items[group_name] = group_item
new_group_items.append(group_item)
# Add new group items to root item if there are any
if not new_group_items:
return False
# Access to root item of main model
root_item = self._instance_model.invisibleRootItem()
root_item.appendRows(new_group_items)
# Create widget for each new group item and store it for future usage
for group_item in new_group_items:
index = self._instance_model.index(
group_item.row(), group_item.column()
)
proxy_index = self._proxy_model.mapFromSource(index)
group_name = group_item.data(GROUP_ROLE)
widget = InstanceListGroupWidget(group_name, self._instance_view)
widget.expand_changed.connect(self._on_group_expand_request)
widget.toggle_requested.connect(self._on_group_toggle_request)
self._group_widgets[group_name] = widget
self._instance_view.setIndexWidget(proxy_index, widget)
return True
def _remove_groups_except(self, group_names):
# Remove groups that are not available anymore
root_item = self._instance_model.invisibleRootItem()
for group_name in tuple(self._group_items.keys()):
if group_name in group_names:
continue
group_item = self._group_items.pop(group_name)
root_item.removeRow(group_item.row())
widget = self._group_widgets.pop(group_name)
widget.deleteLater()
def refresh_instance_states(self):
"""Trigger update of all instances."""
for widget in self._widgets_by_id.values():
widget.update_instance_values()
def _on_active_changed(self, changed_instance_id, new_value):
selected_instance_ids, _ = self.get_selected_items()
selected_instance_ids, _, _ = self.get_selected_items()
selected_ids = set()
found = False
@ -774,6 +876,16 @@ class InstanceListView(AbstractInstanceView):
proxy_index = self._proxy_model.mapFromSource(group_index)
self._instance_view.setExpanded(proxy_index, expanded)
def _on_convertor_group_expand_request(self, _, expanded):
group_item = self._convertor_group_item
if not group_item:
return
group_index = self._instance_model.index(
group_item.row(), group_item.column()
)
proxy_index = self._proxy_model.mapFromSource(group_index)
self._instance_view.setExpanded(proxy_index, expanded)
def _on_group_toggle_request(self, group_name, state):
if state == QtCore.Qt.PartiallyChecked:
return
@ -807,10 +919,17 @@ class InstanceListView(AbstractInstanceView):
tuple<list, bool>: Selected instance ids and boolean if context
is selected.
"""
instance_ids = []
convertor_identifiers = []
context_selected = False
for index in self._instance_view.selectionModel().selectedIndexes():
convertor_identifier = index.data(CONVERTER_IDENTIFIER_ROLE)
if convertor_identifier is not None:
convertor_identifiers.append(convertor_identifier)
continue
instance_id = index.data(INSTANCE_ID_ROLE)
if not context_selected and instance_id == CONTEXT_ID:
context_selected = True
@ -818,14 +937,20 @@ class InstanceListView(AbstractInstanceView):
elif instance_id is not None:
instance_ids.append(instance_id)
return instance_ids, context_selected
return instance_ids, context_selected, convertor_identifiers
def set_selected_items(self, instance_ids, context_selected):
def set_selected_items(
self, instance_ids, context_selected, convertor_identifiers
):
s_instance_ids = set(instance_ids)
cur_ids, cur_context = self.get_selected_items()
s_convertor_identifiers = set(convertor_identifiers)
cur_ids, cur_context, cur_convertor_identifiers = (
self.get_selected_items()
)
if (
set(cur_ids) == s_instance_ids
and cur_context == context_selected
and set(cur_convertor_identifiers) == s_convertor_identifiers
):
return
@ -851,20 +976,35 @@ class InstanceListView(AbstractInstanceView):
(item.child(row), list(new_parent_items))
)
instance_id = item.data(INSTANCE_ID_ROLE)
if not instance_id:
convertor_identifier = item.data(CONVERTER_IDENTIFIER_ROLE)
select = False
expand_parent = True
if convertor_identifier is not None:
if convertor_identifier in s_convertor_identifiers:
select = True
else:
instance_id = item.data(INSTANCE_ID_ROLE)
if instance_id == CONTEXT_ID:
if context_selected:
select = True
expand_parent = False
elif instance_id in s_instance_ids:
select = True
if not select:
continue
if instance_id in s_instance_ids:
select_indexes.append(item.index())
for parent_item in parent_items:
index = parent_item.index()
proxy_index = proxy_model.mapFromSource(index)
if not view.isExpanded(proxy_index):
view.expand(proxy_index)
select_indexes.append(item.index())
if not expand_parent:
continue
elif context_selected and instance_id == CONTEXT_ID:
select_indexes.append(item.index())
for parent_item in parent_items:
index = parent_item.index()
proxy_index = proxy_model.mapFromSource(index)
if not view.isExpanded(proxy_index):
view.expand(proxy_index)
selection_model = view.selectionModel()
if not select_indexes:

View file

@ -124,6 +124,9 @@ class OverviewWidget(QtWidgets.QFrame):
subset_attributes_widget.instance_context_changed.connect(
self._on_instance_context_change
)
subset_attributes_widget.convert_requested.connect(
self._on_convert_requested
)
# --- Controller callbacks ---
controller.event_system.add_callback(
@ -201,7 +204,7 @@ class OverviewWidget(QtWidgets.QFrame):
self.create_requested.emit()
def _on_delete_clicked(self):
instance_ids, _ = self.get_selected_items()
instance_ids, _, _ = self.get_selected_items()
# Ask user if he really wants to remove instances
dialog = QtWidgets.QMessageBox(self)
@ -235,7 +238,9 @@ class OverviewWidget(QtWidgets.QFrame):
if self._refreshing_instances:
return
instance_ids, context_selected = self.get_selected_items()
instance_ids, context_selected, convertor_identifiers = (
self.get_selected_items()
)
# Disable delete button if nothing is selected
self._delete_btn.setEnabled(len(instance_ids) > 0)
@ -246,7 +251,7 @@ class OverviewWidget(QtWidgets.QFrame):
for instance_id in instance_ids
]
self._subset_attributes_widget.set_current_instances(
instances, context_selected
instances, context_selected, convertor_identifiers
)
def _on_active_changed(self):
@ -315,6 +320,10 @@ class OverviewWidget(QtWidgets.QFrame):
self.instance_context_changed.emit()
def _on_convert_requested(self):
_, _, convertor_identifiers = self.get_selected_items()
self._controller.trigger_convertor_items(convertor_identifiers)
def get_selected_items(self):
view = self._subset_views_layout.currentWidget()
return view.get_selected_items()
@ -332,8 +341,12 @@ class OverviewWidget(QtWidgets.QFrame):
else:
new_view.refresh_instance_states()
instance_ids, context_selected = old_view.get_selected_items()
new_view.set_selected_items(instance_ids, context_selected)
instance_ids, context_selected, convertor_identifiers = (
old_view.get_selected_items()
)
new_view.set_selected_items(
instance_ids, context_selected, convertor_identifiers
)
self._subset_views_layout.setCurrentIndex(new_idx)

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