Merge remote-tracking branch 'origin/develop' into bugfix/OP-2081_houdini-create-hda

This commit is contained in:
Ondřej Samohel 2021-12-23 09:35:35 +01:00
commit 8f427dc8df
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
345 changed files with 5094 additions and 1464 deletions

16
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,16 @@
## Brief description
First sentence is brief description.
## Description
Next paragraf is more elaborate text with more info. This will be displayed for example in collapsed form under the first sentence in a changelog.
## Additional info
The rest will be ignored in changelog and should contain any additional
technical information.
## Documentation (add _"type: documentation"_ label)
[feature_documentation](future_url_after_it_will_be_merged)
## Testing notes:
1. start with this step
2. follow this step

View file

@ -1,48 +1,63 @@
# Changelog
## [3.7.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD)
## [3.7.0-nightly.10](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.4...HEAD)
### 📖 Documentation
**Deprecated:**
- docs\[website\]: Add Ellipse Studio \(logo\) as an OpenPype contributor [\#2324](https://github.com/pypeclub/OpenPype/pull/2324)
**🆕 New features**
- Store typed version dependencies for workfiles [\#2192](https://github.com/pypeclub/OpenPype/pull/2192)
- General: Default modules hierarchy n2 [\#2368](https://github.com/pypeclub/OpenPype/pull/2368)
**🚀 Enhancements**
- Hiero: Add experimental tools action [\#2323](https://github.com/pypeclub/OpenPype/pull/2323)
- Input links: Cleanup and unification of differences [\#2322](https://github.com/pypeclub/OpenPype/pull/2322)
- General: Run process log stderr as info log level [\#2309](https://github.com/pypeclub/OpenPype/pull/2309)
- Tools: Cleanup of unused classes [\#2304](https://github.com/pypeclub/OpenPype/pull/2304)
- Project Manager: Added ability to delete project [\#2298](https://github.com/pypeclub/OpenPype/pull/2298)
- Ftrack: Synchronize input links [\#2287](https://github.com/pypeclub/OpenPype/pull/2287)
- StandalonePublisher: Remove unused plugin ExtractHarmonyZip [\#2277](https://github.com/pypeclub/OpenPype/pull/2277)
- Ftrack: Support multiple reviews [\#2271](https://github.com/pypeclub/OpenPype/pull/2271)
- Ftrack: Remove unused clean component plugin [\#2269](https://github.com/pypeclub/OpenPype/pull/2269)
- Houdini: Add experimental tools action [\#2267](https://github.com/pypeclub/OpenPype/pull/2267)
- Tools: Assets widget [\#2265](https://github.com/pypeclub/OpenPype/pull/2265)
- Nuke: extract baked review videos presets [\#2248](https://github.com/pypeclub/OpenPype/pull/2248)
- TVPaint: Workers rendering [\#2209](https://github.com/pypeclub/OpenPype/pull/2209)
- General: Environment variables groups [\#2424](https://github.com/pypeclub/OpenPype/pull/2424)
- Settings UI: Hyperlinks to settings [\#2420](https://github.com/pypeclub/OpenPype/pull/2420)
- Modules: JobQueue module moved one hierarchy level higher [\#2419](https://github.com/pypeclub/OpenPype/pull/2419)
- TimersManager: Start timer post launch hook [\#2418](https://github.com/pypeclub/OpenPype/pull/2418)
- Ftrack: Check existence of object type on recreation [\#2404](https://github.com/pypeclub/OpenPype/pull/2404)
- Flame: moving `utility\_scripts` to api folder also with `scripts` [\#2385](https://github.com/pypeclub/OpenPype/pull/2385)
- Centos 7 dependency compatibility [\#2384](https://github.com/pypeclub/OpenPype/pull/2384)
- Enhancement: Settings: Use project settings values from another project [\#2382](https://github.com/pypeclub/OpenPype/pull/2382)
- Blender 3: Support auto install for new blender version [\#2377](https://github.com/pypeclub/OpenPype/pull/2377)
- Maya add render image path to settings [\#2375](https://github.com/pypeclub/OpenPype/pull/2375)
- Settings: Webpublisher in hosts enum [\#2367](https://github.com/pypeclub/OpenPype/pull/2367)
- Hiero: python3 compatibility [\#2365](https://github.com/pypeclub/OpenPype/pull/2365)
- Burnins: Be able recognize mxf OPAtom format [\#2361](https://github.com/pypeclub/OpenPype/pull/2361)
- Maya: Add is\_static\_image\_plane and is\_in\_all\_views option in imagePlaneLoader [\#2356](https://github.com/pypeclub/OpenPype/pull/2356)
- Local settings: Copyable studio paths [\#2349](https://github.com/pypeclub/OpenPype/pull/2349)
- General: Multilayer EXRs support [\#2315](https://github.com/pypeclub/OpenPype/pull/2315)
**🐛 Bug fixes**
- Fix - provider icons are pulled from a folder [\#2326](https://github.com/pypeclub/OpenPype/pull/2326)
- InputLinks: Typo in "inputLinks" key [\#2314](https://github.com/pypeclub/OpenPype/pull/2314)
- Deadline timeout and logging [\#2312](https://github.com/pypeclub/OpenPype/pull/2312)
- nuke: do not multiply representation on class method [\#2311](https://github.com/pypeclub/OpenPype/pull/2311)
- Workfiles tool: Fix task formatting [\#2306](https://github.com/pypeclub/OpenPype/pull/2306)
- Delivery: Fix delivery paths created on windows [\#2302](https://github.com/pypeclub/OpenPype/pull/2302)
- Maya: Deadline - fix limit groups [\#2295](https://github.com/pypeclub/OpenPype/pull/2295)
- New Publisher: Fix mapping of indexes [\#2285](https://github.com/pypeclub/OpenPype/pull/2285)
- Alternate site for site sync doesnt work for sequences [\#2284](https://github.com/pypeclub/OpenPype/pull/2284)
- FFmpeg: Execute ffprobe using list of arguments instead of string command [\#2281](https://github.com/pypeclub/OpenPype/pull/2281)
- Nuke: Anatomy fill data use task as dictionary [\#2278](https://github.com/pypeclub/OpenPype/pull/2278)
- Bug: fix variable name \_asset\_id in workfiles application [\#2274](https://github.com/pypeclub/OpenPype/pull/2274)
- Version handling fixes [\#2272](https://github.com/pypeclub/OpenPype/pull/2272)
- PS: Introduced settings for invalid characters to use in ValidateNaming plugin [\#2417](https://github.com/pypeclub/OpenPype/pull/2417)
- Settings UI: Breadcrumbs path does not create new entities [\#2416](https://github.com/pypeclub/OpenPype/pull/2416)
- AfterEffects: Variant 2022 is in defaults but missing in schemas [\#2412](https://github.com/pypeclub/OpenPype/pull/2412)
- Nuke: baking representations was not additive [\#2406](https://github.com/pypeclub/OpenPype/pull/2406)
- General: Fix access to environments from default settings [\#2403](https://github.com/pypeclub/OpenPype/pull/2403)
- Fix: Placeholder Input color set fix [\#2399](https://github.com/pypeclub/OpenPype/pull/2399)
- Settings: Fix state change of wrapper label [\#2396](https://github.com/pypeclub/OpenPype/pull/2396)
- Flame: fix ftrack publisher [\#2381](https://github.com/pypeclub/OpenPype/pull/2381)
- hiero: solve custom ocio path [\#2379](https://github.com/pypeclub/OpenPype/pull/2379)
- hiero: fix workio and flatten [\#2378](https://github.com/pypeclub/OpenPype/pull/2378)
- Nuke: fixing menu re-drawing during context change [\#2374](https://github.com/pypeclub/OpenPype/pull/2374)
- Webpublisher: Fix assignment of families of TVpaint instances [\#2373](https://github.com/pypeclub/OpenPype/pull/2373)
- Nuke: fixing node name based on switched asset name [\#2369](https://github.com/pypeclub/OpenPype/pull/2369)
- JobQueue: Fix loading of settings [\#2362](https://github.com/pypeclub/OpenPype/pull/2362)
- Tools: Placeholder color [\#2359](https://github.com/pypeclub/OpenPype/pull/2359)
- Launcher: Minimize button on MacOs [\#2355](https://github.com/pypeclub/OpenPype/pull/2355)
- StandalonePublisher: Fix import of constant [\#2354](https://github.com/pypeclub/OpenPype/pull/2354)
- Adobe products show issue [\#2347](https://github.com/pypeclub/OpenPype/pull/2347)
- Remove wrongly used host for hook [\#2342](https://github.com/pypeclub/OpenPype/pull/2342)
- Tools: Use Qt context on tools show [\#2340](https://github.com/pypeclub/OpenPype/pull/2340)
- Flame: Fix default argument value in custom dictionary [\#2339](https://github.com/pypeclub/OpenPype/pull/2339)
- Royal Render: Fix plugin order and OpenPype auto-detection [\#2291](https://github.com/pypeclub/OpenPype/pull/2291)
**Merged pull requests:**
- \[Fix\]\[MAYA\] Handle message type attribute within CollectLook [\#2394](https://github.com/pypeclub/OpenPype/pull/2394)
- Add validator to check correct version of extension for PS and AE [\#2387](https://github.com/pypeclub/OpenPype/pull/2387)
- Linux : flip updating submodules logic [\#2357](https://github.com/pypeclub/OpenPype/pull/2357)
- Update of avalon-core [\#2346](https://github.com/pypeclub/OpenPype/pull/2346)
## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23)
@ -64,28 +79,6 @@
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.2-nightly.2...3.6.2)
**🚀 Enhancements**
- Royal Render: Support for rr channels in separate dirs [\#2268](https://github.com/pypeclub/OpenPype/pull/2268)
- SceneInventory: Choose loader in asset switcher [\#2262](https://github.com/pypeclub/OpenPype/pull/2262)
- Style: New fonts in OpenPype style [\#2256](https://github.com/pypeclub/OpenPype/pull/2256)
- Tools: SceneInventory in OpenPype [\#2255](https://github.com/pypeclub/OpenPype/pull/2255)
- Tools: Tasks widget [\#2251](https://github.com/pypeclub/OpenPype/pull/2251)
- Added endpoint for configured extensions [\#2221](https://github.com/pypeclub/OpenPype/pull/2221)
**🐛 Bug fixes**
- Tools: Parenting of tools in Nuke and Hiero [\#2266](https://github.com/pypeclub/OpenPype/pull/2266)
- limiting validator to specific editorial hosts [\#2264](https://github.com/pypeclub/OpenPype/pull/2264)
- Tools: Select Context dialog attribute fix [\#2261](https://github.com/pypeclub/OpenPype/pull/2261)
- Maya: Render publishing fails on linux [\#2260](https://github.com/pypeclub/OpenPype/pull/2260)
- LookAssigner: Fix tool reopen [\#2259](https://github.com/pypeclub/OpenPype/pull/2259)
- Standalone: editorial not publishing thumbnails on all subsets [\#2258](https://github.com/pypeclub/OpenPype/pull/2258)
- Loader doesn't allow changing of version before loading [\#2254](https://github.com/pypeclub/OpenPype/pull/2254)
- Burnins: Support mxf metadata [\#2247](https://github.com/pypeclub/OpenPype/pull/2247)
- Maya: Support for configurable AOV separator characters [\#2197](https://github.com/pypeclub/OpenPype/pull/2197)
- Maya: texture colorspace modes in looks [\#2195](https://github.com/pypeclub/OpenPype/pull/2195)
## [3.6.1](https://github.com/pypeclub/OpenPype/tree/3.6.1) (2021-11-16)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.1-nightly.1...3.6.1)
@ -94,49 +87,6 @@
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.0-nightly.6...3.6.0)
### 📖 Documentation
- Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206)
- Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188)
**🚀 Enhancements**
- Tools: Creator in OpenPype [\#2244](https://github.com/pypeclub/OpenPype/pull/2244)
- Tools: Subset manager in OpenPype [\#2243](https://github.com/pypeclub/OpenPype/pull/2243)
- General: Skip module directories without init file [\#2239](https://github.com/pypeclub/OpenPype/pull/2239)
- General: Static interfaces [\#2238](https://github.com/pypeclub/OpenPype/pull/2238)
- Style: Fix transparent image in style [\#2235](https://github.com/pypeclub/OpenPype/pull/2235)
- Add a "following workfile versioning" option on publish [\#2225](https://github.com/pypeclub/OpenPype/pull/2225)
- Modules: Module can add cli commands [\#2224](https://github.com/pypeclub/OpenPype/pull/2224)
- Webpublisher: Separate webpublisher logic [\#2222](https://github.com/pypeclub/OpenPype/pull/2222)
- Add both side availability on Site Sync sites to Loader [\#2220](https://github.com/pypeclub/OpenPype/pull/2220)
- Tools: Center loader and library loader on show [\#2219](https://github.com/pypeclub/OpenPype/pull/2219)
- Maya : Validate shape zero [\#2212](https://github.com/pypeclub/OpenPype/pull/2212)
- Maya : validate unique names [\#2211](https://github.com/pypeclub/OpenPype/pull/2211)
- Tools: OpenPype stylesheet in workfiles tool [\#2208](https://github.com/pypeclub/OpenPype/pull/2208)
- Ftrack: Replace Queue with deque in event handlers logic [\#2204](https://github.com/pypeclub/OpenPype/pull/2204)
- Tools: New select context dialog [\#2200](https://github.com/pypeclub/OpenPype/pull/2200)
- Maya : Validate mesh ngons [\#2199](https://github.com/pypeclub/OpenPype/pull/2199)
- Dirmap in Nuke [\#2198](https://github.com/pypeclub/OpenPype/pull/2198)
- Delivery: Check 'frame' key in template for sequence delivery [\#2196](https://github.com/pypeclub/OpenPype/pull/2196)
- Settings: Site sync project settings improvement [\#2193](https://github.com/pypeclub/OpenPype/pull/2193)
- Usage of tools code [\#2185](https://github.com/pypeclub/OpenPype/pull/2185)
**🐛 Bug fixes**
- Ftrack: Sync project ftrack id cache issue [\#2250](https://github.com/pypeclub/OpenPype/pull/2250)
- Ftrack: Session creation and Prepare project [\#2245](https://github.com/pypeclub/OpenPype/pull/2245)
- Added queue for studio processing in PS [\#2237](https://github.com/pypeclub/OpenPype/pull/2237)
- Python 2: Unicode to string conversion [\#2236](https://github.com/pypeclub/OpenPype/pull/2236)
- Fix - enum for color coding in PS [\#2234](https://github.com/pypeclub/OpenPype/pull/2234)
- Pyblish Tool: Fix targets handling [\#2232](https://github.com/pypeclub/OpenPype/pull/2232)
- Ftrack: Base event fix of 'get\_project\_from\_entity' method [\#2214](https://github.com/pypeclub/OpenPype/pull/2214)
- Maya : multiple subsets review broken [\#2210](https://github.com/pypeclub/OpenPype/pull/2210)
- Fix - different command used for Linux and Mac OS [\#2207](https://github.com/pypeclub/OpenPype/pull/2207)
- Tools: Workfiles tool don't use avalon widgets [\#2205](https://github.com/pypeclub/OpenPype/pull/2205)
- Ftrack: Fill missing ftrack id on mongo project [\#2203](https://github.com/pypeclub/OpenPype/pull/2203)
- Project Manager: Fix copying of tasks [\#2191](https://github.com/pypeclub/OpenPype/pull/2191)
## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.5.0-nightly.8...3.5.0)

View file

@ -41,6 +41,8 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n
ncurses \
ncurses-devel \
qt5-qtbase-devel \
xcb-util-wm \
xcb-util-renderutil \
&& yum clean all
# we need to build our own patchelf
@ -92,7 +94,8 @@ RUN source $HOME/.bashrc \
RUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.7/lib \
&& cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.7/lib \
&& cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.7/lib \
&& cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.7/lib
&& cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.7/lib \
&& cp /usr/lib64/libxcb* ./build/exe.linux-x86_64-3.7/vendor/python/PySide2/Qt/lib
RUN cd /opt/openpype \
rm -rf ./vendor/bin

View file

@ -31,8 +31,6 @@ from .lib import (
)
from .lib.mongo import (
decompose_url,
compose_url,
get_default_components
)
@ -84,8 +82,6 @@ __all__ = [
"Anatomy",
"config",
"execute",
"decompose_url",
"compose_url",
"get_default_components",
"ApplicationManager",
"BuildWorkfile",

View file

@ -138,7 +138,10 @@ def webpublisherwebserver(debug, executable, upload_dir, host=None, port=None):
@click.option("--asset", help="Asset name", default=None)
@click.option("--task", help="Task name", default=None)
@click.option("--app", help="Application name", default=None)
def extractenvironments(output_json_path, project, asset, task, app):
@click.option(
"--envgroup", help="Environment group (e.g. \"farm\")", default=None
)
def extractenvironments(output_json_path, project, asset, task, app, envgroup):
"""Extract environment variables for entered context to a json file.
Entered output filepath will be created if does not exists.
@ -149,7 +152,7 @@ def extractenvironments(output_json_path, project, asset, task, app):
Context options are "project", "asset", "task", "app"
"""
PypeCommands.extractenvironments(
output_json_path, project, asset, task, app
output_json_path, project, asset, task, app, envgroup
)
@ -356,9 +359,22 @@ def run(script):
"--pyargs",
help="Run tests from package",
default=None)
def runtests(folder, mark, pyargs):
@click.option("-t",
"--test_data_folder",
help="Unzipped directory path of test file",
default=None)
@click.option("-s",
"--persist",
help="Persist test DB and published files after test end",
default=None)
@click.option("-a",
"--app_variant",
help="Provide specific app variant for test, empty for latest",
default=None)
def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant):
"""Run all automatic tests after proper initialization via start.py"""
PypeCommands().run_tests(folder, mark, pyargs)
PypeCommands().run_tests(folder, mark, pyargs, test_data_folder,
persist, app_variant)
@main.command()

View file

@ -13,7 +13,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook):
# Should be as last hook because must change launch arguments to string
order = 1000
app_groups = ["nuke", "nukex", "hiero", "nukestudio", "photoshop"]
app_groups = ["nuke", "nukex", "hiero", "nukestudio"]
platforms = ["windows"]
def execute(self):

View file

@ -48,7 +48,7 @@ class GlobalHostDataHook(PreLaunchHook):
"log": self.log
})
prepare_host_environments(temp_data)
prepare_host_environments(temp_data, self.launch_context.env_group)
prepare_context_environments(temp_data)
temp_data.pop("log")

View file

@ -49,7 +49,3 @@ class NonPythonHostHook(PreLaunchHook):
if remainders:
self.launch_context.launch_args.extend(remainders)
# This must be set otherwise it wouldn't be possible to catch output
# when build OpenPype is used.
self.launch_context.kwargs["stdout"] = subprocess.DEVNULL
self.launch_context.kwargs["stderr"] = subprocess.DEVNULL

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
"""Close AE after publish. For Webpublishing only."""
import pyblish.api
from avalon import aftereffects
class CloseAE(pyblish.api.ContextPlugin):
"""Close AE after publish. For Webpublishing only.
"""
order = pyblish.api.IntegratorOrder + 14
label = "Close AE"
optional = True
active = True
hosts = ["aftereffects"]
targets = ["remotepublish"]
def process(self, context):
self.log.info("CloseAE")
stub = aftereffects.stub()
self.log.info("Shutting down AE")
stub.save()
stub.close()
self.log.info("AE closed")

View file

@ -8,7 +8,7 @@ from avalon import aftereffects
class CollectCurrentFile(pyblish.api.ContextPlugin):
"""Inject the current working file into context"""
order = pyblish.api.CollectorOrder - 0.5
order = pyblish.api.CollectorOrder - 0.49
label = "Current File"
hosts = ["aftereffects"]

View file

@ -0,0 +1,56 @@
import os
import re
import pyblish.api
from avalon import aftereffects
class CollectExtensionVersion(pyblish.api.ContextPlugin):
""" Pulls and compares version of installed extension.
It is recommended to use same extension as in provided Openpype code.
Please use Anastasiys Extension Manager or ZXPInstaller to update
extension in case of an error.
You can locate extension.zxp in your installed Openpype code in
`repos/avalon-core/avalon/aftereffects`
"""
# This technically should be a validator, but other collectors might be
# impacted with usage of obsolete extension, so collector that runs first
# was chosen
order = pyblish.api.CollectorOrder - 0.5
label = "Collect extension version"
hosts = ["aftereffects"]
optional = True
active = True
def process(self, context):
installed_version = aftereffects.stub().get_extension_version()
if not installed_version:
raise ValueError("Unknown version, probably old extension")
manifest_url = os.path.join(os.path.dirname(aftereffects.__file__),
"extension", "CSXS", "manifest.xml")
if not os.path.exists(manifest_url):
self.log.debug("Unable to locate extension manifest, not checking")
return
expected_version = None
with open(manifest_url) as fp:
content = fp.read()
found = re.findall(r'(ExtensionBundleVersion=")([0-9\.]+)(")',
content)
if found:
expected_version = found[0][1]
if expected_version != installed_version:
msg = (
"Expected version '{}' found '{}'\n Please update"
" your installed extension, it might not work properly."
).format(expected_version, installed_version)
raise ValueError(msg)

View file

@ -19,10 +19,9 @@ class ExtractLocalRender(openpype.api.Extractor):
staging_dir = instance.data["stagingDir"]
self.log.info("staging_dir::{}".format(staging_dir))
stub.render(staging_dir)
# pull file name from Render Queue Output module
render_q = stub.get_render_info()
stub.render(staging_dir)
if not render_q:
raise ValueError("No file extension set in Render Queue")
_, ext = os.path.splitext(os.path.basename(render_q.file_name))

View file

@ -32,7 +32,7 @@ class InstallPySideToBlender(PreLaunchHook):
def inner_execute(self):
# Get blender's python directory
version_regex = re.compile(r"^2\.[0-9]{2}$")
version_regex = re.compile(r"^[2-3]\.[0-9]+$")
executable = self.launch_context.executable.executable_path
if os.path.basename(executable).lower() != "blender.exe":

View file

@ -47,10 +47,8 @@ class FlameAppFramework(object):
def setdefault(self, k, default=None):
return self.master[self.name].setdefault(k, default)
def pop(self, k, v=object()):
if v is object():
return self.master[self.name].pop(k)
return self.master[self.name].pop(k, v)
def pop(self, *args, **kwargs):
return self.master[self.name].pop(*args, **kwargs)
def update(self, mapping=(), **kwargs):
self.master[self.name].update(mapping, **kwargs)

View file

@ -0,0 +1,58 @@
<?xml version="1.0"?>
<preset version="9">
<type>sequence</type>
<comment>Creates a 8-bit Jpeg file per segment. </comment>
<sequence>
<fileType>NONE</fileType>
<namePattern></namePattern>
<composition>&lt;name&gt;</composition>
<includeVideo>True</includeVideo>
<exportVideo>True</exportVideo>
<videoMedia>
<mediaFileType>image</mediaFileType>
<commit>FX</commit>
<flatten>NoChange</flatten>
<exportHandles>False</exportHandles>
<nbHandles>10</nbHandles>
</videoMedia>
<includeAudio>True</includeAudio>
<exportAudio>False</exportAudio>
<audioMedia>
<mediaFileType>audio</mediaFileType>
<commit>FX</commit>
<flatten>FlattenTracks</flatten>
<exportHandles>True</exportHandles>
<nbHandles>10</nbHandles>
</audioMedia>
</sequence>
<video>
<fileType>Jpeg</fileType>
<codec>923688</codec>
<codecProfile></codecProfile>
<namePattern>&lt;segment name&gt;</namePattern>
<compressionQuality>100</compressionQuality>
<transferCharacteristic>2</transferCharacteristic>
<colorimetricSpecification>4</colorimetricSpecification>
<includeAlpha>False</includeAlpha>
<overwriteWithVersions>False</overwriteWithVersions>
<posterFrame>True</posterFrame>
<useFrameAsPoster>1</useFrameAsPoster>
<resize>
<resizeType>fit</resizeType>
<resizeFilter>lanczos</resizeFilter>
<width>1920</width>
<height>1080</height>
<bitsPerChannel>8</bitsPerChannel>
<numChannels>3</numChannels>
<floatingPoint>False</floatingPoint>
<bigEndian>True</bigEndian>
<pixelRatio>1</pixelRatio>
<scanFormat>P</scanFormat>
</resize>
</video>
<name>
<framePadding>4</framePadding>
<startFrame>1</startFrame>
<frameIndex>2</frameIndex>
</name>
</preset>

View file

@ -0,0 +1,72 @@
<?xml version="1.0"?>
<preset version="10">
<type>sequence</type>
<comment>Create MOV H264 files per segment with thumbnail</comment>
<sequence>
<fileType>NONE</fileType>
<namePattern></namePattern>
<composition>&lt;name&gt;</composition>
<includeVideo>True</includeVideo>
<exportVideo>True</exportVideo>
<videoMedia>
<mediaFileType>movie</mediaFileType>
<commit>FX</commit>
<flatten>FlattenTracks</flatten>
<exportHandles>True</exportHandles>
<nbHandles>5</nbHandles>
</videoMedia>
<includeAudio>True</includeAudio>
<exportAudio>False</exportAudio>
<audioMedia>
<mediaFileType>audio</mediaFileType>
<commit>Original</commit>
<flatten>NoChange</flatten>
<exportHandles>True</exportHandles>
<nbHandles>5</nbHandles>
</audioMedia>
</sequence>
<movie>
<fileType>QuickTime</fileType>
<namePattern>&lt;segment name&gt;</namePattern>
<yuvHeadroom>0</yuvHeadroom>
<yuvColourSpace>PCS_709</yuvColourSpace>
<operationalPattern>None</operationalPattern>
<companyName>Autodesk</companyName>
<productName>Flame</productName>
<versionName>2021</versionName>
</movie>
<video>
<fileType>QuickTime</fileType>
<codec>33622016</codec>
<codecProfile>
<rootPath>/opt/Autodesk/mediaconverter/</rootPath>
<targetVersion>2021</targetVersion>
<pathSuffix>/profiles/.33622016/HDTV_720p_8Mbits.cdxprof</pathSuffix>
</codecProfile>
<namePattern>&lt;segment name&gt;_&lt;video codec&gt;</namePattern>
<compressionQuality>50</compressionQuality>
<transferCharacteristic>2</transferCharacteristic>
<colorimetricSpecification>4</colorimetricSpecification>
<includeAlpha>False</includeAlpha>
<overwriteWithVersions>False</overwriteWithVersions>
<posterFrame>False</posterFrame>
<useFrameAsPoster>1</useFrameAsPoster>
<resize>
<resizeType>fit</resizeType>
<resizeFilter>gaussian</resizeFilter>
<width>1920</width>
<height>1080</height>
<bitsPerChannel>8</bitsPerChannel>
<numChannels>3</numChannels>
<floatingPoint>False</floatingPoint>
<bigEndian>True</bigEndian>
<pixelRatio>1</pixelRatio>
<scanFormat>P</scanFormat>
</resize>
</video>
<name>
<framePadding>4</framePadding>
<startFrame>1</startFrame>
<frameIndex>2</frameIndex>
</name>
</preset>

View file

@ -0,0 +1,162 @@
import os
import io
import ConfigParser as CP
from xml.etree import ElementTree as ET
from contextlib import contextmanager
PLUGIN_DIR = os.path.dirname(os.path.dirname(__file__))
EXPORT_PRESETS_DIR = os.path.join(PLUGIN_DIR, "export_preset")
CONFIG_DIR = os.path.join(os.path.expanduser(
"~/.openpype"), "openpype_flame_to_ftrack")
@contextmanager
def make_temp_dir():
import tempfile
try:
dirpath = tempfile.mkdtemp()
yield dirpath
except IOError as _error:
raise IOError("Not able to create temp dir file: {}".format(_error))
finally:
pass
@contextmanager
def get_config(section=None):
cfg_file_path = os.path.join(CONFIG_DIR, "settings.ini")
# create config dir
if not os.path.exists(CONFIG_DIR):
print("making dirs at: `{}`".format(CONFIG_DIR))
os.makedirs(CONFIG_DIR, mode=0o777)
# write default data to settings.ini
if not os.path.exists(cfg_file_path):
default_cfg = cfg_default()
config = CP.RawConfigParser()
config.readfp(io.BytesIO(default_cfg))
with open(cfg_file_path, 'wb') as cfg_file:
config.write(cfg_file)
try:
config = CP.RawConfigParser()
config.read(cfg_file_path)
if section:
_cfg_data = {
k: v
for s in config.sections()
for k, v in config.items(s)
if s == section
}
else:
_cfg_data = {s: dict(config.items(s)) for s in config.sections()}
yield _cfg_data
except IOError as _error:
raise IOError('Not able to read settings.ini file: {}'.format(_error))
finally:
pass
def set_config(cfg_data, section=None):
cfg_file_path = os.path.join(CONFIG_DIR, "settings.ini")
config = CP.RawConfigParser()
config.read(cfg_file_path)
try:
if not section:
for section in cfg_data:
for key, value in cfg_data[section].items():
config.set(section, key, value)
else:
for key, value in cfg_data.items():
config.set(section, key, value)
with open(cfg_file_path, 'wb') as cfg_file:
config.write(cfg_file)
except IOError as _error:
raise IOError('Not able to write settings.ini file: {}'.format(_error))
def cfg_default():
return """
[main]
workfile_start_frame = 1001
shot_handles = 0
shot_name_template = {sequence}_{shot}
hierarchy_template = shots[Folder]/{sequence}[Sequence]
create_task_type = Compositing
"""
def configure_preset(file_path, data):
split_fp = os.path.splitext(file_path)
new_file_path = split_fp[0] + "_tmp" + split_fp[-1]
with open(file_path, "r") as datafile:
tree = ET.parse(datafile)
for key, value in data.items():
for element in tree.findall(".//{}".format(key)):
print(element)
element.text = str(value)
tree.write(new_file_path)
return new_file_path
def export_thumbnail(sequence, tempdir_path, data):
import flame
export_preset = os.path.join(
EXPORT_PRESETS_DIR,
"openpype_seg_thumbnails_jpg.xml"
)
new_path = configure_preset(export_preset, data)
poster_frame_exporter = flame.PyExporter()
poster_frame_exporter.foreground = True
poster_frame_exporter.export(sequence, new_path, tempdir_path)
def export_video(sequence, tempdir_path, data):
import flame
export_preset = os.path.join(
EXPORT_PRESETS_DIR,
"openpype_seg_video_h264.xml"
)
new_path = configure_preset(export_preset, data)
poster_frame_exporter = flame.PyExporter()
poster_frame_exporter.foreground = True
poster_frame_exporter.export(sequence, new_path, tempdir_path)
def timecode_to_frames(timecode, framerate):
def _seconds(value):
if isinstance(value, str):
_zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':'))
return sum(f * float(t) for f, t in _zip_ft)
elif isinstance(value, (int, float)):
return value / framerate
return 0
def _frames(seconds):
return seconds * framerate
def tc_to_frames(_timecode, start=None):
return _frames(_seconds(_timecode) - _seconds(start))
if '+' in timecode:
timecode = timecode.replace('+', ':')
elif '#' in timecode:
timecode = timecode.replace('#', ':')
frames = int(round(tc_to_frames(timecode, start='00:00:00:00')))
return frames

View file

@ -0,0 +1,448 @@
import os
import sys
import six
import re
import json
import app_utils
# Fill following constants or set them via environment variable
FTRACK_MODULE_PATH = None
FTRACK_API_KEY = None
FTRACK_API_USER = None
FTRACK_SERVER = None
def import_ftrack_api():
try:
import ftrack_api
return ftrack_api
except ImportError:
import sys
ftrk_m_p = FTRACK_MODULE_PATH or os.getenv("FTRACK_MODULE_PATH")
sys.path.append(ftrk_m_p)
import ftrack_api
return ftrack_api
def get_ftrack_session():
import os
ftrack_api = import_ftrack_api()
# fill your own credentials
url = FTRACK_SERVER or os.getenv("FTRACK_SERVER") or ""
user = FTRACK_API_USER or os.getenv("FTRACK_API_USER") or ""
api = FTRACK_API_KEY or os.getenv("FTRACK_API_KEY") or ""
first_validation = True
if not user:
print('- Ftrack Username is not set')
first_validation = False
if not api:
print('- Ftrack API key is not set')
first_validation = False
if not first_validation:
return False
try:
return ftrack_api.Session(
server_url=url,
api_user=user,
api_key=api
)
except Exception as _e:
print("Can't log into Ftrack with used credentials: {}".format(_e))
ftrack_cred = {
'Ftrack server': str(url),
'Username': str(user),
'API key': str(api),
}
item_lens = [len(key) + 1 for key in ftrack_cred]
justify_len = max(*item_lens)
for key, value in ftrack_cred.items():
print('{} {}'.format((key + ':').ljust(justify_len, ' '), value))
return False
def get_project_task_types(project_entity):
tasks = {}
proj_template = project_entity['project_schema']
temp_task_types = proj_template['_task_type_schema']['types']
for type in temp_task_types:
if type['name'] not in tasks:
tasks[type['name']] = type
return tasks
class FtrackComponentCreator:
default_location = "ftrack.server"
ftrack_locations = {}
thumbnails = []
videos = []
temp_dir = None
def __init__(self, session):
self.session = session
self._get_ftrack_location()
def generate_temp_data(self, selection, change_preset_data):
with app_utils.make_temp_dir() as tempdir_path:
for seq in selection:
app_utils.export_thumbnail(
seq, tempdir_path, change_preset_data)
app_utils.export_video(seq, tempdir_path, change_preset_data)
return tempdir_path
def collect_generated_data(self, tempdir_path):
temp_files = os.listdir(tempdir_path)
self.thumbnails = [f for f in temp_files if "jpg" in f]
self.videos = [f for f in temp_files if "mov" in f]
self.temp_dir = tempdir_path
def get_thumb_path(self, shot_name):
# get component files
thumb_f = next((f for f in self.thumbnails if shot_name in f), None)
return os.path.join(self.temp_dir, thumb_f)
def get_video_path(self, shot_name):
# get component files
video_f = next((f for f in self.videos if shot_name in f), None)
return os.path.join(self.temp_dir, video_f)
def close(self):
self.ftrack_locations = {}
self.session = None
def create_comonent(self, shot_entity, data, assetversion_entity=None):
self.shot_entity = shot_entity
location = self._get_ftrack_location()
file_path = data["file_path"]
# get extension
file = os.path.basename(file_path)
_n, ext = os.path.splitext(file)
name = "ftrackreview-mp4" if "mov" in ext else "thumbnail"
component_data = {
"name": name,
"file_path": file_path,
"file_type": ext,
"location": location
}
if name == "ftrackreview-mp4":
duration = data["duration"]
handles = data["handles"]
fps = data["fps"]
component_data["metadata"] = {
'ftr_meta': json.dumps({
'frameIn': int(0),
'frameOut': int(duration + (handles * 2)),
'frameRate': float(fps)
})
}
if not assetversion_entity:
# get assettype entity from session
assettype_entity = self._get_assettype({"short": "reference"})
# get or create asset entity from session
asset_entity = self._get_asset({
"name": "plateReference",
"type": assettype_entity,
"parent": self.shot_entity
})
# get or create assetversion entity from session
assetversion_entity = self._get_assetversion({
"version": 0,
"asset": asset_entity
})
# get or create component entity
self._set_component(component_data, {
"name": name,
"version": assetversion_entity,
})
return assetversion_entity
def _overwrite_members(self, entity, data):
origin_location = self._get_ftrack_location("ftrack.origin")
location = data.pop("location")
self._remove_component_from_location(entity, location)
entity["file_type"] = data["file_type"]
try:
origin_location.add_component(
entity, data["file_path"]
)
# Add components to location.
location.add_component(
entity, origin_location, recursive=True)
except Exception as __e:
print("Error: {}".format(__e))
self._remove_component_from_location(entity, origin_location)
origin_location.add_component(
entity, data["file_path"]
)
# Add components to location.
location.add_component(
entity, origin_location, recursive=True)
def _remove_component_from_location(self, entity, location):
print(location)
# Removing existing members from location
components = list(entity.get("members", []))
components += [entity]
for component in components:
for loc in component.get("component_locations", []):
if location["id"] == loc["location_id"]:
print("<< Removing component: {}".format(component))
location.remove_component(
component, recursive=False
)
# Deleting existing members on component entity
for member in entity.get("members", []):
self.session.delete(member)
print("<< Deleting member: {}".format(member))
del(member)
self._commit()
# Reset members in memory
if "members" in entity.keys():
entity["members"] = []
def _get_assettype(self, data):
return self.session.query(
self._query("AssetType", data)).first()
def _set_component(self, comp_data, base_data):
component_metadata = comp_data.pop("metadata", {})
component_entity = self.session.query(
self._query("Component", base_data)
).first()
if component_entity:
# overwrite existing members in component enity
# - get data for member from `ftrack.origin` location
self._overwrite_members(component_entity, comp_data)
# Adding metadata
existing_component_metadata = component_entity["metadata"]
existing_component_metadata.update(component_metadata)
component_entity["metadata"] = existing_component_metadata
return
assetversion_entity = base_data["version"]
location = comp_data.pop("location")
component_entity = assetversion_entity.create_component(
comp_data["file_path"],
data=comp_data,
location=location
)
# Adding metadata
existing_component_metadata = component_entity["metadata"]
existing_component_metadata.update(component_metadata)
component_entity["metadata"] = existing_component_metadata
if comp_data["name"] == "thumbnail":
self.shot_entity["thumbnail_id"] = component_entity["id"]
assetversion_entity["thumbnail_id"] = component_entity["id"]
self._commit()
def _get_asset(self, data):
# first find already created
asset_entity = self.session.query(
self._query("Asset", data)
).first()
if asset_entity:
return asset_entity
asset_entity = self.session.create("Asset", data)
# _commit if created
self._commit()
return asset_entity
def _get_assetversion(self, data):
assetversion_entity = self.session.query(
self._query("AssetVersion", data)
).first()
if assetversion_entity:
return assetversion_entity
assetversion_entity = self.session.create("AssetVersion", data)
# _commit if created
self._commit()
return assetversion_entity
def _commit(self):
try:
self.session.commit()
except Exception:
tp, value, tb = sys.exc_info()
# self.session.rollback()
# self.session._configure_locations()
six.reraise(tp, value, tb)
def _get_ftrack_location(self, name=None):
name = name or self.default_location
if name in self.ftrack_locations:
return self.ftrack_locations[name]
location = self.session.query(
'Location where name is "{}"'.format(name)
).one()
self.ftrack_locations[name] = location
return location
def _query(self, entitytype, data):
""" Generate a query expression from data supplied.
If a value is not a string, we'll add the id of the entity to the
query.
Args:
entitytype (str): The type of entity to query.
data (dict): The data to identify the entity.
exclusions (list): All keys to exclude from the query.
Returns:
str: String query to use with "session.query"
"""
queries = []
if sys.version_info[0] < 3:
for key, value in data.items():
if not isinstance(value, (str, int)):
print("value: {}".format(value))
if "id" in value.keys():
queries.append(
"{0}.id is \"{1}\"".format(key, value["id"])
)
else:
queries.append("{0} is \"{1}\"".format(key, value))
else:
for key, value in data.items():
if not isinstance(value, (str, int)):
print("value: {}".format(value))
if "id" in value.keys():
queries.append(
"{0}.id is \"{1}\"".format(key, value["id"])
)
else:
queries.append("{0} is \"{1}\"".format(key, value))
query = (
"select id from " + entitytype + " where " + " and ".join(queries)
)
print(query)
return query
class FtrackEntityOperator:
def __init__(self, session, project_entity):
self.session = session
self.project_entity = project_entity
def commit(self):
try:
self.session.commit()
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
self.session._configure_locations()
six.reraise(tp, value, tb)
def create_ftrack_entity(self, session, type, name, parent=None):
parent = parent or self.project_entity
entity = session.create(type, {
'name': name,
'parent': parent
})
try:
session.commit()
except Exception:
tp, value, tb = sys.exc_info()
session.rollback()
session._configure_locations()
six.reraise(tp, value, tb)
return entity
def get_ftrack_entity(self, session, type, name, parent):
query = '{} where name is "{}" and project_id is "{}"'.format(
type, name, self.project_entity["id"])
try:
entity = session.query(query).one()
except Exception:
entity = None
# if entity doesnt exist then create one
if not entity:
entity = self.create_ftrack_entity(
session,
type,
name,
parent
)
return entity
def create_parents(self, template):
parents = []
t_split = template.split("/")
replace_patern = re.compile(r"(\[.*\])")
type_patern = re.compile(r"\[(.*)\]")
for t_s in t_split:
match_type = type_patern.findall(t_s)
if not match_type:
raise Exception((
"Missing correct type flag in : {}"
"/n Example: name[Type]").format(
t_s)
)
new_name = re.sub(replace_patern, "", t_s)
f_type = match_type.pop()
parents.append((new_name, f_type))
return parents
def create_task(self, task_type, task_types, parent):
existing_task = [
child for child in parent['children']
if child.entity_type.lower() == 'task'
if child['name'].lower() in task_type.lower()
]
if existing_task:
return existing_task.pop()
task = self.session.create('Task', {
"name": task_type.lower(),
"parent": parent
})
task["type"] = task_types[task_type]
return task

View file

@ -0,0 +1,524 @@
from PySide2 import QtWidgets, QtCore
import uiwidgets
import app_utils
import ftrack_lib
def clear_inner_modules():
import sys
if "ftrack_lib" in sys.modules.keys():
del sys.modules["ftrack_lib"]
print("Ftrack Lib module removed from sys.modules")
if "app_utils" in sys.modules.keys():
del sys.modules["app_utils"]
print("app_utils module removed from sys.modules")
if "uiwidgets" in sys.modules.keys():
del sys.modules["uiwidgets"]
print("uiwidgets module removed from sys.modules")
class MainWindow(QtWidgets.QWidget):
def __init__(self, klass, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.panel_class = klass
def closeEvent(self, event):
# clear all temp data
print("Removing temp data")
self.panel_class.clear_temp_data()
self.panel_class.close()
clear_inner_modules()
# now the panel can be closed
event.accept()
class FlameToFtrackPanel(object):
session = None
temp_data_dir = None
processed_components = []
project_entity = None
task_types = {}
all_task_types = {}
# TreeWidget
columns = {
"Sequence name": {
"columnWidth": 200,
"order": 0
},
"Shot name": {
"columnWidth": 200,
"order": 1
},
"Clip duration": {
"columnWidth": 100,
"order": 2
},
"Shot description": {
"columnWidth": 500,
"order": 3
},
"Task description": {
"columnWidth": 500,
"order": 4
},
}
def __init__(self, selection):
print(selection)
self.session = ftrack_lib.get_ftrack_session()
self.selection = selection
self.window = MainWindow(self)
# creating ui
self.window.setMinimumSize(1500, 600)
self.window.setWindowTitle('Sequence Shots to Ftrack')
self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.window.setFocusPolicy(QtCore.Qt.StrongFocus)
self.window.setStyleSheet('background-color: #313131')
self._create_project_widget()
self._create_tree_widget()
self._set_sequence_params()
self._generate_widgets()
self._generate_layouts()
self._timeline_info()
self._fix_resolution()
self.window.show()
def _generate_widgets(self):
with app_utils.get_config("main") as cfg_data:
cfg_d = cfg_data
self._create_task_type_widget(cfg_d)
# input fields
self.shot_name_label = uiwidgets.FlameLabel(
'Shot name template', 'normal', self.window)
self.shot_name_template_input = uiwidgets.FlameLineEdit(
cfg_d["shot_name_template"], self.window)
self.hierarchy_label = uiwidgets.FlameLabel(
'Parents template', 'normal', self.window)
self.hierarchy_template_input = uiwidgets.FlameLineEdit(
cfg_d["hierarchy_template"], self.window)
self.start_frame_label = uiwidgets.FlameLabel(
'Workfile start frame', 'normal', self.window)
self.start_frame_input = uiwidgets.FlameLineEdit(
cfg_d["workfile_start_frame"], self.window)
self.handles_label = uiwidgets.FlameLabel(
'Shot handles', 'normal', self.window)
self.handles_input = uiwidgets.FlameLineEdit(
cfg_d["shot_handles"], self.window)
self.width_label = uiwidgets.FlameLabel(
'Sequence width', 'normal', self.window)
self.width_input = uiwidgets.FlameLineEdit(
str(self.seq_width), self.window)
self.height_label = uiwidgets.FlameLabel(
'Sequence height', 'normal', self.window)
self.height_input = uiwidgets.FlameLineEdit(
str(self.seq_height), self.window)
self.pixel_aspect_label = uiwidgets.FlameLabel(
'Pixel aspect ratio', 'normal', self.window)
self.pixel_aspect_input = uiwidgets.FlameLineEdit(
str(1.00), self.window)
self.fps_label = uiwidgets.FlameLabel(
'Frame rate', 'normal', self.window)
self.fps_input = uiwidgets.FlameLineEdit(
str(self.fps), self.window)
# Button
self.select_all_btn = uiwidgets.FlameButton(
'Select All', self.select_all, self.window)
self.remove_temp_data_btn = uiwidgets.FlameButton(
'Remove temp data', self.clear_temp_data, self.window)
self.ftrack_send_btn = uiwidgets.FlameButton(
'Send to Ftrack', self._send_to_ftrack, self.window)
def _generate_layouts(self):
# left props
v_shift = 0
prop_layout_l = QtWidgets.QGridLayout()
prop_layout_l.setHorizontalSpacing(30)
if self.project_selector_enabled:
prop_layout_l.addWidget(self.project_select_label, v_shift, 0)
prop_layout_l.addWidget(self.project_select_input, v_shift, 1)
v_shift += 1
prop_layout_l.addWidget(self.shot_name_label, (v_shift + 0), 0)
prop_layout_l.addWidget(
self.shot_name_template_input, (v_shift + 0), 1)
prop_layout_l.addWidget(self.hierarchy_label, (v_shift + 1), 0)
prop_layout_l.addWidget(
self.hierarchy_template_input, (v_shift + 1), 1)
prop_layout_l.addWidget(self.start_frame_label, (v_shift + 2), 0)
prop_layout_l.addWidget(self.start_frame_input, (v_shift + 2), 1)
prop_layout_l.addWidget(self.handles_label, (v_shift + 3), 0)
prop_layout_l.addWidget(self.handles_input, (v_shift + 3), 1)
prop_layout_l.addWidget(self.task_type_label, (v_shift + 4), 0)
prop_layout_l.addWidget(
self.task_type_input, (v_shift + 4), 1)
# right props
prop_widget_r = QtWidgets.QWidget(self.window)
prop_layout_r = QtWidgets.QGridLayout(prop_widget_r)
prop_layout_r.setHorizontalSpacing(30)
prop_layout_r.setAlignment(
QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
prop_layout_r.setContentsMargins(0, 0, 0, 0)
prop_layout_r.addWidget(self.width_label, 1, 0)
prop_layout_r.addWidget(self.width_input, 1, 1)
prop_layout_r.addWidget(self.height_label, 2, 0)
prop_layout_r.addWidget(self.height_input, 2, 1)
prop_layout_r.addWidget(self.pixel_aspect_label, 3, 0)
prop_layout_r.addWidget(self.pixel_aspect_input, 3, 1)
prop_layout_r.addWidget(self.fps_label, 4, 0)
prop_layout_r.addWidget(self.fps_input, 4, 1)
# prop layout
prop_main_layout = QtWidgets.QHBoxLayout()
prop_main_layout.addLayout(prop_layout_l, 1)
prop_main_layout.addSpacing(20)
prop_main_layout.addWidget(prop_widget_r, 1)
# buttons layout
hbox = QtWidgets.QHBoxLayout()
hbox.addWidget(self.remove_temp_data_btn)
hbox.addWidget(self.select_all_btn)
hbox.addWidget(self.ftrack_send_btn)
# put all layouts together
main_frame = QtWidgets.QVBoxLayout(self.window)
main_frame.setMargin(20)
main_frame.addLayout(prop_main_layout)
main_frame.addWidget(self.tree)
main_frame.addLayout(hbox)
def _set_sequence_params(self):
for select in self.selection:
self.seq_height = select.height
self.seq_width = select.width
self.fps = float(str(select.frame_rate)[:-4])
break
def _create_task_type_widget(self, cfg_d):
print(self.project_entity)
self.task_types = ftrack_lib.get_project_task_types(
self.project_entity)
self.task_type_label = uiwidgets.FlameLabel(
'Create Task (type)', 'normal', self.window)
self.task_type_input = uiwidgets.FlamePushButtonMenu(
cfg_d["create_task_type"], self.task_types.keys(), self.window)
def _create_project_widget(self):
import flame
# get project name from flame current project
self.project_name = flame.project.current_project.name
# get project from ftrack -
# ftrack project name has to be the same as flame project!
query = 'Project where full_name is "{}"'.format(self.project_name)
# globally used variables
self.project_entity = self.session.query(query).first()
self.project_selector_enabled = bool(not self.project_entity)
if self.project_selector_enabled:
self.all_projects = self.session.query(
"Project where status is active").all()
self.project_entity = self.all_projects[0]
project_names = [p["full_name"] for p in self.all_projects]
self.all_task_types = {
p["full_name"]: ftrack_lib.get_project_task_types(p).keys()
for p in self.all_projects
}
self.project_select_label = uiwidgets.FlameLabel(
'Select Ftrack project', 'normal', self.window)
self.project_select_input = uiwidgets.FlamePushButtonMenu(
self.project_entity["full_name"], project_names, self.window)
self.project_select_input.selection_changed.connect(
self._on_project_changed)
def _create_tree_widget(self):
ordered_column_labels = self.columns.keys()
for _name, _value in self.columns.items():
ordered_column_labels.pop(_value["order"])
ordered_column_labels.insert(_value["order"], _name)
self.tree = uiwidgets.FlameTreeWidget(
ordered_column_labels, self.window)
# Allow multiple items in tree to be selected
self.tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
# Set tree column width
for _name, _val in self.columns.items():
self.tree.setColumnWidth(
_val["order"],
_val["columnWidth"]
)
# Prevent weird characters when shrinking tree columns
self.tree.setTextElideMode(QtCore.Qt.ElideNone)
def _resolve_project_entity(self):
if self.project_selector_enabled:
selected_project_name = self.project_select_input.text()
self.project_entity = next(
(p for p in self.all_projects
if p["full_name"] in selected_project_name),
None
)
def _save_ui_state_to_cfg(self):
_cfg_data_back = {
"shot_name_template": self.shot_name_template_input.text(),
"workfile_start_frame": self.start_frame_input.text(),
"shot_handles": self.handles_input.text(),
"hierarchy_template": self.hierarchy_template_input.text(),
"create_task_type": self.task_type_input.text()
}
# add cfg data back to settings.ini
app_utils.set_config(_cfg_data_back, "main")
def _send_to_ftrack(self):
# resolve active project and add it to self.project_entity
self._resolve_project_entity()
self._save_ui_state_to_cfg()
# get hanldes from gui input
handles = self.handles_input.text()
# get frame start from gui input
frame_start = int(self.start_frame_input.text())
# get task type from gui input
task_type = self.task_type_input.text()
# get resolution from gui inputs
fps = self.fps_input.text()
entity_operator = ftrack_lib.FtrackEntityOperator(
self.session, self.project_entity)
component_creator = ftrack_lib.FtrackComponentCreator(self.session)
if not self.temp_data_dir:
self.window.hide()
self.temp_data_dir = component_creator.generate_temp_data(
self.selection,
{
"nbHandles": handles
}
)
self.window.show()
# collect generated files to list data for farther use
component_creator.collect_generated_data(self.temp_data_dir)
# Get all selected items from treewidget
for item in self.tree.selectedItems():
# frame ranges
frame_duration = int(item.text(2))
frame_end = frame_start + frame_duration
# description
shot_description = item.text(3)
task_description = item.text(4)
# other
sequence_name = item.text(0)
shot_name = item.text(1)
thumb_fp = component_creator.get_thumb_path(shot_name)
video_fp = component_creator.get_video_path(shot_name)
print("processed comps: {}".format(self.processed_components))
print("processed thumb_fp: {}".format(thumb_fp))
processed = False
if thumb_fp not in self.processed_components:
self.processed_components.append(thumb_fp)
else:
processed = True
print("processed: {}".format(processed))
# populate full shot info
shot_attributes = {
"sequence": sequence_name,
"shot": shot_name,
"task": task_type
}
# format shot name template
_shot_name = self.shot_name_template_input.text().format(
**shot_attributes)
# format hierarchy template
_hierarchy_text = self.hierarchy_template_input.text().format(
**shot_attributes)
print(_hierarchy_text)
# solve parents
parents = entity_operator.create_parents(_hierarchy_text)
print(parents)
# obtain shot parents entities
_parent = None
for _name, _type in parents:
p_entity = entity_operator.get_ftrack_entity(
self.session,
_type,
_name,
_parent
)
print(p_entity)
_parent = p_entity
# obtain shot ftrack entity
f_s_entity = entity_operator.get_ftrack_entity(
self.session,
"Shot",
_shot_name,
_parent
)
print("Shot entity is: {}".format(f_s_entity))
if not processed:
# first create thumbnail and get version entity
assetversion_entity = component_creator.create_comonent(
f_s_entity, {
"file_path": thumb_fp
}
)
# secondly add video to version entity
component_creator.create_comonent(
f_s_entity, {
"file_path": video_fp,
"duration": frame_duration,
"handles": int(handles),
"fps": float(fps)
}, assetversion_entity
)
# create custom attributtes
custom_attrs = {
"frameStart": frame_start,
"frameEnd": frame_end,
"handleStart": int(handles),
"handleEnd": int(handles),
"resolutionWidth": int(self.width_input.text()),
"resolutionHeight": int(self.height_input.text()),
"pixelAspect": float(self.pixel_aspect_input.text()),
"fps": float(fps)
}
# update custom attributes on shot entity
for key in custom_attrs:
f_s_entity['custom_attributes'][key] = custom_attrs[key]
task_entity = entity_operator.create_task(
task_type, self.task_types, f_s_entity)
# Create notes.
user = self.session.query(
"User where username is \"{}\"".format(self.session.api_user)
).first()
f_s_entity.create_note(shot_description, author=user)
if task_description:
task_entity.create_note(task_description, user)
entity_operator.commit()
component_creator.close()
def _fix_resolution(self):
# Center window in linux
resolution = QtWidgets.QDesktopWidget().screenGeometry()
self.window.move(
(resolution.width() / 2) - (self.window.frameSize().width() / 2),
(resolution.height() / 2) - (self.window.frameSize().height() / 2))
def _on_project_changed(self):
task_types = self.all_task_types[self.project_name]
self.task_type_input.set_menu_options(task_types)
def _timeline_info(self):
# identificar as informacoes dos segmentos na timeline
for sequence in self.selection:
frame_rate = float(str(sequence.frame_rate)[:-4])
for ver in sequence.versions:
for tracks in ver.tracks:
for segment in tracks.segments:
print(segment.attributes)
if str(segment.name)[1:-1] == "":
continue
# get clip frame duration
record_duration = str(segment.record_duration)[1:-1]
clip_duration = app_utils.timecode_to_frames(
record_duration, frame_rate)
# populate shot source metadata
shot_description = ""
for attr in ["tape_name", "source_name", "head",
"tail", "file_path"]:
if not hasattr(segment, attr):
continue
_value = getattr(segment, attr)
_label = attr.replace("_", " ").capitalize()
row = "{}: {}\n".format(_label, _value)
shot_description += row
# Add timeline segment to tree
QtWidgets.QTreeWidgetItem(self.tree, [
str(sequence.name)[1:-1], # seq
str(segment.name)[1:-1], # shot
str(clip_duration), # clip duration
shot_description, # shot description
str(segment.comment)[1:-1] # task description
]).setFlags(
QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable
)
# Select top item in tree
self.tree.setCurrentItem(self.tree.topLevelItem(0))
def select_all(self, ):
self.tree.selectAll()
def clear_temp_data(self):
import shutil
self.processed_components = []
if self.temp_data_dir:
shutil.rmtree(self.temp_data_dir)
self.temp_data_dir = None
print("All Temp data were destroied ...")
def close(self):
self._save_ui_state_to_cfg()
self.session.close()

View file

@ -0,0 +1,212 @@
from PySide2 import QtWidgets, QtCore
class FlameLabel(QtWidgets.QLabel):
"""
Custom Qt Flame Label Widget
For different label looks set label_type as:
'normal', 'background', or 'outline'
To use:
label = FlameLabel('Label Name', 'normal', window)
"""
def __init__(self, label_name, label_type, parent_window, *args, **kwargs):
super(FlameLabel, self).__init__(*args, **kwargs)
self.setText(label_name)
self.setParent(parent_window)
self.setMinimumSize(130, 28)
self.setMaximumHeight(28)
self.setFocusPolicy(QtCore.Qt.NoFocus)
# Set label stylesheet based on label_type
if label_type == 'normal':
self.setStyleSheet(
'QLabel {color: #9a9a9a; border-bottom: 1px inset #282828; font: 14px "Discreet"}' # noqa
'QLabel:disabled {color: #6a6a6a}'
)
elif label_type == 'background':
self.setAlignment(QtCore.Qt.AlignCenter)
self.setStyleSheet(
'color: #9a9a9a; background-color: #393939; font: 14px "Discreet"' # noqa
)
elif label_type == 'outline':
self.setAlignment(QtCore.Qt.AlignCenter)
self.setStyleSheet(
'color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"' # noqa
)
class FlameLineEdit(QtWidgets.QLineEdit):
"""
Custom Qt Flame Line Edit Widget
Main window should include this:
window.setFocusPolicy(QtCore.Qt.StrongFocus)
To use:
line_edit = FlameLineEdit('Some text here', window)
"""
def __init__(self, text, parent_window, *args, **kwargs):
super(FlameLineEdit, self).__init__(*args, **kwargs)
self.setText(text)
self.setParent(parent_window)
self.setMinimumHeight(28)
self.setMinimumWidth(110)
self.setStyleSheet(
'QLineEdit {color: #9a9a9a; background-color: #373e47; selection-color: #262626; selection-background-color: #b8b1a7; font: 14px "Discreet"}' # noqa
'QLineEdit:focus {background-color: #474e58}' # noqa
'QLineEdit:disabled {color: #6a6a6a; background-color: #373737}'
)
class FlameTreeWidget(QtWidgets.QTreeWidget):
"""
Custom Qt Flame Tree Widget
To use:
tree_headers = ['Header1', 'Header2', 'Header3', 'Header4']
tree = FlameTreeWidget(tree_headers, window)
"""
def __init__(self, tree_headers, parent_window, *args, **kwargs):
super(FlameTreeWidget, self).__init__(*args, **kwargs)
self.setMinimumWidth(1000)
self.setMinimumHeight(300)
self.setSortingEnabled(True)
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.setAlternatingRowColors(True)
self.setFocusPolicy(QtCore.Qt.NoFocus)
self.setStyleSheet(
'QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px "Discreet"}' # noqa
'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}' # noqa
'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}' # noqa
'QTreeWidget::item:selected {selection-background-color: #111111}'
'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' # noqa
'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}'
)
self.verticalScrollBar().setStyleSheet('color: #818181')
self.horizontalScrollBar().setStyleSheet('color: #818181')
self.setHeaderLabels(tree_headers)
class FlameButton(QtWidgets.QPushButton):
"""
Custom Qt Flame Button Widget
To use:
button = FlameButton('Button Name', do_this_when_pressed, window)
"""
def __init__(self, button_name, do_when_pressed, parent_window,
*args, **kwargs):
super(FlameButton, self).__init__(*args, **kwargs)
self.setText(button_name)
self.setParent(parent_window)
self.setMinimumSize(QtCore.QSize(110, 28))
self.setMaximumSize(QtCore.QSize(110, 28))
self.setFocusPolicy(QtCore.Qt.NoFocus)
self.clicked.connect(do_when_pressed)
self.setStyleSheet(
'QPushButton {color: #9a9a9a; background-color: #424142; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' # noqa
'QPushButton:pressed {color: #d9d9d9; background-color: #4f4f4f; border-top: 1px inset #666666; font: italic}' # noqa
'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}' # noqa
)
class FlamePushButton(QtWidgets.QPushButton):
"""
Custom Qt Flame Push Button Widget
To use:
pushbutton = FlamePushButton(' Button Name', True_or_False, window)
"""
def __init__(self, button_name, button_checked, parent_window,
*args, **kwargs):
super(FlamePushButton, self).__init__(*args, **kwargs)
self.setText(button_name)
self.setParent(parent_window)
self.setCheckable(True)
self.setChecked(button_checked)
self.setMinimumSize(155, 28)
self.setMaximumSize(155, 28)
self.setFocusPolicy(QtCore.Qt.NoFocus)
self.setStyleSheet(
'QPushButton {color: #9a9a9a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #424142, stop: .94 #2e3b48); text-align: left; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' # noqa
'QPushButton:checked {color: #d9d9d9; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #4f4f4f, stop: .94 #5a7fb4); font: italic; border: 1px inset black; border-bottom: 1px inset #404040; border-right: 1px inset #404040}' # noqa
'QPushButton:disabled {color: #6a6a6a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #383838, stop: .94 #353535); font: light; border-top: 1px solid #575757; border-bottom: 1px solid #242424; border-right: 1px solid #353535; border-left: 1px solid #353535}' # noqa
'QToolTip {color: black; background-color: #ffffde; border: black solid 1px}' # noqa
)
class FlamePushButtonMenu(QtWidgets.QPushButton):
"""
Custom Qt Flame Menu Push Button Widget
To use:
push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4']
menu_push_button = FlamePushButtonMenu('push_button_name',
push_button_menu_options, window)
or
push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4']
menu_push_button = FlamePushButtonMenu(push_button_menu_options[0],
push_button_menu_options, window)
"""
selection_changed = QtCore.Signal(str)
def __init__(self, button_name, menu_options, parent_window,
*args, **kwargs):
super(FlamePushButtonMenu, self).__init__(*args, **kwargs)
self.setParent(parent_window)
self.setMinimumHeight(28)
self.setMinimumWidth(110)
self.setFocusPolicy(QtCore.Qt.NoFocus)
self.setStyleSheet(
'QPushButton {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' # noqa
'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}' # noqa
)
pushbutton_menu = QtWidgets.QMenu(parent_window)
pushbutton_menu.setFocusPolicy(QtCore.Qt.NoFocus)
pushbutton_menu.setStyleSheet(
'QMenu {color: #9a9a9a; background-color:#24303d; font: 14px "Discreet"}' # noqa
'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}'
)
self._pushbutton_menu = pushbutton_menu
self.setMenu(pushbutton_menu)
self.set_menu_options(menu_options, button_name)
def set_menu_options(self, menu_options, current_option=None):
self._pushbutton_menu.clear()
current_option = current_option or menu_options[0]
for option in menu_options:
action = self._pushbutton_menu.addAction(option)
action.triggered.connect(self._on_action_trigger)
if current_option is not None:
self.setText(current_option)
def _on_action_trigger(self):
action = self.sender()
self.setText(action.text())
self.selection_changed.emit(action.text())

View file

@ -0,0 +1,38 @@
from __future__ import print_function
import os
import sys
SCRIPT_DIR = os.path.dirname(__file__)
PACKAGE_DIR = os.path.join(SCRIPT_DIR, "modules")
sys.path.append(PACKAGE_DIR)
def flame_panel_executor(selection):
if "panel_app" in sys.modules.keys():
print("panel_app module is already loaded")
del sys.modules["panel_app"]
print("panel_app module removed from sys.modules")
import panel_app
panel_app.FlameToFtrackPanel(selection)
def scope_sequence(selection):
import flame
return any(isinstance(item, flame.PySequence) for item in selection)
def get_media_panel_custom_ui_actions():
return [
{
"name": "OpenPype: Ftrack",
"actions": [
{
"name": "Create Shots",
"isVisible": scope_sequence,
"execute": flame_panel_executor
}
]
}
]

View file

@ -27,6 +27,7 @@ def _sync_utility_scripts(env=None):
fsd_paths = [os.path.join(
HOST_DIR,
"api",
"utility_scripts"
)]

View file

@ -2,6 +2,7 @@ import os
import json
import tempfile
import contextlib
import socket
from openpype.lib import (
PreLaunchHook, get_openpype_username)
from openpype.hosts import flame as opflame
@ -21,7 +22,7 @@ class FlamePrelaunch(PreLaunchHook):
flame_python_exe = "/opt/Autodesk/python/2021/bin/python2.7"
wtc_script_path = os.path.join(
opflame.HOST_DIR, "scripts", "wiretap_com.py")
opflame.HOST_DIR, "api", "scripts", "wiretap_com.py")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -32,6 +33,7 @@ class FlamePrelaunch(PreLaunchHook):
"""Hook entry method."""
project_doc = self.data["project_doc"]
user_name = get_openpype_username()
hostname = socket.gethostname() # not returning wiretap host name
self.log.debug("Collected user \"{}\"".format(user_name))
self.log.info(pformat(project_doc))
@ -53,11 +55,12 @@ class FlamePrelaunch(PreLaunchHook):
"FieldDominance": "PROGRESSIVE"
}
data_to_script = {
# from settings
"host_name": "localhost",
"volume_name": "stonefs",
"group_name": "staff",
"host_name": os.getenv("FLAME_WIRETAP_HOSTNAME") or hostname,
"volume_name": os.getenv("FLAME_WIRETAP_VOLUME"),
"group_name": os.getenv("FLAME_WIRETAP_GROUP"),
"color_policy": "ACES 1.1",
# from project

View file

@ -6,7 +6,7 @@ def add_implementation_envs(env, _app):
# Add requirements to HIERO_PLUGIN_PATH
pype_root = os.environ["OPENPYPE_REPOS_ROOT"]
new_hiero_paths = [
os.path.join(pype_root, "openpype", "hosts", "hiero", "startup")
os.path.join(pype_root, "openpype", "hosts", "hiero", "api", "startup")
]
old_hiero_path = env.get("HIERO_PLUGIN_PATH") or ""
for path in old_hiero_path.split(os.pathsep):

View file

@ -23,6 +23,7 @@ from .pipeline import (
from .lib import (
pype_tag_name,
flatten,
get_track_items,
get_current_project,
get_current_sequence,
@ -75,6 +76,7 @@ __all__ = [
# Lib functions
"pype_tag_name",
"flatten",
"get_track_items",
"get_current_project",
"get_current_sequence",

View file

@ -4,6 +4,7 @@ Host specific functions where host api is connected
import os
import re
import sys
import platform
import ast
import shutil
import hiero
@ -12,7 +13,6 @@ import avalon.api as avalon
import avalon.io
from openpype.api import (Logger, Anatomy, get_anatomy_settings)
from . import tags
from compiler.ast import flatten
try:
from PySide.QtCore import QFile, QTextStream
@ -38,6 +38,14 @@ self.default_bin_name = "openpypeBin"
AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype")
def flatten(_list):
for item in _list:
if isinstance(item, (list, tuple)):
for sub_item in flatten(item):
yield sub_item
else:
yield item
def get_current_project(remove_untitled=False):
projects = flatten(hiero.core.projects())
if not remove_untitled:
@ -250,7 +258,7 @@ def set_track_item_pype_tag(track_item, data=None):
Returns:
hiero.core.Tag
"""
data = data or dict()
data = data or {}
# basic Tag's attribute
tag_data = {
@ -284,7 +292,7 @@ def get_track_item_pype_data(track_item):
Returns:
dict: data found on pype tag
"""
data = dict()
data = {}
# get pype data tag from track item
tag = get_track_item_pype_tag(track_item)
@ -299,8 +307,20 @@ def get_track_item_pype_data(track_item):
try:
# capture exceptions which are related to strings only
value = ast.literal_eval(v)
except (ValueError, SyntaxError):
if re.match(r"^[\d]+$", v):
value = int(v)
elif re.match(r"^True$", v):
value = True
elif re.match(r"^False$", v):
value = False
elif re.match(r"^None$", v):
value = None
elif re.match(r"^[\w\d_]+$", v):
value = v
else:
value = ast.literal_eval(v)
except (ValueError, SyntaxError) as msg:
log.warning(msg)
value = v
data.update({key: value})
@ -729,9 +749,14 @@ def get_selected_track_items(sequence=None):
def set_selected_track_items(track_items_list, sequence=None):
_sequence = sequence or get_current_sequence()
# make sure only trackItems are in list selection
only_track_items = [
i for i in track_items_list
if isinstance(i, hiero.core.TrackItem)]
# Getting selection
timeline_editor = hiero.ui.getTimelineEditor(_sequence)
return timeline_editor.setSelection(track_items_list)
return timeline_editor.setSelection(only_track_items)
def _read_doc_from_path(path):
@ -759,6 +784,13 @@ def _set_hrox_project_knobs(doc, **knobs):
# set attributes to Project Tag
proj_elem = doc.documentElement().firstChildElement("Project")
for k, v in knobs.items():
if "ocioconfigpath" in k:
paths_to_format = v[platform.system().lower()]
for _path in paths_to_format:
v = _path.format(**os.environ)
if not os.path.exists(v):
continue
log.debug("Project colorspace knob `{}` was set to `{}`".format(k, v))
if isinstance(v, dict):
continue
proj_elem.setAttribute(str(k), v)

View file

@ -5,7 +5,6 @@ import os
import re
import sys
import ast
from compiler.ast import flatten
import opentimelineio as otio
from . import utils
import hiero.core
@ -29,6 +28,15 @@ self.timeline = None
self.include_tags = True
def flatten(_list):
for item in _list:
if isinstance(item, (list, tuple)):
for sub_item in flatten(item):
yield sub_item
else:
yield item
def get_current_hiero_project(remove_untitled=False):
projects = flatten(hiero.core.projects())
if not remove_untitled:
@ -74,13 +82,11 @@ def create_time_effects(otio_clip, track_item):
otio_effect = otio.schema.LinearTimeWarp()
otio_effect.name = "Speed"
otio_effect.time_scalar = speed
otio_effect.metadata = {}
# freeze frame effect
if speed == 0.:
otio_effect = otio.schema.FreezeFrame()
otio_effect.name = "FreezeFrame"
otio_effect.metadata = {}
if otio_effect:
# add otio effect to clip effects

View file

@ -191,7 +191,7 @@ class CreatorWidget(QtWidgets.QDialog):
content_layout = content_layout or self.content_layout[-1]
# fix order of process by defined order value
ordered_keys = data.keys()
ordered_keys = list(data.keys())
for k, v in data.items():
try:
# try removing a key from index which should

View file

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Before After
Before After

View file

@ -18,7 +18,7 @@ except:
### Magic Widget Finding Methods - This stuff crawls all the PySide widgets, looking for an answer
def findWidget(w):
global foundryWidgets
if 'Foundry' in w.metaObject().className():
if "Foundry" in w.metaObject().className():
foundryWidgets += [w]
for c in w.children():
@ -49,7 +49,7 @@ def activeSpreadsheetTreeView():
Does some PySide widget Magic to detect the Active Spreadsheet TreeView.
"""
spreadsheetViews = getFoundryWidgetsWithClassName(
filter='SpreadsheetTreeView')
filter="SpreadsheetTreeView")
for spreadSheet in spreadsheetViews:
if spreadSheet.hasFocus():
activeSpreadSheet = spreadSheet
@ -77,23 +77,23 @@ class SpreadsheetExportCSVAction(QAction):
spreadsheetTreeView = activeSpreadsheetTreeView()
if not spreadsheetTreeView:
return 'Unable to detect the active TreeView.'
return "Unable to detect the active TreeView."
seq = hiero.ui.activeView().sequence()
if not seq:
print 'Unable to detect the active Sequence from the activeView.'
print("Unable to detect the active Sequence from the activeView.")
return
# The data model of the QTreeView
model = spreadsheetTreeView.model()
csvSavePath = os.path.join(QDir.homePath(), 'Desktop',
seq.name() + '.csv')
csvSavePath = os.path.join(QDir.homePath(), "Desktop",
seq.name() + ".csv")
savePath, filter = QFileDialog.getSaveFileName(
None,
caption="Export Spreadsheet to .CSV as...",
dir=csvSavePath,
filter="*.csv")
print 'Saving To: ' + str(savePath)
print("Saving To: {}".format(savePath))
# Saving was cancelled...
if len(savePath) == 0:
@ -101,12 +101,12 @@ class SpreadsheetExportCSVAction(QAction):
# Get the Visible Header Columns from the QTreeView
#csvHeader = ['Event', 'Status', 'Shot Name', 'Reel', 'Track', 'Speed', 'Src In', 'Src Out','Src Duration', 'Dst In', 'Dst Out', 'Dst Duration', 'Clip', 'Clip Media']
#csvHeader = ["Event", "Status", "Shot Name", "Reel", "Track", "Speed", "Src In", "Src Out","Src Duration", "Dst In", "Dst Out", "Dst Duration", "Clip", "Clip Media"]
# Get a CSV writer object
f = open(savePath, 'w')
f = open(savePath, "w")
csvWriter = csv.writer(
f, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL)
f, delimiter=',', quotechar="|", quoting=csv.QUOTE_MINIMAL)
# This is a list of the Column titles
csvHeader = []

View file

@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = "Daniel Flehner Heen"
@ -9,7 +8,7 @@ import hiero.core
from hiero.core import util
import opentimelineio as otio
from openpype.hosts.hiero.otio import hiero_export
from openpype.hosts.hiero.api.otio import hiero_export
class OTIOExportTask(hiero.core.TaskBase):

View file

@ -1,11 +1,13 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = "Daniel Flehner Heen"
__credits__ = ["Jakub Jezek", "Daniel Flehner Heen"]
import hiero.ui
import OTIOExportTask
from .OTIOExportTask import (
OTIOExportTask,
OTIOExportPreset
)
try:
# Hiero >= 11.x
@ -20,14 +22,14 @@ except ImportError:
FormLayout = QFormLayout # lint:ok
from openpype.hosts.hiero.otio import hiero_export
from openpype.hosts.hiero.api.otio import hiero_export
class OTIOExportUI(hiero.ui.TaskUIBase):
def __init__(self, preset):
"""Initialize"""
hiero.ui.TaskUIBase.__init__(
self,
OTIOExportTask.OTIOExportTask,
OTIOExportTask,
preset,
"OTIO Exporter"
)
@ -67,6 +69,6 @@ class OTIOExportUI(hiero.ui.TaskUIBase):
hiero.ui.taskUIRegistry.registerTaskUI(
OTIOExportTask.OTIOExportPreset,
OTIOExportPreset,
OTIOExportUI
)

View file

@ -0,0 +1,7 @@
from .OTIOExportTask import OTIOExportTask
from .OTIOExportUI import OTIOExportUI
__all__ = [
"OTIOExportTask",
"OTIOExportUI"
]

View file

@ -21,7 +21,7 @@ def __trackActiveProjectHandler(event):
global gTrackedActiveProject
selection = event.sender.selection()
binSelection = selection
if len(binSelection) > 0 and hasattr(binSelection[0], 'project'):
if len(binSelection) > 0 and hasattr(binSelection[0], "project"):
proj = binSelection[0].project()
# We only store this if its a valid, active User Project
@ -30,18 +30,18 @@ def __trackActiveProjectHandler(event):
hiero.core.events.registerInterest(
'kSelectionChanged/kBin', __trackActiveProjectHandler)
"kSelectionChanged/kBin", __trackActiveProjectHandler)
hiero.core.events.registerInterest(
'kSelectionChanged/kTimeline', __trackActiveProjectHandler)
"kSelectionChanged/kTimeline", __trackActiveProjectHandler)
hiero.core.events.registerInterest(
'kSelectionChanged/Spreadsheet', __trackActiveProjectHandler)
"kSelectionChanged/Spreadsheet", __trackActiveProjectHandler)
def activeProject():
"""hiero.ui.activeProject() -> returns the current Project
Note: There is not technically a notion of a 'active' Project in Hiero/NukeStudio, as it is a multi-project App.
This method determines what is 'active' by going down the following rules...
Note: There is not technically a notion of a "active" Project in Hiero/NukeStudio, as it is a multi-project App.
This method determines what is "active" by going down the following rules...
# 1 - If the current Viewer (hiero.ui.currentViewer) contains a Clip or Sequence, this item is assumed to give the active Project
# 2 - If nothing is currently in the Viewer, look to the active View, determine project from active selection
@ -54,7 +54,7 @@ def activeProject():
# Case 1 : Look for what the current Viewr tells us - this might not be what we want, and relies on hiero.ui.currentViewer() being robust.
cv = hiero.ui.currentViewer().player().sequence()
if hasattr(cv, 'project'):
if hasattr(cv, "project"):
activeProject = cv.project()
else:
# Case 2: We can't determine a project from the current Viewer, so try seeing what's selected in the activeView
@ -66,16 +66,16 @@ def activeProject():
# Handle the case where nothing is selected in the active view
if len(selection) == 0:
# It's possible that there is no selection in a Timeline/Spreadsheet, but these views have 'sequence' method, so try that...
# It's possible that there is no selection in a Timeline/Spreadsheet, but these views have "sequence" method, so try that...
if isinstance(hiero.ui.activeView(), (hiero.ui.TimelineEditor, hiero.ui.SpreadsheetView)):
activeSequence = activeView.sequence()
if hasattr(currentItem, 'project'):
if hasattr(currentItem, "project"):
activeProject = activeSequence.project()
# The active view has a selection... assume that the first item in the selection has the active Project
else:
currentItem = selection[0]
if hasattr(currentItem, 'project'):
if hasattr(currentItem, "project"):
activeProject = currentItem.project()
# Finally, Cases 3 and 4...
@ -156,9 +156,14 @@ class SaveAllProjects(QAction):
for proj in allProjects:
try:
proj.save()
print 'Saved Project: %s to: %s ' % (proj.name(), proj.path())
print("Saved Project: {} to: {} ".format(
proj.name(), proj.path()
))
except:
print 'Unable to save Project: %s to: %s. Check file permissions.' % (proj.name(), proj.path())
print((
"Unable to save Project: {} to: {}. "
"Check file permissions.").format(
proj.name(), proj.path()))
def eventHandler(self, event):
event.menu.addAction(self)
@ -190,32 +195,38 @@ class SaveNewProjectVersion(QAction):
v = None
prefix = None
try:
(prefix, v) = version_get(path, 'v')
except ValueError, msg:
print msg
(prefix, v) = version_get(path, "v")
except ValueError as msg:
print(msg)
if (prefix is not None) and (v is not None):
v = int(v)
newPath = version_set(path, prefix, v, v + 1)
try:
proj.saveAs(newPath)
print 'Saved new project version: %s to: %s ' % (oldName, newPath)
print("Saved new project version: {} to: {} ".format(
oldName, newPath))
except:
print 'Unable to save Project: %s. Check file permissions.' % (oldName)
print((
"Unable to save Project: {}. Check file permissions."
).format(oldName))
else:
newPath = path.replace(".hrox", "_v01.hrox")
answer = nuke.ask(
'%s does not contain a version number.\nDo you want to save as %s?' % (proj, newPath))
"%s does not contain a version number.\nDo you want to save as %s?" % (proj, newPath))
if answer:
try:
proj.saveAs(newPath)
print 'Saved new project version: %s to: %s ' % (oldName, newPath)
print("Saved new project version: {} to: {} ".format(
oldName, newPath))
except:
print 'Unable to save Project: %s. Check file permissions.' % (oldName)
print((
"Unable to save Project: {}. Check file "
"permissions.").format(oldName))
def eventHandler(self, event):
self.selectedProjects = []
if hasattr(event.sender, 'selection') and event.sender.selection() is not None and len(event.sender.selection()) != 0:
if hasattr(event.sender, "selection") and event.sender.selection() is not None and len(event.sender.selection()) != 0:
selection = event.sender.selection()
self.selectedProjects = uniquify(
[item.project() for item in selection])

View file

@ -0,0 +1,9 @@
"""Puts the selection project into "hiero.selection"""
import hiero
def selectionChanged(event):
hiero.selection = event.sender.selection()
hiero.core.events.registerInterest("kSelectionChanged", selectionChanged)

View file

@ -23,7 +23,7 @@ class SetFrameRateDialog(QDialog):
self._itemSelection = itemSelection
self._frameRateField = QLineEdit()
self._frameRateField.setToolTip('Enter custom frame rate here.')
self._frameRateField.setToolTip("Enter custom frame rate here.")
self._frameRateField.setValidator(QDoubleValidator(1, 99, 3, self))
self._frameRateField.textChanged.connect(self._textChanged)
layout.addRow("Enter fps: ",self._frameRateField)
@ -35,13 +35,13 @@ class SetFrameRateDialog(QDialog):
self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(False)
layout.addRow("",self._buttonbox)
self.setLayout(layout)
def _updateOkButtonState(self):
# Cancel is always an option but only enable Ok if there is some text.
currentFramerate = float(self.currentFramerateString())
enableOk = False
enableOk = ((currentFramerate > 0.0) and (currentFramerate <= 250.0))
print 'enabledOk',enableOk
print("enabledOk", enableOk)
self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(enableOk)
def _textChanged(self, newText):
@ -50,32 +50,32 @@ class SetFrameRateDialog(QDialog):
# Returns the current frame rate as a string
def currentFramerateString(self):
return str(self._frameRateField.text())
# Presents the Dialog and sets the Frame rate from a selection
def showDialogAndSetFrameRateFromSelection(self):
if self._itemSelection is not None:
if self.exec_():
# For the Undo loop...
# Construct an TimeBase object for setting the Frame Rate (fps)
fps = hiero.core.TimeBase().fromString(self.currentFramerateString())
# Set the frame rate for the selected BinItmes
for item in self._itemSelection:
item.setFramerate(fps)
item.setFramerate(fps)
return
# This is just a convenience method for returning QActions with a title, triggered method and icon.
def makeAction(title, method, icon = None):
action = QAction(title,None)
action.setIcon(QIcon(icon))
# We do this magic, so that the title string from the action is used to set the frame rate!
def methodWrapper():
method(title)
action.triggered.connect( methodWrapper )
return action
@ -88,13 +88,13 @@ class SetFrameRateMenu:
# ant: Could use hiero.core.defaultFrameRates() here but messes up with string matching because we seem to mix decimal points
self.frameRates = ['8','12','12.50','15','23.98','24','25','29.97','30','48','50','59.94','60']
self.frameRates = ["8","12","12.50","15","23.98","24","25","29.97","30","48","50","59.94","60"]
hiero.core.events.registerInterest("kShowContextMenu/kBin", self.binViewEventHandler)
self.menuActions = []
def createFrameRateMenus(self,selection):
selectedClipFPS = [str(bi.activeItem().framerate()) for bi in selection if (isinstance(bi,hiero.core.BinItem) and hasattr(bi,'activeItem'))]
selectedClipFPS = [str(bi.activeItem().framerate()) for bi in selection if (isinstance(bi,hiero.core.BinItem) and hasattr(bi,"activeItem"))]
selectedClipFPS = hiero.core.util.uniquify(selectedClipFPS)
sameFrameRate = len(selectedClipFPS)==1
self.menuActions = []
@ -106,39 +106,41 @@ class SetFrameRateMenu:
self.menuActions+=[makeAction(fps,self.setFrameRateFromMenuSelection, icon="icons:remove active.png")]
else:
self.menuActions+=[makeAction(fps,self.setFrameRateFromMenuSelection, icon=None)]
# Now add Custom... menu
self.menuActions+=[makeAction('Custom...',self.setFrameRateFromMenuSelection, icon=None)]
self.menuActions += [makeAction(
"Custom...", self.setFrameRateFromMenuSelection, icon=None)
]
frameRateMenu = QMenu("Set Frame Rate")
for a in self.menuActions:
frameRateMenu.addAction(a)
return frameRateMenu
def setFrameRateFromMenuSelection(self, menuSelectionFPS):
selectedBinItems = [bi.activeItem() for bi in self._selection if (isinstance(bi,hiero.core.BinItem) and hasattr(bi,'activeItem'))]
selectedBinItems = [bi.activeItem() for bi in self._selection if (isinstance(bi,hiero.core.BinItem) and hasattr(bi,"activeItem"))]
currentProject = selectedBinItems[0].project()
with currentProject.beginUndo("Set Frame Rate"):
if menuSelectionFPS == 'Custom...':
if menuSelectionFPS == "Custom...":
self._frameRatesDialog = SetFrameRateDialog(itemSelection = selectedBinItems )
self._frameRatesDialog.showDialogAndSetFrameRateFromSelection()
else:
for b in selectedBinItems:
b.setFramerate(hiero.core.TimeBase().fromString(menuSelectionFPS))
return
# This handles events from the Project Bin View
def binViewEventHandler(self,event):
if not hasattr(event.sender, 'selection'):
if not hasattr(event.sender, "selection"):
# Something has gone wrong, we should only be here if raised
# by the Bin view which gives a selection.
return
# Reset the selection to None...
self._selection = None
s = event.sender.selection()
@ -151,9 +153,9 @@ class SetFrameRateMenu:
if len(self._selection)==0:
return
# Creating the menu based on items selected, to highlight which frame rates are contained
self._frameRateMenu = self.createFrameRateMenus(self._selection)
# Insert the Set Frame Rate Button before the Set Media Colour Transform Action
for action in event.menu.actions():
if str(action.text()) == "Set Media Colour Transform":

View file

@ -16,29 +16,29 @@ except:
from PySide2.QtCore import *
def whereAmI(self, searchType='TrackItem'):
def whereAmI(self, searchType="TrackItem"):
"""returns a list of TrackItem or Sequnece objects in the Project which contain this Clip.
By default this will return a list of TrackItems where the Clip is used in its project.
You can also return a list of Sequences by specifying the searchType to be 'Sequence'.
You can also return a list of Sequences by specifying the searchType to be "Sequence".
Should consider putting this into hiero.core.Clip by default?
Example usage:
shotsForClip = clip.whereAmI('TrackItem')
sequencesForClip = clip.whereAmI('Sequence')
shotsForClip = clip.whereAmI("TrackItem")
sequencesForClip = clip.whereAmI("Sequence")
"""
proj = self.project()
if ('TrackItem' not in searchType) and ('Sequence' not in searchType):
print "searchType argument must be 'TrackItem' or 'Sequence'"
if ("TrackItem" not in searchType) and ("Sequence" not in searchType):
print("searchType argument must be \"TrackItem\" or \"Sequence\"")
return None
# If user specifies a TrackItem, then it will return
searches = hiero.core.findItemsInProject(proj, searchType)
if len(searches) == 0:
print 'Unable to find %s in any items of type: %s' % (str(self),
str(searchType))
print("Unable to find {} in any items of type: {}".format(
str(self), searchType))
return None
# Case 1: Looking for Shots (trackItems)
@ -110,7 +110,7 @@ class VersionAllMenu(object):
for shot in sequenceShotManifest[seq]:
updateReportString += ' %s\n (New Version: %s)\n' % (
shot.name(), shot.currentVersion().name())
updateReportString += '\n'
updateReportString += "\n"
infoBox = QMessageBox(hiero.ui.mainWindow())
infoBox.setIcon(QMessageBox.Information)
@ -202,7 +202,7 @@ class VersionAllMenu(object):
if len(bins) > 0:
# Grab the Clips inside of a Bin and append them to a list
for bin in bins:
clips = hiero.core.findItemsInBin(bin, 'Clip')
clips = hiero.core.findItemsInBin(bin, "Clip")
for clip in clips:
if clip not in clipItems:
clipItems.append(clip)
@ -291,7 +291,7 @@ class VersionAllMenu(object):
for clip in clipSelection:
# Look to see if it exists in a TrackItem somewhere...
shotUsage = clip.whereAmI('TrackItem')
shotUsage = clip.whereAmI("TrackItem")
# Next, depending on the versionOption, make the appropriate update
# There's probably a more neat/compact way of doing this...
@ -326,7 +326,7 @@ class VersionAllMenu(object):
# This handles events from the Project Bin View
def binViewEventHandler(self, event):
if not hasattr(event.sender, 'selection'):
if not hasattr(event.sender, "selection"):
# Something has gone wrong, we should only be here if raised
# by the Bin view which gives a selection.
return

View file

@ -15,55 +15,55 @@ except:
from PySide2.QtWidgets import *
from PySide2.QtCore import *
# Set to True, if you wat 'Set Status' right-click menu, False if not
# Set to True, if you wat "Set Status" right-click menu, False if not
kAddStatusMenu = True
# Set to True, if you wat 'Assign Artist' right-click menu, False if not
# Set to True, if you wat "Assign Artist" right-click menu, False if not
kAssignArtistMenu = True
# Global list of Artist Name Dictionaries
# Note: Override this to add different names, icons, department, IDs.
gArtistList = [{
'artistName': 'John Smith',
'artistIcon': 'icons:TagActor.png',
'artistDepartment': '3D',
'artistID': 0
"artistName": "John Smith",
"artistIcon": "icons:TagActor.png",
"artistDepartment": "3D",
"artistID": 0
}, {
'artistName': 'Savlvador Dali',
'artistIcon': 'icons:TagActor.png',
'artistDepartment': 'Roto',
'artistID': 1
"artistName": "Savlvador Dali",
"artistIcon": "icons:TagActor.png",
"artistDepartment": "Roto",
"artistID": 1
}, {
'artistName': 'Leonardo Da Vinci',
'artistIcon': 'icons:TagActor.png',
'artistDepartment': 'Paint',
'artistID': 2
"artistName": "Leonardo Da Vinci",
"artistIcon": "icons:TagActor.png",
"artistDepartment": "Paint",
"artistID": 2
}, {
'artistName': 'Claude Monet',
'artistIcon': 'icons:TagActor.png',
'artistDepartment': 'Comp',
'artistID': 3
"artistName": "Claude Monet",
"artistIcon": "icons:TagActor.png",
"artistDepartment": "Comp",
"artistID": 3
}, {
'artistName': 'Pablo Picasso',
'artistIcon': 'icons:TagActor.png',
'artistDepartment': 'Animation',
'artistID': 4
"artistName": "Pablo Picasso",
"artistIcon": "icons:TagActor.png",
"artistDepartment": "Animation",
"artistID": 4
}]
# Global Dictionary of Status Tags.
# Note: This can be overwritten if you want to add a new status cellType or custom icon
# Override the gStatusTags dictionary by adding your own 'Status':'Icon.png' key-value pairs.
# Add new custom keys like so: gStatusTags['For Client'] = 'forClient.png'
# Override the gStatusTags dictionary by adding your own "Status":"Icon.png" key-value pairs.
# Add new custom keys like so: gStatusTags["For Client"] = "forClient.png"
gStatusTags = {
'Approved': 'icons:status/TagApproved.png',
'Unapproved': 'icons:status/TagUnapproved.png',
'Ready To Start': 'icons:status/TagReadyToStart.png',
'Blocked': 'icons:status/TagBlocked.png',
'On Hold': 'icons:status/TagOnHold.png',
'In Progress': 'icons:status/TagInProgress.png',
'Awaiting Approval': 'icons:status/TagAwaitingApproval.png',
'Omitted': 'icons:status/TagOmitted.png',
'Final': 'icons:status/TagFinal.png'
"Approved": "icons:status/TagApproved.png",
"Unapproved": "icons:status/TagUnapproved.png",
"Ready To Start": "icons:status/TagReadyToStart.png",
"Blocked": "icons:status/TagBlocked.png",
"On Hold": "icons:status/TagOnHold.png",
"In Progress": "icons:status/TagInProgress.png",
"Awaiting Approval": "icons:status/TagAwaitingApproval.png",
"Omitted": "icons:status/TagOmitted.png",
"Final": "icons:status/TagFinal.png"
}
@ -78,17 +78,17 @@ class CustomSpreadsheetColumns(QObject):
# Ideally, we'd set this list on a Per Item basis, but this is expensive for a large mixed selection
standardColourSpaces = [
'linear', 'sRGB', 'rec709', 'Cineon', 'Gamma1.8', 'Gamma2.2',
'Panalog', 'REDLog', 'ViperLog'
"linear", "sRGB", "rec709", "Cineon", "Gamma1.8", "Gamma2.2",
"Panalog", "REDLog", "ViperLog"
]
arriColourSpaces = [
'Video - Rec709', 'LogC - Camera Native', 'Video - P3', 'ACES',
'LogC - Film', 'LogC - Wide Gamut'
"Video - Rec709", "LogC - Camera Native", "Video - P3", "ACES",
"LogC - Film", "LogC - Wide Gamut"
]
r3dColourSpaces = [
'Linear', 'Rec709', 'REDspace', 'REDlog', 'PDlog685', 'PDlog985',
'CustomPDlog', 'REDgamma', 'SRGB', 'REDlogFilm', 'REDgamma2',
'REDgamma3'
"Linear", "Rec709", "REDspace", "REDlog", "PDlog685", "PDlog985",
"CustomPDlog", "REDgamma", "SRGB", "REDlogFilm", "REDgamma2",
"REDgamma3"
]
gColourSpaces = standardColourSpaces + arriColourSpaces + r3dColourSpaces
@ -97,52 +97,52 @@ class CustomSpreadsheetColumns(QObject):
# This is the list of Columns available
gCustomColumnList = [
{
'name': 'Tags',
'cellType': 'readonly'
"name": "Tags",
"cellType": "readonly"
},
{
'name': 'Colourspace',
'cellType': 'dropdown'
"name": "Colourspace",
"cellType": "dropdown"
},
{
'name': 'Notes',
'cellType': 'readonly'
"name": "Notes",
"cellType": "readonly"
},
{
'name': 'FileType',
'cellType': 'readonly'
"name": "FileType",
"cellType": "readonly"
},
{
'name': 'Shot Status',
'cellType': 'dropdown'
"name": "Shot Status",
"cellType": "dropdown"
},
{
'name': 'Thumbnail',
'cellType': 'readonly'
"name": "Thumbnail",
"cellType": "readonly"
},
{
'name': 'MediaType',
'cellType': 'readonly'
"name": "MediaType",
"cellType": "readonly"
},
{
'name': 'Width',
'cellType': 'readonly'
"name": "Width",
"cellType": "readonly"
},
{
'name': 'Height',
'cellType': 'readonly'
"name": "Height",
"cellType": "readonly"
},
{
'name': 'Pixel Aspect',
'cellType': 'readonly'
"name": "Pixel Aspect",
"cellType": "readonly"
},
{
'name': 'Artist',
'cellType': 'dropdown'
"name": "Artist",
"cellType": "dropdown"
},
{
'name': 'Department',
'cellType': 'readonly'
"name": "Department",
"cellType": "readonly"
},
]
@ -156,7 +156,7 @@ class CustomSpreadsheetColumns(QObject):
"""
Return the name of a custom column
"""
return self.gCustomColumnList[column]['name']
return self.gCustomColumnList[column]["name"]
def getTagsString(self, item):
"""
@ -173,7 +173,7 @@ class CustomSpreadsheetColumns(QObject):
"""
Convenience method for returning all the Notes in a Tag as a string
"""
notes = ''
notes = ""
tags = item.tags()
for tag in tags:
note = tag.note()
@ -186,67 +186,67 @@ class CustomSpreadsheetColumns(QObject):
Return the data in a cell
"""
currentColumn = self.gCustomColumnList[column]
if currentColumn['name'] == 'Tags':
if currentColumn["name"] == "Tags":
return self.getTagsString(item)
if currentColumn['name'] == 'Colourspace':
if currentColumn["name"] == "Colourspace":
try:
colTransform = item.sourceMediaColourTransform()
except:
colTransform = '--'
colTransform = "--"
return colTransform
if currentColumn['name'] == 'Notes':
if currentColumn["name"] == "Notes":
try:
note = self.getNotes(item)
except:
note = ''
note = ""
return note
if currentColumn['name'] == 'FileType':
fileType = '--'
if currentColumn["name"] == "FileType":
fileType = "--"
M = item.source().mediaSource().metadata()
if M.hasKey('foundry.source.type'):
fileType = M.value('foundry.source.type')
elif M.hasKey('media.input.filereader'):
fileType = M.value('media.input.filereader')
if M.hasKey("foundry.source.type"):
fileType = M.value("foundry.source.type")
elif M.hasKey("media.input.filereader"):
fileType = M.value("media.input.filereader")
return fileType
if currentColumn['name'] == 'Shot Status':
if currentColumn["name"] == "Shot Status":
status = item.status()
if not status:
status = "--"
return str(status)
if currentColumn['name'] == 'MediaType':
if currentColumn["name"] == "MediaType":
M = item.mediaType()
return str(M).split('MediaType')[-1].replace('.k', '')
return str(M).split("MediaType")[-1].replace(".k", "")
if currentColumn['name'] == 'Thumbnail':
if currentColumn["name"] == "Thumbnail":
return str(item.eventNumber())
if currentColumn['name'] == 'Width':
if currentColumn["name"] == "Width":
return str(item.source().format().width())
if currentColumn['name'] == 'Height':
if currentColumn["name"] == "Height":
return str(item.source().format().height())
if currentColumn['name'] == 'Pixel Aspect':
if currentColumn["name"] == "Pixel Aspect":
return str(item.source().format().pixelAspect())
if currentColumn['name'] == 'Artist':
if currentColumn["name"] == "Artist":
if item.artist():
name = item.artist()['artistName']
name = item.artist()["artistName"]
return name
else:
return '--'
return "--"
if currentColumn['name'] == 'Department':
if currentColumn["name"] == "Department":
if item.artist():
dep = item.artist()['artistDepartment']
dep = item.artist()["artistDepartment"]
return dep
else:
return '--'
return "--"
return ""
@ -262,10 +262,10 @@ class CustomSpreadsheetColumns(QObject):
Return the tooltip for a cell
"""
currentColumn = self.gCustomColumnList[column]
if currentColumn['name'] == 'Tags':
if currentColumn["name"] == "Tags":
return str([item.name() for item in item.tags()])
if currentColumn['name'] == 'Notes':
if currentColumn["name"] == "Notes":
return str(self.getNotes(item))
return ""
@ -296,24 +296,24 @@ class CustomSpreadsheetColumns(QObject):
Return the icon for a cell
"""
currentColumn = self.gCustomColumnList[column]
if currentColumn['name'] == 'Colourspace':
if currentColumn["name"] == "Colourspace":
return QIcon("icons:LUT.png")
if currentColumn['name'] == 'Shot Status':
if currentColumn["name"] == "Shot Status":
status = item.status()
if status:
return QIcon(gStatusTags[status])
if currentColumn['name'] == 'MediaType':
if currentColumn["name"] == "MediaType":
mediaType = item.mediaType()
if mediaType == hiero.core.TrackItem.kVideo:
return QIcon("icons:VideoOnly.png")
elif mediaType == hiero.core.TrackItem.kAudio:
return QIcon("icons:AudioOnly.png")
if currentColumn['name'] == 'Artist':
if currentColumn["name"] == "Artist":
try:
return QIcon(item.artist()['artistIcon'])
return QIcon(item.artist()["artistIcon"])
except:
return None
return None
@ -322,9 +322,9 @@ class CustomSpreadsheetColumns(QObject):
"""
Return the size hint for a cell
"""
currentColumnName = self.gCustomColumnList[column]['name']
currentColumnName = self.gCustomColumnList[column]["name"]
if currentColumnName == 'Thumbnail':
if currentColumnName == "Thumbnail":
return QSize(90, 50)
return QSize(50, 50)
@ -335,7 +335,7 @@ class CustomSpreadsheetColumns(QObject):
with the default cell painting.
"""
currentColumn = self.gCustomColumnList[column]
if currentColumn['name'] == 'Tags':
if currentColumn["name"] == "Tags":
if option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
iconSize = 20
@ -348,14 +348,14 @@ class CustomSpreadsheetColumns(QObject):
painter.setClipRect(option.rect)
for tag in item.tags():
M = tag.metadata()
if not (M.hasKey('tag.status')
or M.hasKey('tag.artistID')):
if not (M.hasKey("tag.status")
or M.hasKey("tag.artistID")):
QIcon(tag.icon()).paint(painter, r, Qt.AlignLeft)
r.translate(r.width() + 2, 0)
painter.restore()
return True
if currentColumn['name'] == 'Thumbnail':
if currentColumn["name"] == "Thumbnail":
imageView = None
pen = QPen()
r = QRect(option.rect.x() + 2, (option.rect.y() +
@ -409,35 +409,35 @@ class CustomSpreadsheetColumns(QObject):
self.currentView = view
currentColumn = self.gCustomColumnList[column]
if currentColumn['cellType'] == 'readonly':
if currentColumn["cellType"] == "readonly":
cle = QLabel()
cle.setEnabled(False)
cle.setVisible(False)
return cle
if currentColumn['name'] == 'Colourspace':
if currentColumn["name"] == "Colourspace":
cb = QComboBox()
for colourspace in self.gColourSpaces:
cb.addItem(colourspace)
cb.currentIndexChanged.connect(self.colourspaceChanged)
return cb
if currentColumn['name'] == 'Shot Status':
if currentColumn["name"] == "Shot Status":
cb = QComboBox()
cb.addItem('')
cb.addItem("")
for key in gStatusTags.keys():
cb.addItem(QIcon(gStatusTags[key]), key)
cb.addItem('--')
cb.addItem("--")
cb.currentIndexChanged.connect(self.statusChanged)
return cb
if currentColumn['name'] == 'Artist':
if currentColumn["name"] == "Artist":
cb = QComboBox()
cb.addItem('')
cb.addItem("")
for artist in gArtistList:
cb.addItem(artist['artistName'])
cb.addItem('--')
cb.addItem(artist["artistName"])
cb.addItem("--")
cb.currentIndexChanged.connect(self.artistNameChanged)
return cb
return None
@ -479,15 +479,15 @@ class CustomSpreadsheetColumns(QObject):
status = self.sender().currentText()
project = selection[0].project()
with project.beginUndo("Set Status"):
# A string of '--' characters denotes clear the status
if status != '--':
# A string of "--" characters denotes clear the status
if status != "--":
for trackItem in selection:
trackItem.setStatus(status)
else:
for trackItem in selection:
tTags = trackItem.tags()
for tag in tTags:
if tag.metadata().hasKey('tag.status'):
if tag.metadata().hasKey("tag.status"):
trackItem.removeTag(tag)
break
@ -500,15 +500,15 @@ class CustomSpreadsheetColumns(QObject):
name = self.sender().currentText()
project = selection[0].project()
with project.beginUndo("Assign Artist"):
# A string of '--' denotes clear the assignee...
if name != '--':
# A string of "--" denotes clear the assignee...
if name != "--":
for trackItem in selection:
trackItem.setArtistByName(name)
else:
for trackItem in selection:
tTags = trackItem.tags()
for tag in tTags:
if tag.metadata().hasKey('tag.artistID'):
if tag.metadata().hasKey("tag.artistID"):
trackItem.removeTag(tag)
break
@ -518,7 +518,7 @@ def _getArtistFromID(self, artistID):
global gArtistList
artist = [
element for element in gArtistList
if element['artistID'] == int(artistID)
if element["artistID"] == int(artistID)
]
if not artist:
return None
@ -530,7 +530,7 @@ def _getArtistFromName(self, artistName):
global gArtistList
artist = [
element for element in gArtistList
if element['artistName'] == artistName
if element["artistName"] == artistName
]
if not artist:
return None
@ -542,8 +542,8 @@ def _artist(self):
artist = None
tags = self.tags()
for tag in tags:
if tag.metadata().hasKey('tag.artistID'):
artistID = tag.metadata().value('tag.artistID')
if tag.metadata().hasKey("tag.artistID"):
artistID = tag.metadata().value("tag.artistID")
artist = self.getArtistFromID(artistID)
return artist
@ -554,30 +554,30 @@ def _updateArtistTag(self, artistDict):
artistTag = None
tags = self.tags()
for tag in tags:
if tag.metadata().hasKey('tag.artistID'):
if tag.metadata().hasKey("tag.artistID"):
artistTag = tag
break
if not artistTag:
artistTag = hiero.core.Tag('Artist')
artistTag.setIcon(artistDict['artistIcon'])
artistTag.metadata().setValue('tag.artistID',
str(artistDict['artistID']))
artistTag.metadata().setValue('tag.artistName',
str(artistDict['artistName']))
artistTag.metadata().setValue('tag.artistDepartment',
str(artistDict['artistDepartment']))
artistTag = hiero.core.Tag("Artist")
artistTag.setIcon(artistDict["artistIcon"])
artistTag.metadata().setValue("tag.artistID",
str(artistDict["artistID"]))
artistTag.metadata().setValue("tag.artistName",
str(artistDict["artistName"]))
artistTag.metadata().setValue("tag.artistDepartment",
str(artistDict["artistDepartment"]))
self.sequence().editFinished()
self.addTag(artistTag)
self.sequence().editFinished()
return
artistTag.setIcon(artistDict['artistIcon'])
artistTag.metadata().setValue('tag.artistID', str(artistDict['artistID']))
artistTag.metadata().setValue('tag.artistName',
str(artistDict['artistName']))
artistTag.metadata().setValue('tag.artistDepartment',
str(artistDict['artistDepartment']))
artistTag.setIcon(artistDict["artistIcon"])
artistTag.metadata().setValue("tag.artistID", str(artistDict["artistID"]))
artistTag.metadata().setValue("tag.artistName",
str(artistDict["artistName"]))
artistTag.metadata().setValue("tag.artistDepartment",
str(artistDict["artistDepartment"]))
self.sequence().editFinished()
return
@ -588,8 +588,9 @@ def _setArtistByName(self, artistName):
artist = self.getArtistFromName(artistName)
if not artist:
print 'Artist name: %s was not found in the gArtistList.' % str(
artistName)
print((
"Artist name: {} was not found in "
"the gArtistList.").format(artistName))
return
# Do the update.
@ -602,8 +603,8 @@ def _setArtistByID(self, artistID):
artist = self.getArtistFromID(artistID)
if not artist:
print 'Artist name: %s was not found in the gArtistList.' % str(
artistID)
print("Artist name: {} was not found in the gArtistList.".format(
artistID))
return
# Do the update.
@ -625,15 +626,15 @@ def _status(self):
status = None
tags = self.tags()
for tag in tags:
if tag.metadata().hasKey('tag.status'):
status = tag.metadata().value('tag.status')
if tag.metadata().hasKey("tag.status"):
status = tag.metadata().value("tag.status")
return status
def _setStatus(self, status):
"""setShotStatus(status) -> Method to set the Status of a Shot.
Adds a special kind of status Tag to a TrackItem
Example: myTrackItem.setStatus('Final')
Example: myTrackItem.setStatus("Final")
@param status - a string, corresponding to the Status name
"""
@ -641,25 +642,25 @@ def _setStatus(self, status):
# Get a valid Tag object from the Global list of statuses
if not status in gStatusTags.keys():
print 'Status requested was not a valid Status string.'
print("Status requested was not a valid Status string.")
return
# A shot should only have one status. Check if one exists and set accordingly
statusTag = None
tags = self.tags()
for tag in tags:
if tag.metadata().hasKey('tag.status'):
if tag.metadata().hasKey("tag.status"):
statusTag = tag
break
if not statusTag:
statusTag = hiero.core.Tag('Status')
statusTag = hiero.core.Tag("Status")
statusTag.setIcon(gStatusTags[status])
statusTag.metadata().setValue('tag.status', status)
statusTag.metadata().setValue("tag.status", status)
self.addTag(statusTag)
statusTag.setIcon(gStatusTags[status])
statusTag.metadata().setValue('tag.status', status)
statusTag.metadata().setValue("tag.status", status)
self.sequence().editFinished()
return
@ -743,7 +744,7 @@ class SetStatusMenu(QMenu):
# This handles events from the Project Bin View
def eventHandler(self, event):
if not hasattr(event.sender, 'selection'):
if not hasattr(event.sender, "selection"):
# Something has gone wrong, we should only be here if raised
# by the Timeline/Spreadsheet view which gives a selection.
return
@ -781,9 +782,9 @@ class AssignArtistMenu(QMenu):
for artist in self.artists:
self.menuActions += [
titleStringTriggeredAction(
artist['artistName'],
artist["artistName"],
self.setArtistFromMenuSelection,
icon=artist['artistIcon'])
icon=artist["artistIcon"])
]
def setArtistFromMenuSelection(self, menuSelectionArtist):
@ -818,7 +819,7 @@ class AssignArtistMenu(QMenu):
# This handles events from the Project Bin View
def eventHandler(self, event):
if not hasattr(event.sender, 'selection'):
if not hasattr(event.sender, "selection"):
# Something has gone wrong, we should only be here if raised
# by the Timeline/Spreadsheet view which gives a selection.
return
@ -833,7 +834,7 @@ class AssignArtistMenu(QMenu):
event.menu.addMenu(self)
# Add the 'Set Status' context menu to Timeline and Spreadsheet
# Add the "Set Status" context menu to Timeline and Spreadsheet
if kAddStatusMenu:
setStatusMenu = SetStatusMenu()

View file

@ -23,7 +23,7 @@ class PurgeUnusedAction(QAction):
self.triggered.connect(self.PurgeUnused)
hiero.core.events.registerInterest("kShowContextMenu/kBin",
self.eventHandler)
self.setIcon(QIcon('icons:TagDelete.png'))
self.setIcon(QIcon("icons:TagDelete.png"))
# Method to return whether a Bin is empty...
def binIsEmpty(self, b):
@ -67,19 +67,19 @@ class PurgeUnusedAction(QAction):
msgBox.setDefaultButton(QMessageBox.Ok)
ret = msgBox.exec_()
if ret == QMessageBox.Cancel:
print 'Not purging anything.'
print("Not purging anything.")
elif ret == QMessageBox.Ok:
with proj.beginUndo('Purge Unused Clips'):
with proj.beginUndo("Purge Unused Clips"):
BINS = []
for clip in CLIPSTOREMOVE:
BI = clip.binItem()
B = BI.parentBin()
BINS += [B]
print 'Removing:', BI
print("Removing: {}".format(BI))
try:
B.removeItem(BI)
except:
print 'Unable to remove: ' + BI
print("Unable to remove: {}".format(BI))
return
# For each sequence, iterate through each track Item, see if the Clip is in the CLIPS list.
@ -104,24 +104,24 @@ class PurgeUnusedAction(QAction):
ret = msgBox.exec_()
if ret == QMessageBox.Cancel:
print 'Cancel'
print("Cancel")
return
elif ret == QMessageBox.Ok:
BINS = []
with proj.beginUndo('Purge Unused Clips'):
with proj.beginUndo("Purge Unused Clips"):
# Delete the rest of the Clips
for clip in CLIPSTOREMOVE:
BI = clip.binItem()
B = BI.parentBin()
BINS += [B]
print 'Removing:', BI
print("Removing: {}".format(BI))
try:
B.removeItem(BI)
except:
print 'Unable to remove: ' + BI
print("Unable to remove: {}".format(BI))
def eventHandler(self, event):
if not hasattr(event.sender, 'selection'):
if not hasattr(event.sender, "selection"):
# Something has gone wrong, we shouldn't only be here if raised
# by the Bin view which will give a selection.
return

View file

@ -13,21 +13,22 @@ except:
#----------------------------------------------
a = hiero.ui.findMenuAction('Import File(s)...')
# Note: You probably best to make this 'Ctrl+R' - currently conflicts with 'Red' in the Viewer!
a.setShortcut(QKeySequence('R'))
# Note: You probably best to make this 'Ctrl+R' - currently conflicts with "Red" in the Viewer!
a.setShortcut(QKeySequence("R"))
#----------------------------------------------
a = hiero.ui.findMenuAction('Import Folder(s)...')
a.setShortcut(QKeySequence('Shift+R'))
#----------------------------------------------
a = hiero.ui.findMenuAction('Import EDL/XML/AAF...')
a = hiero.ui.findMenuAction("Import EDL/XML/AAF...")
a.setShortcut(QKeySequence('Ctrl+Shift+O'))
#----------------------------------------------
a = hiero.ui.findMenuAction('Metadata View')
a.setShortcut(QKeySequence('I'))
a = hiero.ui.findMenuAction("Metadata View")
a.setShortcut(QKeySequence("I"))
#----------------------------------------------
a = hiero.ui.findMenuAction('Edit Settings')
a.setShortcut(QKeySequence('S'))
a = hiero.ui.findMenuAction("Edit Settings")
a.setShortcut(QKeySequence("S"))
#----------------------------------------------
a = hiero.ui.findMenuAction('Monitor Output')
a.setShortcut(QKeySequence('Ctrl+U'))
a = hiero.ui.findMenuAction("Monitor Output")
if a:
a.setShortcut(QKeySequence('Ctrl+U'))
#----------------------------------------------

View file

@ -44,16 +44,16 @@ def get_transition_type(otio_item, otio_track):
_out = None
if _in and _out:
return 'dissolve'
return "dissolve"
elif _in and not _out:
return 'fade_out'
return "fade_out"
elif not _in and _out:
return 'fade_in'
return "fade_in"
else:
return 'unknown'
return "unknown"
def find_trackitem(name, hiero_track):
@ -84,10 +84,10 @@ def apply_transition(otio_track, otio_item, track):
# Figure out track kind for getattr below
if isinstance(track, hiero.core.VideoTrack):
kind = ''
kind = ""
else:
kind = 'Audio'
kind = "Audio"
try:
# Gather TrackItems involved in trasition
@ -98,7 +98,7 @@ def apply_transition(otio_track, otio_item, track):
)
# Create transition object
if transition_type == 'dissolve':
if transition_type == "dissolve":
transition_func = getattr(
hiero.core.Transition,
'create{kind}DissolveTransition'.format(kind=kind)
@ -111,7 +111,7 @@ def apply_transition(otio_track, otio_item, track):
otio_item.out_offset.value
)
elif transition_type == 'fade_in':
elif transition_type == "fade_in":
transition_func = getattr(
hiero.core.Transition,
'create{kind}FadeInTransition'.format(kind=kind)
@ -121,7 +121,7 @@ def apply_transition(otio_track, otio_item, track):
otio_item.out_offset.value
)
elif transition_type == 'fade_out':
elif transition_type == "fade_out":
transition_func = getattr(
hiero.core.Transition,
'create{kind}FadeOutTransition'.format(kind=kind)
@ -150,11 +150,11 @@ def apply_transition(otio_track, otio_item, track):
def prep_url(url_in):
url = unquote(url_in)
if url.startswith('file://localhost/'):
return url.replace('file://localhost/', '')
if url.startswith("file://localhost/"):
return url.replace("file://localhost/", "")
url = '{url}'.format(
sep=url.startswith(os.sep) and '' or os.sep,
sep=url.startswith(os.sep) and "" or os.sep,
url=url.startswith(os.sep) and url[1:] or url
)
@ -228,8 +228,8 @@ def add_metadata(metadata, hiero_item):
continue
if value is not None:
if not key.startswith('tag.'):
key = 'tag.' + key
if not key.startswith("tag."):
key = "tag." + key
hiero_item.metadata().setValue(key, str(value))
@ -371,10 +371,10 @@ def build_sequence(
if not sequence:
# Create a Sequence
sequence = hiero.core.Sequence(otio_timeline.name or 'OTIOSequence')
sequence = hiero.core.Sequence(otio_timeline.name or "OTIOSequence")
# Set sequence settings from otio timeline if available
if hasattr(otio_timeline, 'global_start_time'):
if hasattr(otio_timeline, "global_start_time"):
if otio_timeline.global_start_time:
start_time = otio_timeline.global_start_time
sequence.setFramerate(start_time.rate)
@ -414,7 +414,7 @@ def build_sequence(
if isinstance(otio_clip, otio.schema.Stack):
bar = hiero.ui.mainWindow().statusBar()
bar.showMessage(
'Nested sequences are created separately.',
"Nested sequences are created separately.",
timeout=3000
)
build_sequence(otio_clip, project, otio_track.kind)

View file

@ -9,19 +9,19 @@ import hiero.core
import PySide2.QtWidgets as qw
from openpype.hosts.hiero.otio.hiero_import import load_otio
from openpype.hosts.hiero.api.otio.hiero_import import load_otio
class OTIOProjectSelect(qw.QDialog):
def __init__(self, projects, *args, **kwargs):
super(OTIOProjectSelect, self).__init__(*args, **kwargs)
self.setWindowTitle('Please select active project')
self.setWindowTitle("Please select active project")
self.layout = qw.QVBoxLayout()
self.label = qw.QLabel(
'Unable to determine which project to import sequence to.\n'
'Please select one.'
"Unable to determine which project to import sequence to.\n"
"Please select one."
)
self.layout.addWidget(self.label)
@ -45,7 +45,7 @@ def get_sequence(view):
elif isinstance(view, hiero.ui.BinView):
for item in view.selection():
if not hasattr(item, 'acitveItem'):
if not hasattr(item, "acitveItem"):
continue
if isinstance(item.activeItem(), hiero.core.Sequence):
@ -57,13 +57,13 @@ def get_sequence(view):
def OTIO_menu_action(event):
# Menu actions
otio_import_action = hiero.ui.createMenuAction(
'Import OTIO...',
"Import OTIO...",
open_otio_file,
icon=None
)
otio_add_track_action = hiero.ui.createMenuAction(
'New Track(s) from OTIO...',
"New Track(s) from OTIO...",
open_otio_file,
icon=None
)
@ -80,19 +80,19 @@ def OTIO_menu_action(event):
otio_add_track_action.setEnabled(True)
for action in event.menu.actions():
if action.text() == 'Import':
if action.text() == "Import":
action.menu().addAction(otio_import_action)
action.menu().addAction(otio_add_track_action)
elif action.text() == 'New Track':
elif action.text() == "New Track":
action.menu().addAction(otio_add_track_action)
def open_otio_file():
files = hiero.ui.openFileBrowser(
caption='Please select an OTIO file of choice',
pattern='*.otio',
requiredExtension='.otio'
caption="Please select an OTIO file of choice",
pattern="*.otio",
requiredExtension=".otio"
)
selection = None
@ -117,7 +117,7 @@ def open_otio_file():
else:
bar = hiero.ui.mainWindow().statusBar()
bar.showMessage(
'OTIO Import aborted by user',
"OTIO Import aborted by user",
timeout=3000
)
return

View file

@ -10,15 +10,15 @@ except:
def setPosterFrame(posterFrame=.5):
'''
"""
Update the poster frame of the given clipItmes
posterFrame = .5 uses the centre frame, a value of 0 uses the first frame, a value of 1 uses the last frame
'''
"""
view = hiero.ui.activeView()
selectedBinItems = view.selection()
selectedClipItems = [(item.activeItem()
if hasattr(item, 'activeItem') else item)
if hasattr(item, "activeItem") else item)
for item in selectedBinItems]
for clip in selectedClipItems:

View file

@ -65,13 +65,9 @@ def open_file(filepath):
def current_file():
current_file = hiero.core.projects()[-1].path()
normalised = os.path.normpath(current_file)
# Unsaved current file
if normalised == "":
if not current_file:
return None
return normalised
return os.path.normpath(current_file)
def work_root(session):

View file

@ -1,11 +1,9 @@
import pyblish
import openpype
from openpype.hosts.hiero import api as phiero
from openpype.hosts.hiero.otio import hiero_export
from openpype.hosts.hiero.api.otio import hiero_export
import hiero
from compiler.ast import flatten
# # developer reload modules
from pprint import pformat
@ -339,10 +337,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
continue
track_index = track.trackIndex()
_sub_track_items = flatten(track.subTrackItems())
_sub_track_items = phiero.flatten(track.subTrackItems())
# continue only if any subtrack items are collected
if len(_sub_track_items) < 1:
if not list(_sub_track_items):
continue
enabled_sti = []
@ -357,7 +355,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
enabled_sti.append(_sti)
# continue only if any subtrack items are collected
if len(enabled_sti) < 1:
if not enabled_sti:
continue
# add collection of subtrackitems to dict
@ -371,7 +369,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
Returns list of Clip's hiero.core.Annotation
"""
annotations = []
subTrackItems = flatten(clip.subTrackItems())
subTrackItems = phiero.flatten(clip.subTrackItems())
annotations += [item for item in subTrackItems if isinstance(
item, hiero.core.Annotation)]
return annotations
@ -382,7 +380,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
Returns list of Clip's hiero.core.SubTrackItem
"""
subtracks = []
subTrackItems = flatten(clip.parent().subTrackItems())
subTrackItems = phiero.flatten(clip.parent().subTrackItems())
for item in subTrackItems:
if "TimeWarp" in item.name():
continue

View file

@ -4,7 +4,7 @@ import hiero.ui
from openpype.hosts.hiero import api as phiero
from avalon import api as avalon
from pprint import pformat
from openpype.hosts.hiero.otio import hiero_export
from openpype.hosts.hiero.api.otio import hiero_export
from Qt.QtGui import QPixmap
import tempfile

View file

@ -1,7 +1,7 @@
from pyblish import api
import hiero
import math
from openpype.hosts.hiero.otio.hiero_export import create_otio_time_range
from openpype.hosts.hiero.api.otio.hiero_export import create_otio_time_range
class PrecollectRetime(api.InstancePlugin):
"""Calculate Retiming of selected track items."""

View file

@ -1,7 +0,0 @@
from OTIOExportTask import OTIOExportTask
from OTIOExportUI import OTIOExportUI
__all__ = [
'OTIOExportTask',
'OTIOExportUI'
]

View file

@ -1,9 +0,0 @@
"""Puts the selection project into 'hiero.selection'"""
import hiero
def selectionChanged(event):
hiero.selection = event.sender.selection()
hiero.core.events.registerInterest('kSelectionChanged', selectionChanged)

View file

@ -13,10 +13,14 @@ class CameraWindow(QtWidgets.QDialog):
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
self.camera = None
self.static_image_plane = False
self.show_in_all_views = False
self.widgets = {
"label": QtWidgets.QLabel("Select camera for image plane."),
"list": QtWidgets.QListWidget(),
"staticImagePlane": QtWidgets.QCheckBox(),
"showInAllViews": QtWidgets.QCheckBox(),
"warning": QtWidgets.QLabel("No cameras selected!"),
"buttons": QtWidgets.QWidget(),
"okButton": QtWidgets.QPushButton("Ok"),
@ -31,6 +35,9 @@ class CameraWindow(QtWidgets.QDialog):
for camera in cameras:
self.widgets["list"].addItem(camera)
self.widgets["staticImagePlane"].setText("Make Image Plane Static")
self.widgets["showInAllViews"].setText("Show Image Plane in All Views")
# Build buttons.
layout = QtWidgets.QHBoxLayout(self.widgets["buttons"])
layout.addWidget(self.widgets["okButton"])
@ -40,6 +47,8 @@ class CameraWindow(QtWidgets.QDialog):
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.widgets["label"])
layout.addWidget(self.widgets["list"])
layout.addWidget(self.widgets["staticImagePlane"])
layout.addWidget(self.widgets["showInAllViews"])
layout.addWidget(self.widgets["buttons"])
layout.addWidget(self.widgets["warning"])
@ -54,6 +63,8 @@ class CameraWindow(QtWidgets.QDialog):
if self.camera is None:
self.widgets["warning"].setVisible(True)
return
self.show_in_all_views = self.widgets["showInAllViews"].isChecked()
self.static_image_plane = self.widgets["staticImagePlane"].isChecked()
self.close()
@ -65,15 +76,15 @@ class CameraWindow(QtWidgets.QDialog):
class ImagePlaneLoader(api.Loader):
"""Specific loader of plate for image planes on selected camera."""
families = ["plate", "render"]
families = ["image", "plate", "render"]
label = "Load imagePlane."
representations = ["mov", "exr", "preview", "png"]
icon = "image"
color = "orange"
def load(self, context, name, namespace, data):
def load(self, context, name, namespace, data, options=None):
import pymel.core as pm
new_nodes = []
image_plane_depth = 1000
asset = context['asset']['name']
@ -85,17 +96,23 @@ class ImagePlaneLoader(api.Loader):
# Get camera from user selection.
camera = None
default_cameras = [
"frontShape", "perspShape", "sideShape", "topShape"
]
cameras = [
x for x in pm.ls(type="camera") if x.name() not in default_cameras
]
camera_names = {x.getParent().name(): x for x in cameras}
camera_names["Create new camera."] = "create_camera"
window = CameraWindow(camera_names.keys())
window.exec_()
camera = camera_names[window.camera]
is_static_image_plane = None
is_in_all_views = None
if data:
camera = pm.PyNode(data.get("camera"))
is_static_image_plane = data.get("static_image_plane")
is_in_all_views = data.get("in_all_views")
if not camera:
cameras = pm.ls(type="camera")
camera_names = {x.getParent().name(): x for x in cameras}
camera_names["Create new camera."] = "create_camera"
window = CameraWindow(camera_names.keys())
window.exec_()
camera = camera_names[window.camera]
is_static_image_plane = window.static_image_plane
is_in_all_views = window.show_in_all_views
if camera == "create_camera":
camera = pm.createNode("camera")
@ -111,13 +128,14 @@ class ImagePlaneLoader(api.Loader):
# Create image plane
image_plane_transform, image_plane_shape = pm.imagePlane(
camera=camera, showInAllViews=False
fileName=context["representation"]["data"]["path"],
camera=camera, showInAllViews=is_in_all_views
)
image_plane_shape.depth.set(image_plane_depth)
image_plane_shape.imageName.set(
context["representation"]["data"]["path"]
)
if is_static_image_plane:
image_plane_shape.detach()
image_plane_transform.setRotation(camera.getRotation())
start_frame = pm.playbackOptions(q=True, min=True)
end_frame = pm.playbackOptions(q=True, max=True)

View file

@ -492,6 +492,8 @@ class CollectLook(pyblish.api.InstancePlugin):
if not cmds.attributeQuery(attr, node=node, exists=True):
continue
attribute = "{}.{}".format(node, attr)
if cmds.getAttr(attribute, type=True) == "message":
continue
node_attributes[attr] = cmds.getAttr(attribute)
attributes.append({"name": node,

View file

@ -192,7 +192,10 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
render_products = layer_render_products.layer_data.products
assert render_products, "no render products generated"
exp_files = []
multipart = False
for product in render_products:
if product.multipart:
multipart = True
product_name = product.productName
if product.camera and layer_render_products.has_camera_token():
product_name = "{}{}".format(
@ -205,7 +208,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
})
self.log.info("multipart: {}".format(
layer_render_products.multipart))
multipart))
assert exp_files, "no file names were generated, this is bug"
self.log.info(exp_files)
@ -221,14 +224,19 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
# append full path
full_exp_files = []
aov_dict = {}
default_render_file = context.data.get('project_settings')\
.get('maya')\
.get('create')\
.get('CreateRender')\
.get('default_render_image_folder')
# replace relative paths with absolute. Render products are
# returned as list of dictionaries.
publish_meta_path = None
for aov in exp_files:
full_paths = []
for file in aov[aov.keys()[0]]:
full_path = os.path.join(workspace, "renders", file)
full_path = os.path.join(workspace, default_render_file,
file)
full_path = full_path.replace("\\", "/")
full_paths.append(full_path)
publish_meta_path = os.path.dirname(full_path)
@ -300,7 +308,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
"subset": expected_layer_name,
"attachTo": attach_to,
"setMembers": layer_name,
"multipartExr": layer_render_products.multipart,
"multipartExr": multipart,
"review": render_instance.data.get("review") or False,
"publish": True,

View file

@ -55,8 +55,16 @@ def maketx(source, destination, *args):
str: Output of `maketx` command.
"""
from openpype.lib import get_oiio_tools_path
maketx_path = get_oiio_tools_path("maketx")
if not os.path.exists(maketx_path):
print(
"OIIO tool not found in {}".format(maketx_path))
raise AssertionError("OIIO tool not found")
cmd = [
"maketx",
maketx_path,
"-v", # verbose
"-u", # update mode
# unpremultiply before conversion (recommended when alpha present)

View file

@ -23,11 +23,24 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin):
def process(self, instance):
assert get_file_rule("images") == "renders", (
"Workspace's `images` file rule must be set to: renders"
default_render_file = self.get_default_render_image_folder(instance)
assert get_file_rule("images") == default_render_file, (
"Workspace's `images` file rule must be set to: {}".format(
default_render_file
)
)
@classmethod
def repair(cls, instance):
pm.workspace.fileRules["images"] = "renders"
default = cls.get_default_render_image_folder(instance)
pm.workspace.fileRules["images"] = default
pm.system.Workspace.save()
@staticmethod
def get_default_render_image_folder(instance):
return instance.context.data.get('project_settings')\
.get('maya') \
.get('create') \
.get('CreateRender') \
.get('default_render_image_folder')

View file

@ -54,6 +54,10 @@ def install():
''' Installing all requarements for Nuke host
'''
# remove all registred callbacks form avalon.nuke
from avalon import pipeline
pipeline._registered_event_handlers.clear()
log.info("Registering Nuke plug-ins..")
pyblish.api.register_plugin_path(PUBLISH_PATH)
avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH)
@ -62,6 +66,7 @@ def install():
# Register Avalon event for workfiles loading.
avalon.api.on("workio.open_file", lib.check_inventory_versions)
avalon.api.on("taskChanged", menu.change_context_label)
pyblish.api.register_callback(
"instanceToggled", on_pyblish_instance_toggled)

View file

@ -1,101 +1,128 @@
import os
import nuke
from avalon.api import Session
from avalon.nuke.pipeline import get_main_window
from .lib import WorkfileSettings
from openpype.api import Logger, BuildWorkfile, get_current_project_settings
from openpype.tools.utils import host_tools
from avalon.nuke.pipeline import get_main_window
log = Logger().get_logger(__name__)
menu_label = os.environ["AVALON_LABEL"]
context_label = None
def install():
main_window = get_main_window()
def change_context_label(*args):
global context_label
menubar = nuke.menu("Nuke")
menu = menubar.findItem(menu_label)
# replace reset resolution from avalon core to pype's
name = "Work Files..."
rm_item = [
(i, item) for i, item in enumerate(menu.items()) if name in item.name()
][0]
log.debug("Changing Item: {}".format(rm_item))
menu.removeItem(rm_item[1].name())
menu.addCommand(
name,
lambda: host_tools.show_workfiles(parent=main_window),
index=2
label = "{0}, {1}".format(
os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"]
)
menu.addSeparator(index=3)
# replace reset resolution from avalon core to pype's
name = "Reset Resolution"
new_name = "Set Resolution"
rm_item = [
(i, item) for i, item in enumerate(menu.items()) if name in item.name()
(i, item) for i, item in enumerate(menu.items())
if context_label in item.name()
][0]
log.debug("Changing Item: {}".format(rm_item))
# rm_item[1].setEnabled(False)
menu.removeItem(rm_item[1].name())
menu.addCommand(
new_name,
lambda: WorkfileSettings().reset_resolution(),
context_action = menu.addCommand(
label,
index=(rm_item[0])
)
context_action.setEnabled(False)
# replace reset frame range from avalon core to pype's
name = "Reset Frame Range"
new_name = "Set Frame Range"
rm_item = [
(i, item) for i, item in enumerate(menu.items()) if name in item.name()
][0]
log.debug("Changing Item: {}".format(rm_item))
# rm_item[1].setEnabled(False)
menu.removeItem(rm_item[1].name())
menu.addCommand(
new_name,
lambda: WorkfileSettings().reset_frame_range_handles(),
index=(rm_item[0])
)
log.info("Task label changed from `{}` to `{}`".format(
context_label, label))
# add colorspace menu item
name = "Set Colorspace"
menu.addCommand(
name, lambda: WorkfileSettings().set_colorspace()
)
log.debug("Adding menu item: {}".format(name))
context_label = label
# add item that applies all setting above
name = "Apply All Settings"
menu.addCommand(
name,
lambda: WorkfileSettings().set_context_settings()
def install():
from openpype.hosts.nuke.api import reload_config
global context_label
# uninstall original avalon menu
uninstall()
main_window = get_main_window()
menubar = nuke.menu("Nuke")
menu = menubar.addMenu(menu_label)
label = "{0}, {1}".format(
os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"]
)
log.debug("Adding menu item: {}".format(name))
context_label = label
context_action = menu.addCommand(label)
context_action.setEnabled(False)
menu.addSeparator()
# add workfile builder menu item
name = "Build Workfile"
menu.addCommand(
name, lambda: BuildWorkfile().process()
"Work Files...",
lambda: host_tools.show_workfiles(parent=main_window)
)
menu.addSeparator()
menu.addCommand(
"Create...",
lambda: host_tools.show_creator(parent=main_window)
)
menu.addCommand(
"Load...",
lambda: host_tools.show_loader(
parent=main_window,
use_context=True
)
)
menu.addCommand(
"Publish...",
lambda: host_tools.show_publish(parent=main_window)
)
menu.addCommand(
"Manage...",
lambda: host_tools.show_scene_inventory(parent=main_window)
)
menu.addSeparator()
menu.addCommand(
"Set Resolution",
lambda: WorkfileSettings().reset_resolution()
)
menu.addCommand(
"Set Frame Range",
lambda: WorkfileSettings().reset_frame_range_handles()
)
menu.addCommand(
"Set Colorspace",
lambda: WorkfileSettings().set_colorspace()
)
menu.addCommand(
"Apply All Settings",
lambda: WorkfileSettings().set_context_settings()
)
menu.addSeparator()
menu.addCommand(
"Build Workfile",
lambda: BuildWorkfile().process()
)
log.debug("Adding menu item: {}".format(name))
# Add experimental tools action
menu.addSeparator()
menu.addCommand(
"Experimental tools...",
lambda: host_tools.show_experimental_tools_dialog(parent=main_window)
)
# add reload pipeline only in debug mode
if bool(os.getenv("NUKE_DEBUG")):
menu.addSeparator()
menu.addCommand("Reload Pipeline", reload_config)
# adding shortcuts
add_shortcuts_from_presets()

View file

@ -116,16 +116,7 @@ class LoadClip(plugin.NukeLoader):
"Representation id `{}` is failing to load".format(repr_id))
return
name_data = {
"asset": repr_cont["asset"],
"subset": repr_cont["subset"],
"representation": context["representation"]["name"],
"ext": repr_cont["representation"],
"id": context["representation"]["_id"],
"class_name": self.__class__.__name__
}
read_name = self.node_name_template.format(**name_data)
read_name = self._get_node_name(context["representation"])
# Create the Loader with the filename path set
read_node = nuke.createNode(
@ -143,7 +134,7 @@ class LoadClip(plugin.NukeLoader):
elif iio_colorspace is not None:
read_node["colorspace"].setValue(iio_colorspace)
self.set_range_to_node(read_node, first, last, start_at_workfile)
self._set_range_to_node(read_node, first, last, start_at_workfile)
# add additional metadata from the version to imprint Avalon knob
add_keys = ["frameStart", "frameEnd",
@ -171,7 +162,7 @@ class LoadClip(plugin.NukeLoader):
data=data_imprint)
if version_data.get("retime", None):
self.make_retimes(read_node, version_data)
self._make_retimes(read_node, version_data)
self.set_as_member(read_node)
@ -230,6 +221,9 @@ class LoadClip(plugin.NukeLoader):
"Representation id `{}` is failing to load".format(repr_id))
return
read_name = self._get_node_name(representation)
read_node["name"].setValue(read_name)
read_node["file"].setValue(file)
# to avoid multiple undo steps for rest of process
@ -242,7 +236,7 @@ class LoadClip(plugin.NukeLoader):
elif iio_colorspace is not None:
read_node["colorspace"].setValue(iio_colorspace)
self.set_range_to_node(read_node, first, last, start_at_workfile)
self._set_range_to_node(read_node, first, last, start_at_workfile)
updated_dict = {
"representation": str(representation["_id"]),
@ -279,21 +273,12 @@ class LoadClip(plugin.NukeLoader):
self.log.info("udated to version: {}".format(version.get("name")))
if version_data.get("retime", None):
self.make_retimes(read_node, version_data)
self._make_retimes(read_node, version_data)
else:
self.clear_members(read_node)
self.set_as_member(read_node)
def set_range_to_node(self, read_node, first, last, start_at_workfile):
read_node['origfirst'].setValue(int(first))
read_node['first'].setValue(int(first))
read_node['origlast'].setValue(int(last))
read_node['last'].setValue(int(last))
# set start frame depending on workfile or version
self.loader_shift(read_node, start_at_workfile)
def remove(self, container):
from avalon.nuke import viewer_update_and_undo_stop
@ -307,7 +292,16 @@ class LoadClip(plugin.NukeLoader):
for member in members:
nuke.delete(member)
def make_retimes(self, parent_node, version_data):
def _set_range_to_node(self, read_node, first, last, start_at_workfile):
read_node['origfirst'].setValue(int(first))
read_node['first'].setValue(int(first))
read_node['origlast'].setValue(int(last))
read_node['last'].setValue(int(last))
# set start frame depending on workfile or version
self._loader_shift(read_node, start_at_workfile)
def _make_retimes(self, parent_node, version_data):
''' Create all retime and timewarping nodes with coppied animation '''
speed = version_data.get('speed', 1)
time_warp_nodes = version_data.get('timewarps', [])
@ -360,7 +354,7 @@ class LoadClip(plugin.NukeLoader):
for i, n in enumerate(dependent_nodes):
last_node.setInput(i, n)
def loader_shift(self, read_node, workfile_start=False):
def _loader_shift(self, read_node, workfile_start=False):
""" Set start frame of read node to a workfile start
Args:
@ -371,3 +365,17 @@ class LoadClip(plugin.NukeLoader):
if workfile_start:
read_node['frame_mode'].setValue("start at")
read_node['frame'].setValue(str(self.script_start))
def _get_node_name(self, representation):
repr_cont = representation["context"]
name_data = {
"asset": repr_cont["asset"],
"subset": repr_cont["subset"],
"representation": representation["name"],
"ext": repr_cont["representation"],
"id": representation["_id"],
"class_name": self.__class__.__name__
}
return self.node_name_template.format(**name_data)

View file

@ -42,10 +42,14 @@ class NukeRenderLocal(openpype.api.Extractor):
self.log.info("Start frame: {}".format(first_frame))
self.log.info("End frame: {}".format(last_frame))
# write node url might contain nuke's ctl expressin
# as [python ...]/path...
path = node["file"].evaluate()
# Ensure output directory exists.
directory = os.path.dirname(node["file"].value())
if not os.path.exists(directory):
os.makedirs(directory)
out_dir = os.path.dirname(path)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
# Render frames
nuke.execute(
@ -58,15 +62,12 @@ class NukeRenderLocal(openpype.api.Extractor):
if "slate" in families:
first_frame += 1
path = node['file'].value()
out_dir = os.path.dirname(path)
ext = node["file_type"].value()
if "representations" not in instance.data:
instance.data["representations"] = []
collected_frames = os.listdir(out_dir)
if len(collected_frames) == 1:
repre = {
'name': ext,

View file

@ -42,6 +42,7 @@ class ExtractReviewDataMov(openpype.api.Extractor):
# generate data
with anlib.maintained_selection():
generated_repres = []
for o_name, o_data in self.outputs.items():
f_families = o_data["filter"]["families"]
f_task_types = o_data["filter"]["task_types"]
@ -112,11 +113,13 @@ class ExtractReviewDataMov(openpype.api.Extractor):
})
else:
data = exporter.generate_mov(**o_data)
generated_repres.extend(data["representations"])
self.log.info(data["representations"])
self.log.info(generated_repres)
# assign to representations
instance.data["representations"] += data["representations"]
if generated_repres:
# assign to representations
instance.data["representations"] += generated_repres
self.log.debug(
"_ representations: {}".format(

View file

@ -67,7 +67,9 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
if not repre.get("files"):
msg = ("no frames were collected, "
"you need to render them")
"you need to render them.\n"
"Check properties of write node (group) and"
"select 'Local' option in 'Publish' dropdown.")
self.log.error(msg)
raise ValidationException(msg)

View file

@ -8,7 +8,7 @@ from avalon import photoshop
class CollectCurrentFile(pyblish.api.ContextPlugin):
"""Inject the current working file into context"""
order = pyblish.api.CollectorOrder - 0.5
order = pyblish.api.CollectorOrder - 0.49
label = "Current File"
hosts = ["photoshop"]

View file

@ -0,0 +1,57 @@
import os
import re
import pyblish.api
from avalon import photoshop
class CollectExtensionVersion(pyblish.api.ContextPlugin):
""" Pulls and compares version of installed extension.
It is recommended to use same extension as in provided Openpype code.
Please use Anastasiys Extension Manager or ZXPInstaller to update
extension in case of an error.
You can locate extension.zxp in your installed Openpype code in
`repos/avalon-core/avalon/photoshop`
"""
# This technically should be a validator, but other collectors might be
# impacted with usage of obsolete extension, so collector that runs first
# was chosen
order = pyblish.api.CollectorOrder - 0.5
label = "Collect extension version"
hosts = ["photoshop"]
optional = True
active = True
def process(self, context):
installed_version = photoshop.stub().get_extension_version()
if not installed_version:
raise ValueError("Unknown version, probably old extension")
manifest_url = os.path.join(os.path.dirname(photoshop.__file__),
"extension", "CSXS", "manifest.xml")
if not os.path.exists(manifest_url):
self.log.debug("Unable to locate extension manifest, not checking")
return
expected_version = None
with open(manifest_url) as fp:
content = fp.read()
found = re.findall(r'(ExtensionBundleVersion=")([0-10\.]+)(")',
content)
if found:
expected_version = found[0][1]
if expected_version != installed_version:
msg = "Expected version '{}' found '{}'\n".format(
expected_version, installed_version)
msg += "Please update your installed extension, it might not work "
msg += "properly."
raise ValueError(msg)

View file

@ -1,3 +1,5 @@
import re
import pyblish.api
import openpype.api
from avalon import photoshop
@ -19,20 +21,33 @@ class ValidateNamingRepair(pyblish.api.Action):
and result["instance"] not in failed):
failed.append(result["instance"])
invalid_chars, replace_char = plugin.get_replace_chars()
self.log.info("{} --- {}".format(invalid_chars, replace_char))
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(failed, plugin)
stub = photoshop.stub()
for instance in instances:
self.log.info("validate_naming instance {}".format(instance))
name = instance.data["name"].replace(" ", "_")
name = name.replace(instance.data["family"], '')
instance[0].Name = name
data = stub.read(instance[0])
data["subset"] = "image" + name
stub.imprint(instance[0], data)
metadata = stub.read(instance[0])
self.log.info("metadata instance {}".format(metadata))
layer_name = None
if metadata.get("uuid"):
layer_data = stub.get_layer(metadata["uuid"])
self.log.info("layer_data {}".format(layer_data))
if layer_data:
layer_name = re.sub(invalid_chars,
replace_char,
layer_data.name)
name = stub.PUBLISH_ICON + name
stub.rename_layer(instance.data["uuid"], name)
stub.rename_layer(instance.data["uuid"], layer_name)
subset_name = re.sub(invalid_chars, replace_char,
instance.data["name"])
instance[0].Name = layer_name or subset_name
metadata["subset"] = subset_name
stub.imprint(instance[0], metadata)
return True
@ -49,12 +64,21 @@ class ValidateNaming(pyblish.api.InstancePlugin):
families = ["image"]
actions = [ValidateNamingRepair]
# configured by Settings
invalid_chars = ''
replace_char = ''
def process(self, instance):
help_msg = ' Use Repair action (A) in Pyblish to fix it.'
msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"],
help_msg)
assert " " not in instance.data["name"], msg
assert not re.search(self.invalid_chars, instance.data["name"]), msg
msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"],
help_msg)
assert " " not in instance.data["subset"], msg
assert not re.search(self.invalid_chars, instance.data["subset"]), msg
@classmethod
def get_replace_chars(cls):
"""Pass values configured in Settings for Repair."""
return cls.invalid_chars, cls.replace_char

View file

@ -0,0 +1,158 @@
import sys
from Qt import QtWidgets, QtCore, QtGui
from openpype import (
resources,
style
)
from openpype.tools.utils import host_tools
from openpype.tools.utils.lib import qt_app_context
class ToolsBtnsWidget(QtWidgets.QWidget):
"""Widget containing buttons which are clickable."""
tool_required = QtCore.Signal(str)
def __init__(self, parent=None):
super(ToolsBtnsWidget, self).__init__(parent)
create_btn = QtWidgets.QPushButton("Create...", self)
load_btn = QtWidgets.QPushButton("Load...", self)
publish_btn = QtWidgets.QPushButton("Publish...", self)
manage_btn = QtWidgets.QPushButton("Manage...", self)
experimental_tools_btn = QtWidgets.QPushButton(
"Experimental tools...", self
)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(create_btn, 0)
layout.addWidget(load_btn, 0)
layout.addWidget(publish_btn, 0)
layout.addWidget(manage_btn, 0)
layout.addWidget(experimental_tools_btn, 0)
layout.addStretch(1)
create_btn.clicked.connect(self._on_create)
load_btn.clicked.connect(self._on_load)
publish_btn.clicked.connect(self._on_publish)
manage_btn.clicked.connect(self._on_manage)
experimental_tools_btn.clicked.connect(self._on_experimental)
def _on_create(self):
self.tool_required.emit("creator")
def _on_load(self):
self.tool_required.emit("loader")
def _on_publish(self):
self.tool_required.emit("publish")
def _on_manage(self):
self.tool_required.emit("sceneinventory")
def _on_experimental(self):
self.tool_required.emit("experimental_tools")
class ToolsDialog(QtWidgets.QDialog):
"""Dialog with tool buttons that will stay opened until user close it."""
def __init__(self, *args, **kwargs):
super(ToolsDialog, self).__init__(*args, **kwargs)
self.setWindowTitle("OpenPype tools")
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(
QtCore.Qt.Window
| QtCore.Qt.WindowStaysOnTopHint
)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
tools_widget = ToolsBtnsWidget(self)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(tools_widget)
tools_widget.tool_required.connect(self._on_tool_require)
self._tools_widget = tools_widget
self._first_show = True
def sizeHint(self):
result = super(ToolsDialog, self).sizeHint()
result.setWidth(result.width() * 2)
return result
def showEvent(self, event):
super(ToolsDialog, self).showEvent(event)
if self._first_show:
self.setStyleSheet(style.load_stylesheet())
self._first_show = False
def _on_tool_require(self, tool_name):
host_tools.show_tool_by_name(tool_name, parent=self)
class ToolsPopup(ToolsDialog):
"""Popup with tool buttons that will close when loose focus."""
def __init__(self, *args, **kwargs):
super(ToolsPopup, self).__init__(*args, **kwargs)
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint
| QtCore.Qt.Popup
)
def showEvent(self, event):
super(ToolsPopup, self).showEvent(event)
app = QtWidgets.QApplication.instance()
app.processEvents()
pos = QtGui.QCursor.pos()
self.move(pos)
class WindowCache:
"""Cached objects and methods to be used in global scope."""
_dialog = None
_popup = None
_first_show = True
@classmethod
def _before_show(cls):
"""Create QApplication if does not exists yet."""
if not cls._first_show:
return
cls._first_show = False
if not QtWidgets.QApplication.instance():
QtWidgets.QApplication(sys.argv)
@classmethod
def show_popup(cls):
cls._before_show()
with qt_app_context():
if cls._popup is None:
cls._popup = ToolsPopup()
cls._popup.show()
@classmethod
def show_dialog(cls):
cls._before_show()
with qt_app_context():
if cls._dialog is None:
cls._dialog = ToolsDialog()
cls._dialog.show()
cls._dialog.raise_()
cls._dialog.activateWindow()
def show_tools_popup():
WindowCache.show_popup()
def show_tools_dialog():
WindowCache.show_dialog()

View file

@ -138,7 +138,7 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin):
"family": "render"
}
subset_name = get_subset_name_with_asset_doc(
self.render_pass_family,
self.render_layer_family,
variant,
task_name,
asset_doc,
@ -223,7 +223,7 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin):
"name": subset_name,
"subset": subset_name,
"label": subset_name,
"family": self.render_pass_family,
"family": "render",
# Add `review` family for thumbnail integration
"families": [self.render_pass_family, "review"],
"representations": [],
@ -239,9 +239,9 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin):
"name": subset_name,
"subset": subset_name,
"label": subset_name,
"family": self.render_pass_family,
"family": "render",
# Add `review` family for thumbnail integration
"families": [self.render_pass_family, "review"],
"families": [self.render_layer_family, "review"],
"representations": [],
"layers": layers,
"stagingDir": staging_dir

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