mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'main' into workflow/main-milestones
This commit is contained in:
commit
a70a9a3ea6
117 changed files with 3408 additions and 1075 deletions
2
.github/workflows/milestone_assign.yml
vendored
2
.github/workflows/milestone_assign.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
39
CHANGELOG.md
39
CHANGELOG.md
|
|
@ -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**
|
||||
|
||||
|
|
|
|||
123
HISTORY.md
123
HISTORY.md
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayAction
|
||||
from openpype.modules import (
|
||||
OpenPypeModule,
|
||||
ITrayAction,
|
||||
)
|
||||
|
||||
|
||||
class LauncherAction(OpenPypeModule, ITrayAction):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayModule
|
||||
from openpype.modules import OpenPypeModule, ITrayModule
|
||||
|
||||
|
||||
class LogViewModule(OpenPypeModule, ITrayModule):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayAction
|
||||
from openpype.modules import OpenPypeModule, ITrayAction
|
||||
|
||||
|
||||
class ProjectManagerAction(OpenPypeModule, ITrayAction):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayAction
|
||||
from openpype.modules import OpenPypeModule, ITrayAction
|
||||
|
||||
|
||||
class PythonInterpreterAction(OpenPypeModule, ITrayAction):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayAction
|
||||
from openpype.modules import OpenPypeModule, ITrayAction
|
||||
|
||||
|
||||
class SettingsAction(OpenPypeModule, ITrayAction):
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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__))
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ from .context_tools import (
|
|||
register_host,
|
||||
registered_host,
|
||||
deregister_host,
|
||||
get_process_id,
|
||||
)
|
||||
install = install_host
|
||||
uninstall = uninstall_host
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from openpype.api import ApplicationManager
|
||||
from openpype.lib import ApplicationManager
|
||||
from openpype.pipeline import load
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,14 @@ from .widgets import (
|
|||
AttributeDefinitionsWidget,
|
||||
)
|
||||
|
||||
from .dialog import (
|
||||
AttributeDefinitionsDialog,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"create_widget_for_attr_def",
|
||||
"AttributeDefinitionsWidget",
|
||||
|
||||
"AttributeDefinitionsDialog",
|
||||
)
|
||||
33
openpype/tools/attribute_defs/dialog.py
Normal file
33
openpype/tools/attribute_defs/dialog.py
Normal 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()
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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__ = (
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
BIN
openpype/tools/publisher/widgets/images/clear_thumbnail.png
Normal file
BIN
openpype/tools/publisher/widgets/images/clear_thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue