mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 00:44:52 +01:00
Merge branch 'develop' of github.com:pypeclub/OpenPype into feature/OP-2766_PS-to-new-publisher
This commit is contained in:
commit
9c49efa87a
36 changed files with 614 additions and 467 deletions
67
CHANGELOG.md
67
CHANGELOG.md
|
|
@ -1,34 +1,50 @@
|
|||
# Changelog
|
||||
|
||||
## [3.9.2-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.9.2](https://github.com/pypeclub/OpenPype/tree/3.9.2) (2022-04-04)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.1...HEAD)
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.1...3.9.2)
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Documentation: Added mention of adding My Drive as a root [\#2999](https://github.com/pypeclub/OpenPype/pull/2999)
|
||||
- Docs: Added MongoDB requirements [\#2951](https://github.com/pypeclub/OpenPype/pull/2951)
|
||||
- Documentation: New publisher develop docs [\#2896](https://github.com/pypeclub/OpenPype/pull/2896)
|
||||
|
||||
**🆕 New features**
|
||||
|
||||
- Multiverse: First PR [\#2908](https://github.com/pypeclub/OpenPype/pull/2908)
|
||||
- nuke: bypass baking [\#2992](https://github.com/pypeclub/OpenPype/pull/2992)
|
||||
- Multiverse: Initial Support [\#2908](https://github.com/pypeclub/OpenPype/pull/2908)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Photoshop: create image without instance [\#3001](https://github.com/pypeclub/OpenPype/pull/3001)
|
||||
- TVPaint: Render scene family [\#3000](https://github.com/pypeclub/OpenPype/pull/3000)
|
||||
- Nuke: ReviewDataMov Read RAW attribute [\#2985](https://github.com/pypeclub/OpenPype/pull/2985)
|
||||
- General: `METADATA\_KEYS` constant as `frozenset` for optimal immutable lookup [\#2980](https://github.com/pypeclub/OpenPype/pull/2980)
|
||||
- General: Tools with host filters [\#2975](https://github.com/pypeclub/OpenPype/pull/2975)
|
||||
- Hero versions: Use custom templates [\#2967](https://github.com/pypeclub/OpenPype/pull/2967)
|
||||
- Slack: Added configurable maximum file size of review upload to Slack [\#2945](https://github.com/pypeclub/OpenPype/pull/2945)
|
||||
- NewPublisher: Prepared implementation of optional pyblish plugin [\#2943](https://github.com/pypeclub/OpenPype/pull/2943)
|
||||
- TVPaint: Extractor to convert PNG into EXR [\#2942](https://github.com/pypeclub/OpenPype/pull/2942)
|
||||
- Workfiles: Open published workfiles [\#2925](https://github.com/pypeclub/OpenPype/pull/2925)
|
||||
- General: Default modules loaded dynamically [\#2923](https://github.com/pypeclub/OpenPype/pull/2923)
|
||||
- CI: change the version bump logic [\#2919](https://github.com/pypeclub/OpenPype/pull/2919)
|
||||
- Deadline: Add headless argument [\#2916](https://github.com/pypeclub/OpenPype/pull/2916)
|
||||
- Nuke: Add no-audio Tag [\#2911](https://github.com/pypeclub/OpenPype/pull/2911)
|
||||
- Ftrack: Fill workfile in custom attribute [\#2906](https://github.com/pypeclub/OpenPype/pull/2906)
|
||||
- Nuke: improving readability [\#2903](https://github.com/pypeclub/OpenPype/pull/2903)
|
||||
- Settings UI: Add simple tooltips for settings entities [\#2901](https://github.com/pypeclub/OpenPype/pull/2901)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Hosts: Remove path existence checks in 'add\_implementation\_envs' [\#3004](https://github.com/pypeclub/OpenPype/pull/3004)
|
||||
- Fix - remove doubled dot in workfile created from template [\#2998](https://github.com/pypeclub/OpenPype/pull/2998)
|
||||
- PS: fix renaming subset incorrectly in PS [\#2991](https://github.com/pypeclub/OpenPype/pull/2991)
|
||||
- Fix: Disable setuptools auto discovery [\#2990](https://github.com/pypeclub/OpenPype/pull/2990)
|
||||
- AEL: fix opening existing workfile if no scene opened [\#2989](https://github.com/pypeclub/OpenPype/pull/2989)
|
||||
- Maya: Don't do hardlinks on windows for look publishing [\#2986](https://github.com/pypeclub/OpenPype/pull/2986)
|
||||
- Settings UI: Fix version completer on linux [\#2981](https://github.com/pypeclub/OpenPype/pull/2981)
|
||||
- Photoshop: Fix creation of subset names in PS review and workfile [\#2969](https://github.com/pypeclub/OpenPype/pull/2969)
|
||||
- Slack: Added default for review\_upload\_limit for Slack [\#2965](https://github.com/pypeclub/OpenPype/pull/2965)
|
||||
- General: OIIO conversion for ffmeg can handle sequences [\#2958](https://github.com/pypeclub/OpenPype/pull/2958)
|
||||
- Settings: Conditional dictionary avoid invalid logs [\#2956](https://github.com/pypeclub/OpenPype/pull/2956)
|
||||
- General: Smaller fixes and typos [\#2950](https://github.com/pypeclub/OpenPype/pull/2950)
|
||||
- LogViewer: Don't refresh on initialization [\#2949](https://github.com/pypeclub/OpenPype/pull/2949)
|
||||
- nuke: python3 compatibility issue with `iteritems` [\#2948](https://github.com/pypeclub/OpenPype/pull/2948)
|
||||
- General: anatomy data with correct task short key [\#2947](https://github.com/pypeclub/OpenPype/pull/2947)
|
||||
|
|
@ -39,20 +55,21 @@
|
|||
- Settings UI: Collapsed of collapsible wrapper works as expected [\#2934](https://github.com/pypeclub/OpenPype/pull/2934)
|
||||
- Maya: Do not pass `set` to maya commands \(fixes support for older maya versions\) [\#2932](https://github.com/pypeclub/OpenPype/pull/2932)
|
||||
- General: Don't print log record on OSError [\#2926](https://github.com/pypeclub/OpenPype/pull/2926)
|
||||
- Hiero: Fix import of 'register\_event\_callback' [\#2924](https://github.com/pypeclub/OpenPype/pull/2924)
|
||||
- Ftrack: Missing Ftrack id after editorial publish [\#2905](https://github.com/pypeclub/OpenPype/pull/2905)
|
||||
- AfterEffects: Fix rendering for single frame in DL [\#2875](https://github.com/pypeclub/OpenPype/pull/2875)
|
||||
- Flame: centos related debugging [\#2922](https://github.com/pypeclub/OpenPype/pull/2922)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- General: Move plugins register and discover [\#2935](https://github.com/pypeclub/OpenPype/pull/2935)
|
||||
- General: Move Attribute Definitions from pipeline [\#2931](https://github.com/pypeclub/OpenPype/pull/2931)
|
||||
- General: Removed silo references and terminal splash [\#2927](https://github.com/pypeclub/OpenPype/pull/2927)
|
||||
- General: Move pipeline constants to OpenPype [\#2918](https://github.com/pypeclub/OpenPype/pull/2918)
|
||||
- General: Move formatting and workfile functions [\#2914](https://github.com/pypeclub/OpenPype/pull/2914)
|
||||
- General: Move remaining plugins from avalon [\#2912](https://github.com/pypeclub/OpenPype/pull/2912)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Bump paramiko from 2.9.2 to 2.10.1 [\#2973](https://github.com/pypeclub/OpenPype/pull/2973)
|
||||
- Bump minimist from 1.2.5 to 1.2.6 in /website [\#2954](https://github.com/pypeclub/OpenPype/pull/2954)
|
||||
- Bump node-forge from 1.2.1 to 1.3.0 in /website [\#2953](https://github.com/pypeclub/OpenPype/pull/2953)
|
||||
- Maya - added transparency into review creator [\#2952](https://github.com/pypeclub/OpenPype/pull/2952)
|
||||
|
||||
## [3.9.1](https://github.com/pypeclub/OpenPype/tree/3.9.1) (2022-03-18)
|
||||
|
|
@ -77,7 +94,6 @@
|
|||
- General: Remove forgotten use of avalon Creator [\#2885](https://github.com/pypeclub/OpenPype/pull/2885)
|
||||
- General: Avoid circular import [\#2884](https://github.com/pypeclub/OpenPype/pull/2884)
|
||||
- Fixes for attaching loaded containers \(\#2837\) [\#2874](https://github.com/pypeclub/OpenPype/pull/2874)
|
||||
- Maya: Deformer node ids validation plugin [\#2826](https://github.com/pypeclub/OpenPype/pull/2826)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
|
|
@ -88,10 +104,6 @@
|
|||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.0-nightly.9...3.9.0)
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
- AssetCreator: Remove the tool [\#2845](https://github.com/pypeclub/OpenPype/pull/2845)
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Documentation: Change Photoshop & AfterEffects plugin path [\#2878](https://github.com/pypeclub/OpenPype/pull/2878)
|
||||
|
|
@ -102,10 +114,6 @@
|
|||
- NewPublisher: Descriptions and Icons in creator dialog [\#2867](https://github.com/pypeclub/OpenPype/pull/2867)
|
||||
- NewPublisher: Changing task on publishing instance [\#2863](https://github.com/pypeclub/OpenPype/pull/2863)
|
||||
- TrayPublisher: Choose project widget is more clear [\#2859](https://github.com/pypeclub/OpenPype/pull/2859)
|
||||
- New: Validation exceptions [\#2841](https://github.com/pypeclub/OpenPype/pull/2841)
|
||||
- Maya: add loaded containers to published instance [\#2837](https://github.com/pypeclub/OpenPype/pull/2837)
|
||||
- Ftrack: Can sync fps as string [\#2836](https://github.com/pypeclub/OpenPype/pull/2836)
|
||||
- General: Custom function for find executable [\#2822](https://github.com/pypeclub/OpenPype/pull/2822)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
|
|
@ -118,30 +126,11 @@
|
|||
- New Publisher: Error dialog got right styles [\#2857](https://github.com/pypeclub/OpenPype/pull/2857)
|
||||
- General: Fix getattr clalback on dynamic modules [\#2855](https://github.com/pypeclub/OpenPype/pull/2855)
|
||||
- Nuke: slate resolution to input video resolution [\#2853](https://github.com/pypeclub/OpenPype/pull/2853)
|
||||
- WebPublisher: Fix username stored in DB [\#2852](https://github.com/pypeclub/OpenPype/pull/2852)
|
||||
- WebPublisher: Fix wrong number of frames for video file [\#2851](https://github.com/pypeclub/OpenPype/pull/2851)
|
||||
- Nuke: Fix family test in validate\_write\_legacy to work with stillImage [\#2847](https://github.com/pypeclub/OpenPype/pull/2847)
|
||||
- Nuke: fix multiple baking profile farm publishing [\#2842](https://github.com/pypeclub/OpenPype/pull/2842)
|
||||
- Blender: Fixed parameters for FBX export of the camera [\#2840](https://github.com/pypeclub/OpenPype/pull/2840)
|
||||
- Maya: Stop creation of reviews for Cryptomattes [\#2832](https://github.com/pypeclub/OpenPype/pull/2832)
|
||||
- Deadline: Remove recreated event [\#2828](https://github.com/pypeclub/OpenPype/pull/2828)
|
||||
- Deadline: Added missing events folder [\#2827](https://github.com/pypeclub/OpenPype/pull/2827)
|
||||
- Settings: Missing document with OP versions may break start of OpenPype [\#2825](https://github.com/pypeclub/OpenPype/pull/2825)
|
||||
- Deadline: more detailed temp file name for environment json [\#2824](https://github.com/pypeclub/OpenPype/pull/2824)
|
||||
- General: Host name was formed from obsolete code [\#2821](https://github.com/pypeclub/OpenPype/pull/2821)
|
||||
- Settings UI: Fix "Apply from" action [\#2820](https://github.com/pypeclub/OpenPype/pull/2820)
|
||||
- Ftrack: Job killer with missing user [\#2819](https://github.com/pypeclub/OpenPype/pull/2819)
|
||||
- Nuke: Use AVALON\_APP to get value for "app" key [\#2818](https://github.com/pypeclub/OpenPype/pull/2818)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- Refactor: move webserver tool to openpype [\#2876](https://github.com/pypeclub/OpenPype/pull/2876)
|
||||
- General: Move create logic from avalon to OpenPype [\#2854](https://github.com/pypeclub/OpenPype/pull/2854)
|
||||
- General: Add vendors from avalon [\#2848](https://github.com/pypeclub/OpenPype/pull/2848)
|
||||
- General: Basic event system [\#2846](https://github.com/pypeclub/OpenPype/pull/2846)
|
||||
- General: Move change context functions [\#2839](https://github.com/pypeclub/OpenPype/pull/2839)
|
||||
- Tools: Don't use avalon tools code [\#2829](https://github.com/pypeclub/OpenPype/pull/2829)
|
||||
- Move Unreal Implementation to OpenPype [\#2823](https://github.com/pypeclub/OpenPype/pull/2823)
|
||||
|
||||
## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,12 +29,12 @@ def add_implementation_envs(env, _app):
|
|||
env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or ""
|
||||
)
|
||||
for path in openpype_blender_user_scripts.split(os.pathsep):
|
||||
if path and os.path.exists(path):
|
||||
if path:
|
||||
previous_user_scripts.add(os.path.normpath(path))
|
||||
|
||||
blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or ""
|
||||
for path in blender_user_scripts.split(os.pathsep):
|
||||
if path and os.path.exists(path):
|
||||
if path:
|
||||
previous_user_scripts.add(os.path.normpath(path))
|
||||
|
||||
# Remove implementation path from user script paths as is set to
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ def add_implementation_envs(env, _app):
|
|||
]
|
||||
old_hiero_path = env.get("HIERO_PLUGIN_PATH") or ""
|
||||
for path in old_hiero_path.split(os.pathsep):
|
||||
if not path or not os.path.exists(path):
|
||||
if not path:
|
||||
continue
|
||||
|
||||
norm_path = os.path.normpath(path)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ def add_implementation_envs(env, _app):
|
|||
old_houdini_menu_path = env.get("HOUDINI_MENU_PATH") or ""
|
||||
|
||||
for path in old_houdini_path.split(os.pathsep):
|
||||
if not path or not os.path.exists(path):
|
||||
if not path:
|
||||
continue
|
||||
|
||||
norm_path = os.path.normpath(path)
|
||||
|
|
@ -23,7 +23,7 @@ def add_implementation_envs(env, _app):
|
|||
new_houdini_path.append(norm_path)
|
||||
|
||||
for path in old_houdini_menu_path.split(os.pathsep):
|
||||
if not path or not os.path.exists(path):
|
||||
if not path:
|
||||
continue
|
||||
|
||||
norm_path = os.path.normpath(path)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ def add_implementation_envs(env, _app):
|
|||
]
|
||||
old_python_path = env.get("PYTHONPATH") or ""
|
||||
for path in old_python_path.split(os.pathsep):
|
||||
if not path or not os.path.exists(path):
|
||||
if not path:
|
||||
continue
|
||||
|
||||
norm_path = os.path.normpath(path)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ def add_implementation_envs(env, _app):
|
|||
]
|
||||
old_nuke_path = env.get("NUKE_PATH") or ""
|
||||
for path in old_nuke_path.split(os.pathsep):
|
||||
if not path or not os.path.exists(path):
|
||||
if not path:
|
||||
continue
|
||||
|
||||
norm_path = os.path.normpath(path)
|
||||
|
|
|
|||
|
|
@ -1048,17 +1048,28 @@ def add_review_knob(node):
|
|||
def add_deadline_tab(node):
|
||||
node.addKnob(nuke.Tab_Knob("Deadline"))
|
||||
|
||||
knob = nuke.Int_Knob("deadlineChunkSize", "Chunk Size")
|
||||
knob.setValue(0)
|
||||
node.addKnob(knob)
|
||||
|
||||
knob = nuke.Int_Knob("deadlinePriority", "Priority")
|
||||
knob.setValue(50)
|
||||
node.addKnob(knob)
|
||||
|
||||
knob = nuke.Int_Knob("deadlineChunkSize", "Chunk Size")
|
||||
knob.setValue(0)
|
||||
node.addKnob(knob)
|
||||
|
||||
knob = nuke.Int_Knob("deadlineConcurrentTasks", "Concurrent tasks")
|
||||
# zero as default will get value from Settings during collection
|
||||
# instead of being an explicit user override, see precollect_write.py
|
||||
knob.setValue(0)
|
||||
node.addKnob(knob)
|
||||
|
||||
|
||||
def get_deadline_knob_names():
|
||||
return ["Deadline", "deadlineChunkSize", "deadlinePriority"]
|
||||
return [
|
||||
"Deadline",
|
||||
"deadlineChunkSize",
|
||||
"deadlinePriority",
|
||||
"deadlineConcurrentTasks"
|
||||
]
|
||||
|
||||
|
||||
def create_backdrop(label="", color=None, layer=0,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
from collections import OrderedDict
|
||||
import nuke
|
||||
import six
|
||||
|
||||
from avalon import io
|
||||
|
||||
|
|
@ -333,7 +334,7 @@ class LoadEffects(load.LoaderPlugin):
|
|||
for key, value in input.items()}
|
||||
elif isinstance(input, list):
|
||||
return [self.byteify(element) for element in input]
|
||||
elif isinstance(input, str):
|
||||
elif isinstance(input, six.text_type):
|
||||
return str(input)
|
||||
else:
|
||||
return input
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
from collections import OrderedDict
|
||||
|
||||
import six
|
||||
import nuke
|
||||
|
||||
from avalon import io
|
||||
|
|
@ -353,7 +353,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin):
|
|||
for key, value in input.items()}
|
||||
elif isinstance(input, list):
|
||||
return [self.byteify(element) for element in input]
|
||||
elif isinstance(input, str):
|
||||
elif isinstance(input, six.text_type):
|
||||
return str(input)
|
||||
else:
|
||||
return input
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import nuke
|
||||
|
||||
import six
|
||||
from avalon import io
|
||||
|
||||
from openpype.pipeline import (
|
||||
|
|
@ -243,8 +243,8 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
|
|||
for key, value in input.items()}
|
||||
elif isinstance(input, list):
|
||||
return [self.byteify(element) for element in input]
|
||||
elif isinstance(input, unicode):
|
||||
return input.encode('utf-8')
|
||||
elif isinstance(input, six.text_type):
|
||||
return str(input)
|
||||
else:
|
||||
return input
|
||||
|
||||
|
|
|
|||
47
openpype/hosts/nuke/plugins/publish/extract_review_data.py
Normal file
47
openpype/hosts/nuke/plugins/publish/extract_review_data.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
import openpype
|
||||
from pprint import pformat
|
||||
|
||||
|
||||
class ExtractReviewData(openpype.api.Extractor):
|
||||
"""Extracts review tag into available representation
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder + 0.01
|
||||
# order = pyblish.api.CollectorOrder + 0.499
|
||||
label = "Extract Review Data"
|
||||
|
||||
families = ["review"]
|
||||
hosts = ["nuke"]
|
||||
|
||||
def process(self, instance):
|
||||
fpath = instance.data["path"]
|
||||
ext = os.path.splitext(fpath)[-1][1:]
|
||||
|
||||
representations = instance.data.get("representations", [])
|
||||
|
||||
# review can be removed since `ProcessSubmittedJobOnFarm` will create
|
||||
# reviable representation if needed
|
||||
if (
|
||||
"render.farm" in instance.data["families"]
|
||||
and "review" in instance.data["families"]
|
||||
):
|
||||
instance.data["families"].remove("review")
|
||||
|
||||
# iterate representations and add `review` tag
|
||||
for repre in representations:
|
||||
if ext != repre["ext"]:
|
||||
continue
|
||||
|
||||
if not repre.get("tags"):
|
||||
repre["tags"] = []
|
||||
|
||||
if "review" not in repre["tags"]:
|
||||
repre["tags"].append("review")
|
||||
|
||||
self.log.debug("Matching representation: {}".format(
|
||||
pformat(repre)
|
||||
))
|
||||
|
||||
instance.data["representations"] = representations
|
||||
|
|
@ -123,6 +123,7 @@ class ExtractReviewDataMov(openpype.api.Extractor):
|
|||
if generated_repres:
|
||||
# assign to representations
|
||||
instance.data["representations"] += generated_repres
|
||||
instance.data["hasReviewableRepresentations"] = True
|
||||
else:
|
||||
instance.data["families"].remove("review")
|
||||
self.log.info((
|
||||
|
|
|
|||
|
|
@ -128,13 +128,17 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
|
|||
}
|
||||
|
||||
group_node = [x for x in instance if x.Class() == "Group"][0]
|
||||
deadlineChunkSize = 1
|
||||
dl_chunk_size = 1
|
||||
if "deadlineChunkSize" in group_node.knobs():
|
||||
deadlineChunkSize = group_node["deadlineChunkSize"].value()
|
||||
dl_chunk_size = group_node["deadlineChunkSize"].value()
|
||||
|
||||
deadlinePriority = 50
|
||||
dl_priority = 50
|
||||
if "deadlinePriority" in group_node.knobs():
|
||||
deadlinePriority = group_node["deadlinePriority"].value()
|
||||
dl_priority = group_node["deadlinePriority"].value()
|
||||
|
||||
dl_concurrent_tasks = 0
|
||||
if "deadlineConcurrentTasks" in group_node.knobs():
|
||||
dl_concurrent_tasks = group_node["deadlineConcurrentTasks"].value()
|
||||
|
||||
instance.data.update({
|
||||
"versionData": version_data,
|
||||
|
|
@ -144,8 +148,9 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
|
|||
"label": label,
|
||||
"outputType": output_type,
|
||||
"colorspace": colorspace,
|
||||
"deadlineChunkSize": deadlineChunkSize,
|
||||
"deadlinePriority": deadlinePriority
|
||||
"deadlineChunkSize": dl_chunk_size,
|
||||
"deadlinePriority": dl_priority,
|
||||
"deadlineConcurrentTasks": dl_concurrent_tasks
|
||||
})
|
||||
|
||||
if self.is_prerender(_families_test):
|
||||
|
|
|
|||
|
|
@ -20,21 +20,30 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
json.dumps(workfile_instances, indent=4)
|
||||
))
|
||||
|
||||
filtered_instance_data = []
|
||||
# Backwards compatibility for workfiles that already have review
|
||||
# instance in metadata.
|
||||
review_instance_exist = False
|
||||
for instance_data in workfile_instances:
|
||||
if instance_data["family"] == "review":
|
||||
family = instance_data["family"]
|
||||
if family == "review":
|
||||
review_instance_exist = True
|
||||
break
|
||||
|
||||
elif family not in ("renderPass", "renderLayer"):
|
||||
self.log.info("Unknown family \"{}\". Skipping {}".format(
|
||||
family, json.dumps(instance_data, indent=4)
|
||||
))
|
||||
continue
|
||||
|
||||
filtered_instance_data.append(instance_data)
|
||||
|
||||
# Fake review instance if review was not found in metadata families
|
||||
if not review_instance_exist:
|
||||
workfile_instances.append(
|
||||
filtered_instance_data.append(
|
||||
self._create_review_instance_data(context)
|
||||
)
|
||||
|
||||
for instance_data in workfile_instances:
|
||||
for instance_data in filtered_instance_data:
|
||||
instance_data["fps"] = context.data["sceneFps"]
|
||||
|
||||
# Store workfile instance data to instance data
|
||||
|
|
@ -42,8 +51,11 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
# Global instance data modifications
|
||||
# Fill families
|
||||
family = instance_data["family"]
|
||||
families = [family]
|
||||
if family != "review":
|
||||
families.append("review")
|
||||
# Add `review` family for thumbnail integration
|
||||
instance_data["families"] = [family, "review"]
|
||||
instance_data["families"] = families
|
||||
|
||||
# Instance name
|
||||
subset_name = instance_data["subset"]
|
||||
|
|
@ -78,7 +90,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
# Project name from workfile context
|
||||
project_name = context.data["workfile_context"]["project"]
|
||||
# Host name from environment variable
|
||||
host_name = os.environ["AVALON_APP"]
|
||||
host_name = context.data["hostName"]
|
||||
# Use empty variant value
|
||||
variant = ""
|
||||
task_name = io.Session["AVALON_TASK"]
|
||||
|
|
@ -106,12 +118,6 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
instance = self.create_render_pass_instance(
|
||||
context, instance_data
|
||||
)
|
||||
else:
|
||||
raise AssertionError(
|
||||
"Instance with unknown family \"{}\": {}".format(
|
||||
family, instance_data
|
||||
)
|
||||
)
|
||||
|
||||
if instance is None:
|
||||
continue
|
||||
|
|
|
|||
110
openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py
Normal file
110
openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import json
|
||||
import copy
|
||||
import pyblish.api
|
||||
from avalon import io
|
||||
|
||||
from openpype.lib import get_subset_name_with_asset_doc
|
||||
|
||||
|
||||
class CollectRenderScene(pyblish.api.ContextPlugin):
|
||||
"""Collect instance which renders whole scene in PNG.
|
||||
|
||||
Creates instance with family 'renderScene' which will have all layers
|
||||
to render which will be composite into one result. The instance is not
|
||||
collected from scene.
|
||||
|
||||
Scene will be rendered with all visible layers similar way like review is.
|
||||
|
||||
Instance is disabled if there are any created instances of 'renderLayer'
|
||||
or 'renderPass'. That is because it is expected that this instance is
|
||||
used as lazy publish of TVPaint file.
|
||||
|
||||
Subset name is created similar way like 'renderLayer' family. It can use
|
||||
`renderPass` and `renderLayer` keys which can be set using settings and
|
||||
`variant` is filled using `renderPass` value.
|
||||
"""
|
||||
label = "Collect Render Scene"
|
||||
order = pyblish.api.CollectorOrder - 0.39
|
||||
hosts = ["tvpaint"]
|
||||
|
||||
# Value of 'render_pass' in subset name template
|
||||
render_pass = "beauty"
|
||||
|
||||
# Settings attributes
|
||||
enabled = False
|
||||
# Value of 'render_layer' and 'variant' in subset name template
|
||||
render_layer = "Main"
|
||||
|
||||
def process(self, context):
|
||||
# Check if there are created instances of renderPass and renderLayer
|
||||
# - that will define if renderScene instance is enabled after
|
||||
# collection
|
||||
any_created_instance = False
|
||||
for instance in context:
|
||||
family = instance.data["family"]
|
||||
if family in ("renderPass", "renderLayer"):
|
||||
any_created_instance = True
|
||||
break
|
||||
|
||||
# Global instance data modifications
|
||||
# Fill families
|
||||
family = "renderScene"
|
||||
# Add `review` family for thumbnail integration
|
||||
families = [family, "review"]
|
||||
|
||||
# Collect asset doc to get asset id
|
||||
# - not sure if it's good idea to require asset id in
|
||||
# get_subset_name?
|
||||
workfile_context = context.data["workfile_context"]
|
||||
asset_name = workfile_context["asset"]
|
||||
asset_doc = io.find_one({
|
||||
"type": "asset",
|
||||
"name": asset_name
|
||||
})
|
||||
|
||||
# Project name from workfile context
|
||||
project_name = context.data["workfile_context"]["project"]
|
||||
# Host name from environment variable
|
||||
host_name = context.data["hostName"]
|
||||
# Variant is using render pass name
|
||||
variant = self.render_layer
|
||||
dynamic_data = {
|
||||
"render_layer": self.render_layer,
|
||||
"render_pass": self.render_pass
|
||||
}
|
||||
|
||||
task_name = workfile_context["task"]
|
||||
subset_name = get_subset_name_with_asset_doc(
|
||||
"render",
|
||||
variant,
|
||||
task_name,
|
||||
asset_doc,
|
||||
project_name,
|
||||
host_name,
|
||||
dynamic_data=dynamic_data
|
||||
)
|
||||
|
||||
instance_data = {
|
||||
"family": family,
|
||||
"families": families,
|
||||
"fps": context.data["sceneFps"],
|
||||
"subset": subset_name,
|
||||
"name": subset_name,
|
||||
"label": "{} [{}-{}]".format(
|
||||
subset_name,
|
||||
context.data["sceneMarkIn"] + 1,
|
||||
context.data["sceneMarkOut"] + 1
|
||||
),
|
||||
"active": not any_created_instance,
|
||||
"publish": not any_created_instance,
|
||||
"representations": [],
|
||||
"layers": copy.deepcopy(context.data["layersData"]),
|
||||
"asset": asset_name,
|
||||
"task": task_name
|
||||
}
|
||||
|
||||
instance = context.create_instance(**instance_data)
|
||||
|
||||
self.log.debug("Created instance: {}\n{}".format(
|
||||
instance, json.dumps(instance.data, indent=4)
|
||||
))
|
||||
|
|
@ -18,7 +18,7 @@ from openpype.hosts.tvpaint.lib import (
|
|||
class ExtractSequence(pyblish.api.Extractor):
|
||||
label = "Extract Sequence"
|
||||
hosts = ["tvpaint"]
|
||||
families = ["review", "renderPass", "renderLayer"]
|
||||
families = ["review", "renderPass", "renderLayer", "renderScene"]
|
||||
|
||||
# Modifiable with settings
|
||||
review_bg = [255, 255, 255, 255]
|
||||
|
|
@ -159,7 +159,7 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
|
||||
# Fill tags and new families
|
||||
tags = []
|
||||
if family_lowered in ("review", "renderlayer"):
|
||||
if family_lowered in ("review", "renderlayer", "renderscene"):
|
||||
tags.append("review")
|
||||
|
||||
# Sequence of one frame
|
||||
|
|
@ -185,7 +185,7 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
|
||||
instance.data["representations"].append(new_repre)
|
||||
|
||||
if family_lowered in ("renderpass", "renderlayer"):
|
||||
if family_lowered in ("renderpass", "renderlayer", "renderscene"):
|
||||
# Change family to render
|
||||
instance.data["family"] = "render"
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class ValidateLayersVisiblity(pyblish.api.InstancePlugin):
|
|||
|
||||
label = "Validate Layers Visibility"
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["review", "renderPass", "renderLayer"]
|
||||
families = ["review", "renderPass", "renderLayer", "renderScene"]
|
||||
|
||||
def process(self, instance):
|
||||
layer_names = set()
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
# presets
|
||||
priority = 50
|
||||
chunk_size = 1
|
||||
concurrent_tasks = 1
|
||||
primary_pool = ""
|
||||
secondary_pool = ""
|
||||
group = ""
|
||||
|
|
@ -149,11 +150,16 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
pass
|
||||
|
||||
# define chunk and priority
|
||||
chunk_size = instance.data.get("deadlineChunkSize")
|
||||
chunk_size = instance.data["deadlineChunkSize"]
|
||||
if chunk_size == 0 and self.chunk_size:
|
||||
chunk_size = self.chunk_size
|
||||
|
||||
priority = instance.data.get("deadlinePriority")
|
||||
# define chunk and priority
|
||||
concurrent_tasks = instance.data["deadlineConcurrentTasks"]
|
||||
if concurrent_tasks == 0 and self.concurrent_tasks:
|
||||
concurrent_tasks = self.concurrent_tasks
|
||||
|
||||
priority = instance.data["deadlinePriority"]
|
||||
if not priority:
|
||||
priority = self.priority
|
||||
|
||||
|
|
@ -177,6 +183,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
|
||||
"Priority": priority,
|
||||
"ChunkSize": chunk_size,
|
||||
"ConcurrentTasks": concurrent_tasks,
|
||||
|
||||
"Department": self.department,
|
||||
|
||||
"Pool": self.primary_pool,
|
||||
|
|
|
|||
|
|
@ -509,8 +509,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
most cases, but if not - we create representation from each of them.
|
||||
|
||||
Arguments:
|
||||
instance (pyblish.plugin.Instance): instance for which we are
|
||||
setting representations
|
||||
instance (dict): instance data for which we are
|
||||
setting representations
|
||||
exp_files (list): list of expected files
|
||||
|
||||
Returns:
|
||||
|
|
@ -528,6 +528,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
# preview video rendering
|
||||
for app in self.aov_filter.keys():
|
||||
if os.environ.get("AVALON_APP", "") == app:
|
||||
# no need to add review if `hasReviewableRepresentations`
|
||||
if instance.get("hasReviewableRepresentations"):
|
||||
break
|
||||
|
||||
# iteratre all aov filters
|
||||
for aov in self.aov_filter[app]:
|
||||
if re.match(
|
||||
aov,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
"image": "img",
|
||||
"reference": "reference"
|
||||
}
|
||||
keep_first_subset_name_for_review = True
|
||||
|
||||
def process(self, instance):
|
||||
self.log.debug("instance {}".format(instance))
|
||||
|
|
@ -168,7 +169,40 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
# Change asset name of each new component for review
|
||||
is_first_review_repre = True
|
||||
not_first_components = []
|
||||
extended_asset_name = ""
|
||||
multiple_reviewable = len(review_representations) > 1
|
||||
for repre in review_representations:
|
||||
# Create copy of base comp item and append it
|
||||
review_item = copy.deepcopy(base_component_item)
|
||||
|
||||
# condition for multiple reviewable representations
|
||||
# expand name to better label componenst
|
||||
if (
|
||||
not self.keep_first_subset_name_for_review
|
||||
and multiple_reviewable
|
||||
):
|
||||
asset_name = review_item["asset_data"]["name"]
|
||||
# define new extended name
|
||||
extended_asset_name = "_".join(
|
||||
(asset_name, repre["name"])
|
||||
)
|
||||
review_item["asset_data"]["name"] = extended_asset_name
|
||||
|
||||
# rename asset name only if multiple reviewable repre
|
||||
if is_first_review_repre:
|
||||
# and rename all already created components
|
||||
for _ci in component_list:
|
||||
_ci["asset_data"]["name"] = extended_asset_name
|
||||
|
||||
# and rename all already created src components
|
||||
for _sci in src_components_to_add:
|
||||
_sci["asset_data"]["name"] = extended_asset_name
|
||||
|
||||
# rename also first thumbnail component if any
|
||||
if first_thumbnail_component is not None:
|
||||
first_thumbnail_component[
|
||||
"asset_data"]["name"] = extended_asset_name
|
||||
|
||||
frame_start = repre.get("frameStartFtrack")
|
||||
frame_end = repre.get("frameEndFtrack")
|
||||
if frame_start is None or frame_end is None:
|
||||
|
|
@ -184,8 +218,6 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
if fps is None:
|
||||
fps = instance_fps
|
||||
|
||||
# Create copy of base comp item and append it
|
||||
review_item = copy.deepcopy(base_component_item)
|
||||
# Change location
|
||||
review_item["component_path"] = repre["published_path"]
|
||||
# Change component data
|
||||
|
|
@ -200,18 +232,15 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
})
|
||||
}
|
||||
}
|
||||
# Create copy of item before setting location or changing asset
|
||||
src_components_to_add.append(copy.deepcopy(review_item))
|
||||
|
||||
if is_first_review_repre:
|
||||
is_first_review_repre = False
|
||||
else:
|
||||
# Add representation name to asset name of "not first" review
|
||||
asset_name = review_item["asset_data"]["name"]
|
||||
review_item["asset_data"]["name"] = "_".join(
|
||||
(asset_name, repre["name"])
|
||||
)
|
||||
not_first_components.append(review_item)
|
||||
|
||||
# Create copy of item before setting location
|
||||
src_components_to_add.append(copy.deepcopy(review_item))
|
||||
|
||||
# Set location
|
||||
review_item["component_location"] = ftrack_server_location
|
||||
# Add item to component list
|
||||
|
|
@ -249,6 +278,11 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
continue
|
||||
# Create copy of base comp item and append it
|
||||
other_item = copy.deepcopy(base_component_item)
|
||||
|
||||
# add extended name if any
|
||||
if extended_asset_name:
|
||||
other_item["asset_data"]["name"] = extended_asset_name
|
||||
|
||||
other_item["component_data"] = {
|
||||
"name": repre["name"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
"use_published": true,
|
||||
"priority": 50,
|
||||
"chunk_size": 10,
|
||||
"concurrent_tasks": 1,
|
||||
"primary_pool": "",
|
||||
"secondary_pool": "",
|
||||
"group": "",
|
||||
|
|
|
|||
|
|
@ -407,7 +407,8 @@
|
|||
"vrayproxy": "cache",
|
||||
"redshiftproxy": "cache",
|
||||
"usd": "usd"
|
||||
}
|
||||
},
|
||||
"keep_first_subset_name_for_review": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -106,6 +106,9 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"ExtractReviewData": {
|
||||
"enabled": false
|
||||
},
|
||||
"ExtractReviewDataLut": {
|
||||
"enabled": false
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"stop_timer_on_application_exit": false,
|
||||
"publish": {
|
||||
"CollectRenderScene": {
|
||||
"enabled": false,
|
||||
"render_layer": "Main"
|
||||
},
|
||||
"ExtractSequence": {
|
||||
"review_bg": [
|
||||
255,
|
||||
|
|
|
|||
|
|
@ -192,6 +192,9 @@
|
|||
"key": "use_published",
|
||||
"label": "Use Published scene"
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "priority",
|
||||
|
|
@ -202,6 +205,14 @@
|
|||
"key": "chunk_size",
|
||||
"label": "Chunk Size"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "concurrent_tasks",
|
||||
"label": "Number of concurrent tasks"
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "primary_pool",
|
||||
|
|
@ -217,6 +228,9 @@
|
|||
"key": "group",
|
||||
"label": "Group"
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "department",
|
||||
|
|
|
|||
|
|
@ -784,6 +784,12 @@
|
|||
"object_type": {
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "keep_first_subset_name_for_review",
|
||||
"label": "Make subset name as first asset name",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,30 @@
|
|||
"key": "publish",
|
||||
"label": "Publish plugins",
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "CollectRenderScene",
|
||||
"label": "Collect Render Scene",
|
||||
"is_group": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "It is possible to fill <b>'render_layer'</b> or <b>'variant'</b> in subset name template with custom value.<br/>- value of <b>'render_pass'</b> is always \"beauty\"."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "render_layer",
|
||||
"label": "Render Layer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -138,6 +138,21 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"key": "ExtractReviewData",
|
||||
"label": "ExtractReviewData",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -1269,6 +1269,14 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
|||
background: #21252B;
|
||||
}
|
||||
|
||||
/* Workfiles */
|
||||
#WorkfilesPublishedContextSelect {
|
||||
background: rgba(0, 0, 0, 127);
|
||||
}
|
||||
#WorkfilesPublishedContextSelect QLabel {
|
||||
font-size: 17pt;
|
||||
}
|
||||
|
||||
/* Tray */
|
||||
#TrayRestartButton {
|
||||
background: {color:restart-btn-bg};
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ from .model import (
|
|||
DATE_MODIFIED_ROLE,
|
||||
)
|
||||
from .save_as_dialog import SaveAsDialog
|
||||
from .lib import TempPublishFiles
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -45,11 +44,35 @@ class FilesView(QtWidgets.QTreeView):
|
|||
return super(FilesView, self).mouseDoubleClickEvent(event)
|
||||
|
||||
|
||||
class SelectContextOverlay(QtWidgets.QFrame):
|
||||
def __init__(self, parent):
|
||||
super(SelectContextOverlay, self).__init__(parent)
|
||||
|
||||
self.setObjectName("WorkfilesPublishedContextSelect")
|
||||
label_widget = QtWidgets.QLabel(
|
||||
"Please choose context on the left<br/><",
|
||||
self
|
||||
)
|
||||
label_widget.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter)
|
||||
|
||||
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
parent.installEventFilter(self)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QtCore.QEvent.Resize:
|
||||
self.resize(obj.size())
|
||||
|
||||
return super(SelectContextOverlay, self).eventFilter(obj, event)
|
||||
|
||||
|
||||
class FilesWidget(QtWidgets.QWidget):
|
||||
"""A widget displaying files that allows to save and open files."""
|
||||
file_selected = QtCore.Signal(str)
|
||||
file_opened = QtCore.Signal()
|
||||
publish_file_viewed = QtCore.Signal()
|
||||
workfile_created = QtCore.Signal(str)
|
||||
published_visible_changed = QtCore.Signal(bool)
|
||||
|
||||
|
|
@ -71,9 +94,6 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
self._workfiles_root = None
|
||||
self._workdir_path = None
|
||||
self.host = api.registered_host()
|
||||
temp_publish_files = TempPublishFiles()
|
||||
temp_publish_files.cleanup()
|
||||
self._temp_publish_files = temp_publish_files
|
||||
|
||||
# Whether to automatically select the latest modified
|
||||
# file on a refresh of the files model.
|
||||
|
|
@ -93,14 +113,14 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
|
||||
filter_layout = QtWidgets.QHBoxLayout(filter_widget)
|
||||
filter_layout.setContentsMargins(0, 0, 0, 0)
|
||||
filter_layout.addWidget(published_checkbox, 0)
|
||||
filter_layout.addWidget(filter_input, 1)
|
||||
filter_layout.addWidget(published_checkbox, 0)
|
||||
|
||||
# Create the Files models
|
||||
extensions = set(self.host.file_extensions())
|
||||
|
||||
views_widget = QtWidgets.QWidget(self)
|
||||
# Workarea view
|
||||
# --- Workarea view ---
|
||||
workarea_files_model = WorkAreaFilesModel(extensions)
|
||||
|
||||
# Create proxy model for files to be able sort and filter
|
||||
|
|
@ -118,13 +138,14 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
# Date modified delegate
|
||||
workarea_time_delegate = PrettyTimeDelegate()
|
||||
workarea_files_view.setItemDelegateForColumn(1, workarea_time_delegate)
|
||||
workarea_files_view.setIndentation(3) # smaller indentation
|
||||
# smaller indentation
|
||||
workarea_files_view.setIndentation(3)
|
||||
|
||||
# Default to a wider first filename column it is what we mostly care
|
||||
# about and the date modified is relatively small anyway.
|
||||
workarea_files_view.setColumnWidth(0, 330)
|
||||
|
||||
# Publish files view
|
||||
# --- Publish files view ---
|
||||
publish_files_model = PublishFilesModel(extensions, io, self.anatomy)
|
||||
|
||||
publish_proxy_model = QtCore.QSortFilterProxyModel()
|
||||
|
|
@ -141,12 +162,16 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
# Date modified delegate
|
||||
publish_time_delegate = PrettyTimeDelegate()
|
||||
publish_files_view.setItemDelegateForColumn(1, publish_time_delegate)
|
||||
publish_files_view.setIndentation(3) # smaller indentation
|
||||
# smaller indentation
|
||||
publish_files_view.setIndentation(3)
|
||||
|
||||
# Default to a wider first filename column it is what we mostly care
|
||||
# about and the date modified is relatively small anyway.
|
||||
publish_files_view.setColumnWidth(0, 330)
|
||||
|
||||
publish_context_overlay = SelectContextOverlay(views_widget)
|
||||
publish_context_overlay.setVisible(False)
|
||||
|
||||
views_layout = QtWidgets.QHBoxLayout(views_widget)
|
||||
views_layout.setContentsMargins(0, 0, 0, 0)
|
||||
views_layout.addWidget(workarea_files_view, 1)
|
||||
|
|
@ -155,18 +180,43 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
# Home Page
|
||||
# Build buttons widget for files widget
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
btn_save = QtWidgets.QPushButton("Save As", btns_widget)
|
||||
btn_browse = QtWidgets.QPushButton("Browse", btns_widget)
|
||||
btn_open = QtWidgets.QPushButton("Open", btns_widget)
|
||||
|
||||
btn_view_published = QtWidgets.QPushButton("View", btns_widget)
|
||||
workarea_btns_widget = QtWidgets.QWidget(btns_widget)
|
||||
btn_save = QtWidgets.QPushButton("Save As", workarea_btns_widget)
|
||||
btn_browse = QtWidgets.QPushButton("Browse", workarea_btns_widget)
|
||||
btn_open = QtWidgets.QPushButton("Open", workarea_btns_widget)
|
||||
|
||||
workarea_btns_layout = QtWidgets.QHBoxLayout(workarea_btns_widget)
|
||||
workarea_btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
workarea_btns_layout.addWidget(btn_open, 1)
|
||||
workarea_btns_layout.addWidget(btn_browse, 1)
|
||||
workarea_btns_layout.addWidget(btn_save, 1)
|
||||
|
||||
publish_btns_widget = QtWidgets.QWidget(btns_widget)
|
||||
btn_save_as_published = QtWidgets.QPushButton(
|
||||
"Copy && Open", publish_btns_widget
|
||||
)
|
||||
btn_change_context = QtWidgets.QPushButton(
|
||||
"Choose different context", publish_btns_widget
|
||||
)
|
||||
btn_select_context_published = QtWidgets.QPushButton(
|
||||
"Copy && Open", publish_btns_widget
|
||||
)
|
||||
btn_cancel_published = QtWidgets.QPushButton(
|
||||
"Cancel", publish_btns_widget
|
||||
)
|
||||
|
||||
publish_btns_layout = QtWidgets.QHBoxLayout(publish_btns_widget)
|
||||
publish_btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
publish_btns_layout.addWidget(btn_save_as_published, 1)
|
||||
publish_btns_layout.addWidget(btn_change_context, 1)
|
||||
publish_btns_layout.addWidget(btn_select_context_published, 1)
|
||||
publish_btns_layout.addWidget(btn_cancel_published, 1)
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
btns_layout.addWidget(btn_open, 1)
|
||||
btns_layout.addWidget(btn_browse, 1)
|
||||
btns_layout.addWidget(btn_save, 1)
|
||||
btns_layout.addWidget(btn_view_published, 1)
|
||||
btns_layout.addWidget(workarea_btns_widget, 1)
|
||||
btns_layout.addWidget(publish_btns_widget, 1)
|
||||
|
||||
# Build files widgets for home page
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
|
|
@ -188,14 +238,22 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
workarea_files_view.selectionModel().selectionChanged.connect(
|
||||
self.on_file_select
|
||||
)
|
||||
publish_files_view.doubleClickedLeft.connect(
|
||||
self._on_view_published_pressed
|
||||
)
|
||||
|
||||
btn_open.pressed.connect(self._on_workarea_open_pressed)
|
||||
btn_browse.pressed.connect(self.on_browse_pressed)
|
||||
btn_save.pressed.connect(self.on_save_as_pressed)
|
||||
btn_view_published.pressed.connect(self._on_view_published_pressed)
|
||||
btn_save.pressed.connect(self._on_save_as_pressed)
|
||||
btn_save_as_published.pressed.connect(
|
||||
self._on_published_save_as_pressed
|
||||
)
|
||||
btn_change_context.pressed.connect(
|
||||
self._on_publish_change_context_pressed
|
||||
)
|
||||
btn_select_context_published.pressed.connect(
|
||||
self._on_publish_select_context_pressed
|
||||
)
|
||||
btn_cancel_published.pressed.connect(
|
||||
self._on_publish_cancel_pressed
|
||||
)
|
||||
|
||||
# Store attributes
|
||||
self._published_checkbox = published_checkbox
|
||||
|
|
@ -211,18 +269,29 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
self._publish_files_model = publish_files_model
|
||||
self._publish_proxy_model = publish_proxy_model
|
||||
|
||||
self._btns_widget = btns_widget
|
||||
self._publish_context_overlay = publish_context_overlay
|
||||
|
||||
self._workarea_btns_widget = workarea_btns_widget
|
||||
self._publish_btns_widget = publish_btns_widget
|
||||
self._btn_open = btn_open
|
||||
self._btn_browse = btn_browse
|
||||
self._btn_save = btn_save
|
||||
self._btn_view_published = btn_view_published
|
||||
|
||||
self._btn_save_as_published = btn_save_as_published
|
||||
self._btn_change_context = btn_change_context
|
||||
self._btn_select_context_published = btn_select_context_published
|
||||
self._btn_cancel_published = btn_cancel_published
|
||||
|
||||
# Create a proxy widget for files widget
|
||||
self.setFocusProxy(btn_open)
|
||||
|
||||
# Hide publish files widgets
|
||||
publish_files_view.setVisible(False)
|
||||
btn_view_published.setVisible(False)
|
||||
publish_btns_widget.setVisible(False)
|
||||
btn_select_context_published.setVisible(False)
|
||||
btn_cancel_published.setVisible(False)
|
||||
|
||||
self._publish_context_select_mode = False
|
||||
|
||||
@property
|
||||
def published_enabled(self):
|
||||
|
|
@ -232,12 +301,10 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
published_enabled = self.published_enabled
|
||||
|
||||
self._workarea_files_view.setVisible(not published_enabled)
|
||||
self._btn_open.setVisible(not published_enabled)
|
||||
self._btn_browse.setVisible(not published_enabled)
|
||||
self._btn_save.setVisible(not published_enabled)
|
||||
self._workarea_btns_widget.setVisible(not published_enabled)
|
||||
|
||||
self._publish_files_view.setVisible(published_enabled)
|
||||
self._btn_view_published.setVisible(published_enabled)
|
||||
self._publish_btns_widget.setVisible(published_enabled)
|
||||
|
||||
self._update_filtering()
|
||||
self._update_asset_task()
|
||||
|
|
@ -258,6 +325,9 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
|
||||
def set_save_enabled(self, enabled):
|
||||
self._btn_save.setEnabled(enabled)
|
||||
if not enabled and self._published_checkbox.isChecked():
|
||||
self._published_checkbox.setChecked(False)
|
||||
self._published_checkbox.setVisible(enabled)
|
||||
|
||||
def set_asset_task(self, asset_id, task_name, task_type):
|
||||
if asset_id != self._asset_id:
|
||||
|
|
@ -268,12 +338,14 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
self._update_asset_task()
|
||||
|
||||
def _update_asset_task(self):
|
||||
if self.published_enabled:
|
||||
if self.published_enabled and not self._publish_context_select_mode:
|
||||
self._publish_files_model.set_context(
|
||||
self._asset_id, self._task_name
|
||||
)
|
||||
has_valid_items = self._publish_files_model.has_valid_items()
|
||||
self._btn_view_published.setEnabled(has_valid_items)
|
||||
self._btn_save_as_published.setEnabled(has_valid_items)
|
||||
self._btn_change_context.setEnabled(has_valid_items)
|
||||
|
||||
else:
|
||||
# Define a custom session so we can query the work root
|
||||
# for a "Work area" that is not our current Session.
|
||||
|
|
@ -291,6 +363,13 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
has_valid_items = self._workarea_files_model.has_valid_items()
|
||||
self._btn_browse.setEnabled(has_valid_items)
|
||||
self._btn_open.setEnabled(has_valid_items)
|
||||
|
||||
if self._publish_context_select_mode:
|
||||
self._btn_select_context_published.setEnabled(
|
||||
bool(self._asset_id) and bool(self._task_name)
|
||||
)
|
||||
return
|
||||
|
||||
# Manually trigger file selection
|
||||
if not has_valid_items:
|
||||
self.on_file_select()
|
||||
|
|
@ -400,11 +479,18 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
"""
|
||||
session = self._get_session()
|
||||
|
||||
if self.published_enabled:
|
||||
filepath = self._get_selected_filepath()
|
||||
extensions = [os.path.splitext(filepath)[1]]
|
||||
else:
|
||||
extensions = self.host.file_extensions()
|
||||
|
||||
window = SaveAsDialog(
|
||||
parent=self,
|
||||
root=self._workfiles_root,
|
||||
anatomy=self.anatomy,
|
||||
template_key=self.template_key,
|
||||
extensions=extensions,
|
||||
session=session
|
||||
)
|
||||
window.exec_()
|
||||
|
|
@ -462,10 +548,15 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
if work_file:
|
||||
self.open_file(work_file)
|
||||
|
||||
def on_save_as_pressed(self):
|
||||
def _on_save_as_pressed(self):
|
||||
self._save_as_with_dialog()
|
||||
|
||||
def _save_as_with_dialog(self):
|
||||
work_filename = self.get_filename()
|
||||
if not work_filename:
|
||||
return
|
||||
return None
|
||||
|
||||
src_path = self._get_selected_filepath()
|
||||
|
||||
# Trigger before save event
|
||||
emit_event(
|
||||
|
|
@ -486,13 +577,20 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
log.debug("Initializing Work Directory: %s", self._workfiles_root)
|
||||
os.makedirs(self._workfiles_root)
|
||||
|
||||
# Update session if context has changed
|
||||
self._enter_session()
|
||||
# Prepare full path to workfile and save it
|
||||
filepath = os.path.join(
|
||||
os.path.normpath(self._workfiles_root), work_filename
|
||||
)
|
||||
self.host.save_file(filepath)
|
||||
|
||||
# Update session if context has changed
|
||||
self._enter_session()
|
||||
|
||||
if not self.published_enabled:
|
||||
self.host.save_file(filepath)
|
||||
else:
|
||||
shutil.copy(src_path, filepath)
|
||||
self.host.open_file(filepath)
|
||||
|
||||
# Create extra folders
|
||||
create_workdir_extra_folders(
|
||||
self._workdir_path,
|
||||
|
|
@ -510,17 +608,55 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
|
||||
self.workfile_created.emit(filepath)
|
||||
# Refresh files model
|
||||
self.refresh()
|
||||
if self.published_enabled:
|
||||
self._published_checkbox.setChecked(False)
|
||||
else:
|
||||
self.refresh()
|
||||
return filepath
|
||||
|
||||
def _on_view_published_pressed(self):
|
||||
filepath = self._get_selected_filepath()
|
||||
if not filepath or not os.path.exists(filepath):
|
||||
return
|
||||
item = self._temp_publish_files.add_file(filepath)
|
||||
self.host.open_file(item.filepath)
|
||||
self.publish_file_viewed.emit()
|
||||
# Change state back to workarea
|
||||
self._published_checkbox.setChecked(False)
|
||||
def _on_published_save_as_pressed(self):
|
||||
self._save_as_with_dialog()
|
||||
|
||||
def _set_publish_context_select_mode(self, enabled):
|
||||
self._publish_context_select_mode = enabled
|
||||
|
||||
# Show buttons related to context selection
|
||||
self._publish_context_overlay.setVisible(enabled)
|
||||
self._btn_cancel_published.setVisible(enabled)
|
||||
self._btn_select_context_published.setVisible(enabled)
|
||||
# Change enabled state based on select context
|
||||
self._btn_select_context_published.setEnabled(
|
||||
bool(self._asset_id) and bool(self._task_name)
|
||||
)
|
||||
|
||||
self._btn_save_as_published.setVisible(not enabled)
|
||||
self._btn_change_context.setVisible(not enabled)
|
||||
|
||||
# Change views and disable workarea view if enabled
|
||||
self._workarea_files_view.setEnabled(not enabled)
|
||||
if self.published_enabled:
|
||||
self._workarea_files_view.setVisible(enabled)
|
||||
self._publish_files_view.setVisible(not enabled)
|
||||
else:
|
||||
self._workarea_files_view.setVisible(True)
|
||||
self._publish_files_view.setVisible(False)
|
||||
|
||||
# Disable filter widgets
|
||||
self._published_checkbox.setEnabled(not enabled)
|
||||
self._filter_input.setEnabled(not enabled)
|
||||
|
||||
def _on_publish_change_context_pressed(self):
|
||||
self._set_publish_context_select_mode(True)
|
||||
|
||||
def _on_publish_select_context_pressed(self):
|
||||
result = self._save_as_with_dialog()
|
||||
if result is not None:
|
||||
self._set_publish_context_select_mode(False)
|
||||
self._update_asset_task()
|
||||
|
||||
def _on_publish_cancel_pressed(self):
|
||||
self._set_publish_context_select_mode(False)
|
||||
self._update_asset_task()
|
||||
|
||||
def on_file_select(self):
|
||||
self.file_selected.emit(self._get_selected_filepath())
|
||||
|
|
|
|||
|
|
@ -1,272 +0,0 @@
|
|||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
import contextlib
|
||||
|
||||
import appdirs
|
||||
|
||||
|
||||
class TempPublishFilesItem(object):
|
||||
"""Object representing copied workfile in app temp folder.
|
||||
|
||||
Args:
|
||||
item_id (str): Id of item used as subfolder.
|
||||
data (dict): Metadata about temp files.
|
||||
directory (str): Path to directory where files are copied to.
|
||||
"""
|
||||
|
||||
def __init__(self, item_id, data, directory):
|
||||
self._id = item_id
|
||||
self._directory = directory
|
||||
self._filepath = os.path.join(directory, data["filename"])
|
||||
|
||||
@property
|
||||
def directory(self):
|
||||
return self._directory
|
||||
|
||||
@property
|
||||
def filepath(self):
|
||||
return self._filepath
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
if os.path.exists(self.filepath):
|
||||
s = os.stat(self.filepath)
|
||||
return s.st_size
|
||||
return 0
|
||||
|
||||
|
||||
class TempPublishFiles(object):
|
||||
"""Directory where published workfiles are copied when opened.
|
||||
|
||||
Directory is located in appdirs on the machine. Folder contains file
|
||||
with metadata about stored files. Each item in metadata has id, filename
|
||||
and expiration time. When expiration time is higher then current time the
|
||||
item is removed from metadata and it's files are deleted. Files of items
|
||||
are stored in subfolder named by item's id.
|
||||
|
||||
Metadata file can be in theory opened and modified by multiple processes,
|
||||
threads at one time. For those cases is created simple lock file which
|
||||
is created before modification begins and is removed when modification
|
||||
ends. Existence of the file means that it should not be modified by
|
||||
any other process at the same time.
|
||||
|
||||
Metadata example:
|
||||
```
|
||||
{
|
||||
"96050b4a-8974-4fca-8179-7c446c478d54": {
|
||||
"created": 1647880725.555,
|
||||
"expiration": 1647884325.555,
|
||||
"filename": "cg_pigeon_workfileModeling_v025.ma"
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Why is this needed
|
||||
Combination of more issues. Temp files are not automatically removed by
|
||||
OS on windows so using tempfiles in TEMP would lead to kill disk space of
|
||||
machine. There are also cases when someone wants to open multiple files
|
||||
in short period of time and want to manually remove those files so keeping
|
||||
track of temporary copied files in pre-defined structure is needed.
|
||||
"""
|
||||
minute_in_seconds = 60
|
||||
hour_in_seconds = 60 * minute_in_seconds
|
||||
day_in_seconds = 24 * hour_in_seconds
|
||||
|
||||
def __init__(self):
|
||||
root_dir = appdirs.user_data_dir(
|
||||
"published_workfiles_temp", "openpype"
|
||||
)
|
||||
if not os.path.exists(root_dir):
|
||||
os.makedirs(root_dir)
|
||||
|
||||
metadata_path = os.path.join(root_dir, "metadata.json")
|
||||
lock_path = os.path.join(root_dir, "lock.json")
|
||||
|
||||
self._root_dir = root_dir
|
||||
self._metadata_path = metadata_path
|
||||
self._lock_path = lock_path
|
||||
self._log = None
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = logging.getLogger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
@property
|
||||
def life_time(self):
|
||||
"""How long will be new item kept in temp in seconds.
|
||||
|
||||
Returns:
|
||||
int: Lifetime of temp item.
|
||||
"""
|
||||
return int(self.hour_in_seconds)
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
"""File size of existing items."""
|
||||
size = 0
|
||||
for item in self.get_items():
|
||||
size += item.size
|
||||
return size
|
||||
|
||||
def add_file(self, src_path):
|
||||
"""Add workfile to temp directory.
|
||||
|
||||
This will create new item and source path is copied to it's directory.
|
||||
"""
|
||||
filename = os.path.basename(src_path)
|
||||
|
||||
item_id = str(uuid.uuid4())
|
||||
dst_dirpath = os.path.join(self._root_dir, item_id)
|
||||
if not os.path.exists(dst_dirpath):
|
||||
os.makedirs(dst_dirpath)
|
||||
|
||||
dst_path = os.path.join(dst_dirpath, filename)
|
||||
shutil.copy(src_path, dst_path)
|
||||
|
||||
now = time.time()
|
||||
item_data = {
|
||||
"filename": filename,
|
||||
"expiration": now + self.life_time,
|
||||
"created": now
|
||||
}
|
||||
with self._modify_data() as data:
|
||||
data[item_id] = item_data
|
||||
|
||||
return TempPublishFilesItem(item_id, item_data, dst_dirpath)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _modify_data(self):
|
||||
"""Create lock file when data in metadata file are modified."""
|
||||
start_time = time.time()
|
||||
timeout = 3
|
||||
while os.path.exists(self._lock_path):
|
||||
time.sleep(0.01)
|
||||
if start_time > timeout:
|
||||
self.log.warning((
|
||||
"Waited for {} seconds to free lock file. Overriding lock."
|
||||
).format(timeout))
|
||||
|
||||
with open(self._lock_path, "w") as stream:
|
||||
json.dump({"pid": os.getpid()}, stream)
|
||||
|
||||
try:
|
||||
data = self._get_data()
|
||||
yield data
|
||||
with open(self._metadata_path, "w") as stream:
|
||||
json.dump(data, stream)
|
||||
|
||||
finally:
|
||||
os.remove(self._lock_path)
|
||||
|
||||
def _get_data(self):
|
||||
output = {}
|
||||
if not os.path.exists(self._metadata_path):
|
||||
return output
|
||||
|
||||
try:
|
||||
with open(self._metadata_path, "r") as stream:
|
||||
output = json.load(stream)
|
||||
except Exception:
|
||||
self.log.warning("Failed to read metadata file.", exc_info=True)
|
||||
return output
|
||||
|
||||
def cleanup(self, check_expiration=True):
|
||||
"""Cleanup files based on metadata.
|
||||
|
||||
Items that passed expiration are removed when this is called. Or all
|
||||
files are removed when `check_expiration` is set to False.
|
||||
|
||||
Args:
|
||||
check_expiration (bool): All items and files are removed when set
|
||||
to True.
|
||||
"""
|
||||
data = self._get_data()
|
||||
now = time.time()
|
||||
remove_ids = set()
|
||||
all_ids = set()
|
||||
for item_id, item_data in data.items():
|
||||
all_ids.add(item_id)
|
||||
if check_expiration and now < item_data["expiration"]:
|
||||
continue
|
||||
|
||||
remove_ids.add(item_id)
|
||||
|
||||
for item_id in remove_ids:
|
||||
try:
|
||||
self.remove_id(item_id)
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to remove temp publish item \"{}\"".format(
|
||||
item_id
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
# Remove unknown folders/files
|
||||
for filename in os.listdir(self._root_dir):
|
||||
if filename in all_ids:
|
||||
continue
|
||||
|
||||
full_path = os.path.join(self._root_dir, filename)
|
||||
if full_path in (self._metadata_path, self._lock_path):
|
||||
continue
|
||||
|
||||
try:
|
||||
shutil.rmtree(full_path)
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Couldn't remove arbitrary path \"{}\"".format(full_path),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
self.cleanup(False)
|
||||
|
||||
def get_items(self):
|
||||
"""Receive all items from metadata file.
|
||||
|
||||
Returns:
|
||||
list<TempPublishFilesItem>: Info about each item in metadata.
|
||||
"""
|
||||
output = []
|
||||
data = self._get_data()
|
||||
for item_id, item_data in data.items():
|
||||
item_path = os.path.join(self._root_dir, item_id)
|
||||
output.append(TempPublishFilesItem(item_id, item_data, item_path))
|
||||
return output
|
||||
|
||||
def remove_id(self, item_id):
|
||||
"""Remove files of item and then remove the item from metadata."""
|
||||
filepath = os.path.join(self._root_dir, item_id)
|
||||
if os.path.exists(filepath):
|
||||
shutil.rmtree(filepath)
|
||||
|
||||
with self._modify_data() as data:
|
||||
data.pop(item_id, None)
|
||||
|
||||
|
||||
def file_size_to_string(file_size):
|
||||
size = 0
|
||||
size_ending_mapping = {
|
||||
"KB": 1024 ** 1,
|
||||
"MB": 1024 ** 2,
|
||||
"GB": 1024 ** 3
|
||||
}
|
||||
ending = "B"
|
||||
for _ending, _size in size_ending_mapping.items():
|
||||
if file_size < _size:
|
||||
break
|
||||
size = file_size / _size
|
||||
ending = _ending
|
||||
return "{:.2f} {}".format(size, ending)
|
||||
|
|
@ -193,7 +193,9 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, parent, root, anatomy, template_key, session=None):
|
||||
def __init__(
|
||||
self, parent, root, anatomy, template_key, extensions, session=None
|
||||
):
|
||||
super(SaveAsDialog, self).__init__(parent=parent)
|
||||
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
|
||||
|
||||
|
|
@ -201,6 +203,7 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
self.host = api.registered_host()
|
||||
self.root = root
|
||||
self.work_file = None
|
||||
self._extensions = extensions
|
||||
|
||||
if not session:
|
||||
# Fallback to active session
|
||||
|
|
@ -257,7 +260,7 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
# Add styled delegate to use stylesheets
|
||||
ext_delegate = QtWidgets.QStyledItemDelegate()
|
||||
ext_combo.setItemDelegate(ext_delegate)
|
||||
ext_combo.addItems(self.host.file_extensions())
|
||||
ext_combo.addItems(self._extensions)
|
||||
|
||||
# Build inputs
|
||||
inputs_layout = QtWidgets.QFormLayout(inputs_widget)
|
||||
|
|
@ -336,7 +339,7 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
|
||||
def get_existing_comments(self):
|
||||
matcher = CommentMatcher(self.anatomy, self.template_key, self.data)
|
||||
host_extensions = set(self.host.file_extensions())
|
||||
host_extensions = set(self._extensions)
|
||||
comments = set()
|
||||
if os.path.isdir(self.root):
|
||||
for fname in os.listdir(self.root):
|
||||
|
|
@ -392,7 +395,7 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
return anatomy_filled[self.template_key]["file"]
|
||||
|
||||
def refresh(self):
|
||||
extensions = self.host.file_extensions()
|
||||
extensions = list(self._extensions)
|
||||
extension = self.data["ext"]
|
||||
if extension is None:
|
||||
# Define saving file extension
|
||||
|
|
|
|||
|
|
@ -14,7 +14,22 @@ from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
|
|||
from openpype.tools.utils.tasks_widget import TasksWidget
|
||||
|
||||
from .files_widget import FilesWidget
|
||||
from .lib import TempPublishFiles, file_size_to_string
|
||||
|
||||
|
||||
def file_size_to_string(file_size):
|
||||
size = 0
|
||||
size_ending_mapping = {
|
||||
"KB": 1024 ** 1,
|
||||
"MB": 1024 ** 2,
|
||||
"GB": 1024 ** 3
|
||||
}
|
||||
ending = "B"
|
||||
for _ending, _size in size_ending_mapping.items():
|
||||
if file_size < _size:
|
||||
break
|
||||
size = file_size / _size
|
||||
ending = _ending
|
||||
return "{:.2f} {}".format(size, ending)
|
||||
|
||||
|
||||
class SidePanelWidget(QtWidgets.QWidget):
|
||||
|
|
@ -44,67 +59,25 @@ class SidePanelWidget(QtWidgets.QWidget):
|
|||
btn_note_save, 0, alignment=QtCore.Qt.AlignRight
|
||||
)
|
||||
|
||||
publish_temp_widget = QtWidgets.QWidget(self)
|
||||
publish_temp_info_label = QtWidgets.QLabel(
|
||||
self.published_workfile_message.format(
|
||||
file_size_to_string(0)
|
||||
),
|
||||
publish_temp_widget
|
||||
)
|
||||
publish_temp_info_label.setWordWrap(True)
|
||||
|
||||
btn_clear_temp = QtWidgets.QPushButton(
|
||||
"Clear temp", publish_temp_widget
|
||||
)
|
||||
|
||||
publish_temp_layout = QtWidgets.QVBoxLayout(publish_temp_widget)
|
||||
publish_temp_layout.setContentsMargins(0, 0, 0, 0)
|
||||
publish_temp_layout.addWidget(publish_temp_info_label, 0)
|
||||
publish_temp_layout.addWidget(
|
||||
btn_clear_temp, 0, alignment=QtCore.Qt.AlignRight
|
||||
)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(details_label, 0)
|
||||
main_layout.addWidget(details_input, 1)
|
||||
main_layout.addWidget(artist_note_widget, 1)
|
||||
main_layout.addWidget(publish_temp_widget, 0)
|
||||
|
||||
note_input.textChanged.connect(self._on_note_change)
|
||||
btn_note_save.clicked.connect(self._on_save_click)
|
||||
btn_clear_temp.clicked.connect(self._on_clear_temp_click)
|
||||
|
||||
self._details_input = details_input
|
||||
self._artist_note_widget = artist_note_widget
|
||||
self._note_input = note_input
|
||||
self._btn_note_save = btn_note_save
|
||||
|
||||
self._publish_temp_info_label = publish_temp_info_label
|
||||
self._publish_temp_widget = publish_temp_widget
|
||||
|
||||
self._orig_note = ""
|
||||
self._workfile_doc = None
|
||||
|
||||
publish_temp_widget.setVisible(False)
|
||||
|
||||
def set_published_visible(self, published_visible):
|
||||
self._artist_note_widget.setVisible(not published_visible)
|
||||
self._publish_temp_widget.setVisible(published_visible)
|
||||
if published_visible:
|
||||
self.refresh_publish_temp_sizes()
|
||||
|
||||
def refresh_publish_temp_sizes(self):
|
||||
temp_publish_files = TempPublishFiles()
|
||||
text = self.published_workfile_message.format(
|
||||
file_size_to_string(temp_publish_files.size)
|
||||
)
|
||||
self._publish_temp_info_label.setText(text)
|
||||
|
||||
def _on_clear_temp_click(self):
|
||||
temp_publish_files = TempPublishFiles()
|
||||
temp_publish_files.clear()
|
||||
self.refresh_publish_temp_sizes()
|
||||
|
||||
def _on_note_change(self):
|
||||
text = self._note_input.toPlainText()
|
||||
|
|
@ -225,9 +198,6 @@ class Window(QtWidgets.QMainWindow):
|
|||
files_widget.file_selected.connect(self.on_file_select)
|
||||
files_widget.workfile_created.connect(self.on_workfile_create)
|
||||
files_widget.file_opened.connect(self._on_file_opened)
|
||||
files_widget.publish_file_viewed.connect(
|
||||
self._on_publish_file_viewed
|
||||
)
|
||||
files_widget.published_visible_changed.connect(
|
||||
self._on_published_change
|
||||
)
|
||||
|
|
@ -292,9 +262,6 @@ class Window(QtWidgets.QMainWindow):
|
|||
def _on_file_opened(self):
|
||||
self.close()
|
||||
|
||||
def _on_publish_file_viewed(self):
|
||||
self.side_panel.refresh_publish_temp_sizes()
|
||||
|
||||
def _on_published_change(self, visible):
|
||||
self.side_panel.set_published_visible(visible)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.9.2-nightly.3"
|
||||
__version__ = "3.9.2"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "OpenPype"
|
||||
version = "3.9.2-nightly.3" # OpenPype
|
||||
version = "3.9.2" # OpenPype
|
||||
description = "Open VFX and Animation pipeline with support."
|
||||
authors = ["OpenPype Team <info@openpype.io>"]
|
||||
license = "MIT License"
|
||||
|
|
@ -136,3 +136,19 @@ hash = "de63a8bf7f6c45ff59ecafeba13123f710c2cbc1783ec9e0b938e980d4f5c37f"
|
|||
[openpype.thirdparty.oiio.darwin]
|
||||
url = "https://distribute.openpype.io/thirdparty/oiio-2.2.0-darwin.tgz"
|
||||
hash = "sha256:..."
|
||||
|
||||
[tool.pyright]
|
||||
include = [
|
||||
"igniter",
|
||||
"openpype",
|
||||
"repos",
|
||||
"vendor"
|
||||
]
|
||||
exclude = [
|
||||
"**/node_modules",
|
||||
"**/__pycache__"
|
||||
]
|
||||
ignore = ["website", "docs", ".git"]
|
||||
|
||||
reportMissingImports = true
|
||||
reportMissingTypeStubs = false
|
||||
|
|
@ -123,6 +123,10 @@ To get working connection to Google Drive there are some necessary steps:
|
|||
- add new site back in OpenPype Settings, name as you want, provider needs to be 'gdrive'
|
||||
- distribute credentials file via shared mounted disk location
|
||||
|
||||
:::note
|
||||
If you are using regular personal GDrive for testing don't forget adding `/My Drive` as the prefix in root configuration. Business accounts and share drives don't need this.
|
||||
:::
|
||||
|
||||
### SFTP
|
||||
|
||||
SFTP provider is used to connect to SFTP server. Currently authentication with `user:password` or `user:ssh key` is implemented.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue