mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 00:44:52 +01:00
Merge branch 'develop' into feature/new_publisher_core
This commit is contained in:
commit
ac9575d722
108 changed files with 2886 additions and 1065 deletions
8
.github/workflows/prerelease.yml
vendored
8
.github/workflows/prerelease.yml
vendored
|
|
@ -43,10 +43,10 @@ jobs:
|
|||
uses: heinrichreimer/github-changelog-generator-action@v2.2
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
breakingLabel: '#### 💥 Breaking'
|
||||
enhancementLabel: '#### 🚀 Enhancements'
|
||||
bugsLabel: '#### 🐛 Bug fixes'
|
||||
deprecatedLabel: '#### ⚠️ Deprecations'
|
||||
breakingLabel: '**💥 Breaking**'
|
||||
enhancementLabel: '**🚀 Enhancements**'
|
||||
bugsLabel: '**🐛 Bug fixes**'
|
||||
deprecatedLabel: '**⚠️ Deprecations**'
|
||||
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
|
||||
issues: false
|
||||
issuesWoLabels: false
|
||||
|
|
|
|||
116
.github/workflows/release.yml
vendored
116
.github/workflows/release.yml
vendored
|
|
@ -1,9 +1,9 @@
|
|||
name: Stable Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*[0-9].*[0-9].*[0-9]*'
|
||||
release:
|
||||
types:
|
||||
- prereleased
|
||||
|
||||
jobs:
|
||||
create_release:
|
||||
|
|
@ -23,35 +23,26 @@ jobs:
|
|||
- name: Install Python requirements
|
||||
run: pip install gitpython semver
|
||||
|
||||
- name: Set env
|
||||
run: |
|
||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
git config user.email ${{ secrets.CI_EMAIL }}
|
||||
git config user.name ${{ secrets.CI_USER }}
|
||||
git fetch
|
||||
git checkout -b main origin/main
|
||||
git tag -d ${GITHUB_REF#refs/*/}
|
||||
git remote set-url --push origin https://pypebot:${{ secrets.ADMIN_TOKEN }}@github.com/pypeclub/openpype
|
||||
git push origin --delete ${GITHUB_REF#refs/*/}
|
||||
echo PREVIOUS_VERSION=`git describe --tags --match="[0-9]*" --abbrev=0` >> $GITHUB_ENV
|
||||
|
||||
- name: 💉 Inject new version into files
|
||||
id: version
|
||||
if: steps.version_type.outputs.type != 'skip'
|
||||
run: |
|
||||
python ./tools/ci_tools.py --version ${{ env.RELEASE_VERSION }}
|
||||
echo ::set-output name=current_version::${GITHUB_REF#refs/*/}
|
||||
RESULT=$(python ./tools/ci_tools.py --finalize ${GITHUB_REF#refs/*/})
|
||||
LASTRELEASE=$(python ./tools/ci_tools.py --lastversion release)
|
||||
|
||||
echo ::set-output name=last_release::$LASTRELEASE
|
||||
echo ::set-output name=release_tag::$RESULT
|
||||
|
||||
- name: "✏️ Generate full changelog"
|
||||
if: steps.version_type.outputs.type != 'skip'
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
id: generate-full-changelog
|
||||
uses: heinrichreimer/github-changelog-generator-action@v2.2
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
breakingLabel: '#### 💥 Breaking'
|
||||
enhancementLabel: '#### 🚀 Enhancements'
|
||||
bugsLabel: '#### 🐛 Bug fixes'
|
||||
deprecatedLabel: '#### ⚠️ Deprecations'
|
||||
breakingLabel: '**💥 Breaking**'
|
||||
enhancementLabel: '**🚀 Enhancements**'
|
||||
bugsLabel: '**🐛 Bug fixes**'
|
||||
deprecatedLabel: '**⚠️ Deprecations**'
|
||||
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
|
||||
issues: false
|
||||
issuesWoLabels: false
|
||||
|
|
@ -64,39 +55,76 @@ jobs:
|
|||
compareLink: true
|
||||
stripGeneratorNotice: true
|
||||
verbose: true
|
||||
futureRelease: ${{ env.RELEASE_VERSION }}
|
||||
futureRelease: ${{ steps.version.outputs.release_tag }}
|
||||
excludeTagsRegex: "CI/.+"
|
||||
releaseBranch: "main"
|
||||
|
||||
- name: "🖨️ Print changelog to console"
|
||||
run: echo ${{ steps.generate-last-changelog.outputs.changelog }}
|
||||
|
||||
- name: 💾 Commit and Tag
|
||||
id: git_commit
|
||||
if: steps.version_type.outputs.type != 'skip'
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
run: |
|
||||
git config user.email ${{ secrets.CI_EMAIL }}
|
||||
git config user.name ${{ secrets.CI_USER }}
|
||||
git add .
|
||||
git commit -m "[Automated] Release"
|
||||
tag_name="${{ env.RELEASE_VERSION }}"
|
||||
git push
|
||||
git tag -fa $tag_name -m "stable release"
|
||||
git remote set-url --push origin https://pypebot:${{ secrets.ADMIN_TOKEN }}@github.com/pypeclub/openpype
|
||||
git push origin $tag_name
|
||||
tag_name="${{ steps.version.outputs.release_tag }}"
|
||||
git tag -a $tag_name -m "stable release"
|
||||
|
||||
- name: "🚀 Github Release"
|
||||
uses: docker://antonyurchenko/git-release:latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ADMIN_TOKEN }}
|
||||
DRAFT_RELEASE: "false"
|
||||
PRE_RELEASE: "false"
|
||||
CHANGELOG_FILE: "CHANGELOG.md"
|
||||
ALLOW_EMPTY_CHANGELOG: "false"
|
||||
ALLOW_TAG_PREFIX: "true"
|
||||
- name: 🔏 Push to protected main branch
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
uses: CasperWA/push-protected@v2
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
branch: main
|
||||
tags: true
|
||||
unprotect_reviews: true
|
||||
|
||||
- name: "✏️ Generate last changelog"
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
id: generate-last-changelog
|
||||
uses: heinrichreimer/github-changelog-generator-action@v2.2
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
breakingLabel: '**💥 Breaking**'
|
||||
enhancementLabel: '**🚀 Enhancements**'
|
||||
bugsLabel: '**🐛 Bug fixes**'
|
||||
deprecatedLabel: '**⚠️ Deprecations**'
|
||||
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
|
||||
issues: false
|
||||
issuesWoLabels: false
|
||||
sinceTag: ${{ steps.version.outputs.last_release }}
|
||||
maxIssues: 100
|
||||
pullRequests: true
|
||||
prWoLabels: false
|
||||
author: false
|
||||
unreleased: true
|
||||
compareLink: true
|
||||
stripGeneratorNotice: true
|
||||
verbose: true
|
||||
futureRelease: ${{ steps.version.outputs.release_tag }}
|
||||
excludeTagsRegex: "CI/.+"
|
||||
releaseBranch: "main"
|
||||
stripHeaders: true
|
||||
base: 'none'
|
||||
|
||||
|
||||
- name: 🔨 Merge main back to develop
|
||||
- name: 🚀 Github Release
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
body: ${{ steps.generate-last-changelog.outputs.changelog }}
|
||||
tag: ${{ steps.version.outputs.release_tag }}
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
|
||||
- name: ☠ Delete Pre-release
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
uses: cb80/delrel@latest
|
||||
with:
|
||||
tag: "${{ steps.version.outputs.current_version }}"
|
||||
|
||||
- name: 🔁 Merge main back to develop
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
uses: everlytic/branch-merge@1.1.0
|
||||
if: steps.version_type.outputs.type != 'skip'
|
||||
with:
|
||||
github_token: ${{ secrets.ADMIN_TOKEN }}
|
||||
source_ref: 'main'
|
||||
|
|
|
|||
101
CHANGELOG.md
101
CHANGELOG.md
|
|
@ -1,11 +1,96 @@
|
|||
# Changelog
|
||||
|
||||
## [3.1.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.2.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...HEAD)
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD)
|
||||
|
||||
#### 🚀 Enhancements
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Settings UI copy/paste [\#1769](https://github.com/pypeclub/OpenPype/pull/1769)
|
||||
- Workfile tool widths [\#1766](https://github.com/pypeclub/OpenPype/pull/1766)
|
||||
- Push hierarchical attributes care about task parent changes [\#1763](https://github.com/pypeclub/OpenPype/pull/1763)
|
||||
- Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757)
|
||||
- Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739)
|
||||
- Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736)
|
||||
- PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726)
|
||||
- Autoupdate launcher [\#1725](https://github.com/pypeclub/OpenPype/pull/1725)
|
||||
- Subset template and TVPaint subset template docs [\#1717](https://github.com/pypeclub/OpenPype/pull/1717)
|
||||
- Toggle Ftrack upload in StandalonePublisher [\#1708](https://github.com/pypeclub/OpenPype/pull/1708)
|
||||
- Overscan color extract review [\#1701](https://github.com/pypeclub/OpenPype/pull/1701)
|
||||
- Nuke: Prerender Frame Range by default [\#1699](https://github.com/pypeclub/OpenPype/pull/1699)
|
||||
- Smoother edges of color triangle [\#1695](https://github.com/pypeclub/OpenPype/pull/1695)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- FFprobe streams order [\#1775](https://github.com/pypeclub/OpenPype/pull/1775)
|
||||
- Project specific environments [\#1767](https://github.com/pypeclub/OpenPype/pull/1767)
|
||||
- Settings UI with refresh button [\#1764](https://github.com/pypeclub/OpenPype/pull/1764)
|
||||
- Standalone publisher thumbnail extractor fix [\#1761](https://github.com/pypeclub/OpenPype/pull/1761)
|
||||
- Anatomy others templates don't cause crash [\#1758](https://github.com/pypeclub/OpenPype/pull/1758)
|
||||
- Backend acre module commit update [\#1745](https://github.com/pypeclub/OpenPype/pull/1745)
|
||||
- hiero: precollect instances failing when audio selected [\#1743](https://github.com/pypeclub/OpenPype/pull/1743)
|
||||
- Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742)
|
||||
- Nuke: fixing render creator for no selection format failing [\#1741](https://github.com/pypeclub/OpenPype/pull/1741)
|
||||
- Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737)
|
||||
- TVPaint white background on thumbnail [\#1735](https://github.com/pypeclub/OpenPype/pull/1735)
|
||||
- Ftrack missing custom attribute message [\#1734](https://github.com/pypeclub/OpenPype/pull/1734)
|
||||
- Launcher project changes [\#1733](https://github.com/pypeclub/OpenPype/pull/1733)
|
||||
- Ftrack sync status [\#1732](https://github.com/pypeclub/OpenPype/pull/1732)
|
||||
- TVPaint use layer name for default variant [\#1724](https://github.com/pypeclub/OpenPype/pull/1724)
|
||||
- Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716)
|
||||
- Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714)
|
||||
- Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711)
|
||||
- Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Bump prismjs from 1.23.0 to 1.24.0 in /website [\#1773](https://github.com/pypeclub/OpenPype/pull/1773)
|
||||
- TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755)
|
||||
- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710)
|
||||
|
||||
## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- celaction fixes [\#1754](https://github.com/pypeclub/OpenPype/pull/1754)
|
||||
- celaciton: audio subset changed data structure [\#1750](https://github.com/pypeclub/OpenPype/pull/1750)
|
||||
|
||||
## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-23)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727)
|
||||
|
||||
## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- StandalonePublisher: adding exception for adding `delete` tag to repre [\#1650](https://github.com/pypeclub/OpenPype/pull/1650)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Maya: Extract review hotfix - 2.x backport [\#1713](https://github.com/pypeclub/OpenPype/pull/1713)
|
||||
- StandalonePublisher: instance data attribute `keepSequence` [\#1668](https://github.com/pypeclub/OpenPype/pull/1668)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- 1698 Nuke: Prerender Frame Range by default [\#1709](https://github.com/pypeclub/OpenPype/pull/1709)
|
||||
|
||||
## [3.1.0](https://github.com/pypeclub/OpenPype/tree/3.1.0) (2021-06-15)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.1.0-nightly.4...3.1.0)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703)
|
||||
- Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702)
|
||||
- OpenPype style in modules [\#1694](https://github.com/pypeclub/OpenPype/pull/1694)
|
||||
- Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689)
|
||||
- \#683 - Validate Frame Range in Standalone Publisher [\#1683](https://github.com/pypeclub/OpenPype/pull/1683)
|
||||
- Hiero: old container versions identify with red color [\#1682](https://github.com/pypeclub/OpenPype/pull/1682)
|
||||
|
|
@ -13,26 +98,30 @@
|
|||
- Remove outline in stylesheet [\#1667](https://github.com/pypeclub/OpenPype/pull/1667)
|
||||
- TVPaint: Creator take layer name as default value for subset variant [\#1663](https://github.com/pypeclub/OpenPype/pull/1663)
|
||||
- TVPaint custom subset template [\#1662](https://github.com/pypeclub/OpenPype/pull/1662)
|
||||
- Editorial: conform assets validator [\#1659](https://github.com/pypeclub/OpenPype/pull/1659)
|
||||
- Feature Slack integration [\#1657](https://github.com/pypeclub/OpenPype/pull/1657)
|
||||
- Nuke - Publish simplification [\#1653](https://github.com/pypeclub/OpenPype/pull/1653)
|
||||
- StandalonePublisher: adding exception for adding `delete` tag to repre [\#1650](https://github.com/pypeclub/OpenPype/pull/1650)
|
||||
- \#1333 - added tooltip hints to Pyblish buttons [\#1649](https://github.com/pypeclub/OpenPype/pull/1649)
|
||||
|
||||
#### 🐛 Bug fixes
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707)
|
||||
- Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705)
|
||||
- Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691)
|
||||
- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687)
|
||||
- Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675)
|
||||
- Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671)
|
||||
- Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660)
|
||||
- Fix missing dbm python module [\#1652](https://github.com/pypeclub/OpenPype/pull/1652)
|
||||
- Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648)
|
||||
- Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646)
|
||||
- Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645)
|
||||
- New project anatomy values [\#1644](https://github.com/pypeclub/OpenPype/pull/1644)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697)
|
||||
- Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686)
|
||||
- Add docstrings to Project manager tool [\#1556](https://github.com/pypeclub/OpenPype/pull/1556)
|
||||
|
||||
# Changelog
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from copy import deepcopy
|
||||
import openpype.hosts.hiero.api as phiero
|
||||
# from openpype.hosts.hiero.api import plugin, lib
|
||||
# reload(lib)
|
||||
|
|
@ -206,20 +207,24 @@ class CreateShotClip(phiero.Creator):
|
|||
presets = None
|
||||
|
||||
def process(self):
|
||||
# Creator copy of object attributes that are modified during `process`
|
||||
presets = deepcopy(self.presets)
|
||||
gui_inputs = deepcopy(self.gui_inputs)
|
||||
|
||||
# get key pares from presets and match it on ui inputs
|
||||
for k, v in self.gui_inputs.items():
|
||||
for k, v in gui_inputs.items():
|
||||
if v["type"] in ("dict", "section"):
|
||||
# nested dictionary (only one level allowed
|
||||
# for sections and dict)
|
||||
for _k, _v in v["value"].items():
|
||||
if self.presets.get(_k):
|
||||
self.gui_inputs[k][
|
||||
"value"][_k]["value"] = self.presets[_k]
|
||||
if self.presets.get(k):
|
||||
self.gui_inputs[k]["value"] = self.presets[k]
|
||||
if presets.get(_k):
|
||||
gui_inputs[k][
|
||||
"value"][_k]["value"] = presets[_k]
|
||||
if presets.get(k):
|
||||
gui_inputs[k]["value"] = presets[k]
|
||||
|
||||
# open widget for plugins inputs
|
||||
widget = self.widget(self.gui_name, self.gui_info, self.gui_inputs)
|
||||
widget = self.widget(self.gui_name, self.gui_info, gui_inputs)
|
||||
widget.exec_()
|
||||
|
||||
if len(self.selected) < 1:
|
||||
|
|
|
|||
|
|
@ -46,12 +46,6 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
source_clip = track_item.source()
|
||||
self.log.debug("clip_name: {}".format(clip_name))
|
||||
|
||||
# get clips subtracks and anotations
|
||||
annotations = self.clip_annotations(source_clip)
|
||||
subtracks = self.clip_subtrack(track_item)
|
||||
self.log.debug("Annotations: {}".format(annotations))
|
||||
self.log.debug(">> Subtracks: {}".format(subtracks))
|
||||
|
||||
# get openpype tag data
|
||||
tag_data = phiero.get_track_item_pype_data(track_item)
|
||||
self.log.debug("__ tag_data: {}".format(pformat(tag_data)))
|
||||
|
|
@ -62,6 +56,12 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
if tag_data.get("id") != "pyblish.avalon.instance":
|
||||
continue
|
||||
|
||||
# get clips subtracks and anotations
|
||||
annotations = self.clip_annotations(source_clip)
|
||||
subtracks = self.clip_subtrack(track_item)
|
||||
self.log.debug("Annotations: {}".format(annotations))
|
||||
self.log.debug(">> Subtracks: {}".format(subtracks))
|
||||
|
||||
# solve handles length
|
||||
tag_data["handleStart"] = min(
|
||||
tag_data["handleStart"], int(track_item.handleInLength()))
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class ExtractPlayblast(openpype.api.Extractor):
|
|||
|
||||
# Isolate view is requested by having objects in the set besides a
|
||||
# camera.
|
||||
if preset.pop("isolate_view", False) or instance.data.get("isolate"):
|
||||
if preset.pop("isolate_view", False) and instance.data.get("isolate"):
|
||||
preset["isolate"] = instance.data["setMembers"]
|
||||
|
||||
# Show/Hide image planes on request.
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class ExtractThumbnail(openpype.api.Extractor):
|
|||
|
||||
# Isolate view is requested by having objects in the set besides a
|
||||
# camera.
|
||||
if preset.pop("isolate_view", False) or instance.data.get("isolate"):
|
||||
if preset.pop("isolate_view", False) and instance.data.get("isolate"):
|
||||
preset["isolate"] = instance.data["setMembers"]
|
||||
|
||||
with maintained_time():
|
||||
|
|
|
|||
|
|
@ -286,7 +286,8 @@ def add_button_write_to_read(node):
|
|||
node.addKnob(knob)
|
||||
|
||||
|
||||
def create_write_node(name, data, input=None, prenodes=None, review=True):
|
||||
def create_write_node(name, data, input=None, prenodes=None,
|
||||
review=True, linked_knobs=None):
|
||||
''' Creating write node which is group node
|
||||
|
||||
Arguments:
|
||||
|
|
@ -465,12 +466,16 @@ def create_write_node(name, data, input=None, prenodes=None, review=True):
|
|||
GN.addKnob(nuke.Text_Knob('', 'Rendering'))
|
||||
|
||||
# Add linked knobs.
|
||||
linked_knob_names = [
|
||||
"_grp-start_",
|
||||
"use_limit", "first", "last",
|
||||
"_grp-end_",
|
||||
"Render"
|
||||
]
|
||||
linked_knob_names = []
|
||||
|
||||
# add input linked knobs and create group only if any input
|
||||
if linked_knobs:
|
||||
linked_knob_names.append("_grp-start_")
|
||||
linked_knob_names.extend(linked_knobs)
|
||||
linked_knob_names.append("_grp-end_")
|
||||
|
||||
linked_knob_names.append("Render")
|
||||
|
||||
for name in linked_knob_names:
|
||||
if "_grp-start_" in name:
|
||||
knob = nuke.Tab_Knob(
|
||||
|
|
@ -481,13 +486,20 @@ def create_write_node(name, data, input=None, prenodes=None, review=True):
|
|||
"rnd_attr_end", "Rendering attributes", nuke.TABENDGROUP)
|
||||
GN.addKnob(knob)
|
||||
else:
|
||||
link = nuke.Link_Knob("")
|
||||
link.makeLink(write_node.name(), name)
|
||||
link.setName(name)
|
||||
if "Render" in name:
|
||||
link.setLabel("Render Local")
|
||||
link.setFlag(0x1000)
|
||||
GN.addKnob(link)
|
||||
if "___" in name:
|
||||
# add devider
|
||||
GN.addKnob(nuke.Text_Knob(""))
|
||||
else:
|
||||
# add linked knob by name
|
||||
link = nuke.Link_Knob("")
|
||||
link.makeLink(write_node.name(), name)
|
||||
link.setName(name)
|
||||
|
||||
# make render
|
||||
if "Render" in name:
|
||||
link.setLabel("Render Local")
|
||||
link.setFlag(0x1000)
|
||||
GN.addKnob(link)
|
||||
|
||||
# adding write to read button
|
||||
add_button_write_to_read(GN)
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@ class CreateWritePrerender(plugin.PypeCreator):
|
|||
write_data,
|
||||
input=selected_node,
|
||||
prenodes=[],
|
||||
review=False)
|
||||
review=False,
|
||||
linked_knobs=["channels", "___", "first", "last", "use_limit"])
|
||||
|
||||
# relinking to collected connections
|
||||
for i, input in enumerate(inputs):
|
||||
|
|
@ -122,19 +123,9 @@ class CreateWritePrerender(plugin.PypeCreator):
|
|||
w_node = n
|
||||
write_node.end()
|
||||
|
||||
# add inner write node Tab
|
||||
write_node.addKnob(nuke.Tab_Knob("WriteLinkedKnobs"))
|
||||
|
||||
# linking knobs to group property panel
|
||||
linking_knobs = ["channels", "___", "first", "last", "use_limit"]
|
||||
for k in linking_knobs:
|
||||
if "___" in k:
|
||||
write_node.addKnob(nuke.Text_Knob(''))
|
||||
else:
|
||||
lnk = nuke.Link_Knob(k)
|
||||
lnk.makeLink(w_node.name(), k)
|
||||
lnk.setName(k.replace('_', ' ').capitalize())
|
||||
lnk.clearFlag(nuke.STARTLINE)
|
||||
write_node.addKnob(lnk)
|
||||
if self.presets.get("use_range_limit"):
|
||||
w_node["use_limit"].setValue(True)
|
||||
w_node["first"].setValue(nuke.root()["first_frame"].value())
|
||||
w_node["last"].setValue(nuke.root()["last_frame"].value())
|
||||
|
||||
return write_node
|
||||
|
|
|
|||
|
|
@ -100,6 +100,13 @@ class CreateWriteRender(plugin.PypeCreator):
|
|||
"/{subset}.{frame}.{ext}")})
|
||||
|
||||
# add crop node to cut off all outside of format bounding box
|
||||
# get width and height
|
||||
try:
|
||||
width, height = (selected_node.width(), selected_node.height())
|
||||
except AttributeError:
|
||||
actual_format = nuke.root().knob('format').value()
|
||||
width, height = (actual_format.width(), actual_format.height())
|
||||
|
||||
_prenodes = [
|
||||
{
|
||||
"name": "Crop01",
|
||||
|
|
@ -108,8 +115,8 @@ class CreateWriteRender(plugin.PypeCreator):
|
|||
("box", [
|
||||
0.0,
|
||||
0.0,
|
||||
selected_node.width(),
|
||||
selected_node.height()
|
||||
width,
|
||||
height
|
||||
])
|
||||
],
|
||||
"dependent": None
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import re
|
||||
from pprint import pformat
|
||||
import nuke
|
||||
import pyblish.api
|
||||
import openpype.api as pype
|
||||
|
|
@ -17,6 +18,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
|
|||
|
||||
def process(self, instance):
|
||||
_families_test = [instance.data["family"]] + instance.data["families"]
|
||||
self.log.debug("_families_test: {}".format(_families_test))
|
||||
|
||||
node = None
|
||||
for x in instance:
|
||||
|
|
@ -133,22 +135,29 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
|
|||
"outputDir": output_dir,
|
||||
"ext": ext,
|
||||
"label": label,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"frameStart": first_frame + handle_start,
|
||||
"frameEnd": last_frame - handle_end,
|
||||
"frameStartHandle": first_frame,
|
||||
"frameEndHandle": last_frame,
|
||||
"outputType": output_type,
|
||||
"colorspace": colorspace,
|
||||
"deadlineChunkSize": deadlineChunkSize,
|
||||
"deadlinePriority": deadlinePriority
|
||||
})
|
||||
|
||||
if "prerender" in _families_test:
|
||||
if self.is_prerender(_families_test):
|
||||
instance.data.update({
|
||||
"family": "prerender",
|
||||
"families": []
|
||||
"handleStart": 0,
|
||||
"handleEnd": 0,
|
||||
"frameStart": first_frame,
|
||||
"frameEnd": last_frame,
|
||||
"frameStartHandle": first_frame,
|
||||
"frameEndHandle": last_frame,
|
||||
})
|
||||
else:
|
||||
instance.data.update({
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"frameStart": first_frame + handle_start,
|
||||
"frameEnd": last_frame - handle_end,
|
||||
"frameStartHandle": first_frame,
|
||||
"frameEndHandle": last_frame,
|
||||
})
|
||||
|
||||
# * Add audio to instance if exists.
|
||||
|
|
@ -170,4 +179,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
|
|||
"filename": api.get_representation_path(repre_doc)
|
||||
}]
|
||||
|
||||
self.log.debug("instance.data: {}".format(instance.data))
|
||||
self.log.debug("instance.data: {}".format(pformat(instance.data)))
|
||||
|
||||
def is_prerender(self, families):
|
||||
return next((f for f in families if "prerender" in f), None)
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
|
|||
hosts = ["nuke", "nukestudio"]
|
||||
actions = [RepairCollectionActionToLocal, RepairCollectionActionToFarm]
|
||||
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
for repre in instance.data["representations"]:
|
||||
|
|
@ -78,10 +77,10 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
|
|||
|
||||
collection = collections[0]
|
||||
|
||||
frame_length = int(
|
||||
instance.data["frameEndHandle"]
|
||||
- instance.data["frameStartHandle"] + 1
|
||||
)
|
||||
fstartH = instance.data["frameStartHandle"]
|
||||
fendH = instance.data["frameEndHandle"]
|
||||
|
||||
frame_length = int(fendH - fstartH + 1)
|
||||
|
||||
if frame_length != 1:
|
||||
if len(collections) != 1:
|
||||
|
|
@ -95,7 +94,16 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
|
|||
raise ValidationException(msg)
|
||||
|
||||
collected_frames_len = int(len(collection.indexes))
|
||||
coll_start = min(collection.indexes)
|
||||
coll_end = max(collection.indexes)
|
||||
|
||||
self.log.info("frame_length: {}".format(frame_length))
|
||||
self.log.info("collected_frames_len: {}".format(
|
||||
collected_frames_len))
|
||||
self.log.info("fstartH-fendH: {}-{}".format(fstartH, fendH))
|
||||
self.log.info(
|
||||
"coll_start-coll_end: {}-{}".format(coll_start, coll_end))
|
||||
|
||||
self.log.info(
|
||||
"len(collection.indexes): {}".format(collected_frames_len)
|
||||
)
|
||||
|
|
@ -103,8 +111,11 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
|
|||
if ("slate" in instance.data["families"]) \
|
||||
and (frame_length != collected_frames_len):
|
||||
collected_frames_len -= 1
|
||||
fstartH += 1
|
||||
|
||||
assert (collected_frames_len == frame_length), (
|
||||
assert ((collected_frames_len >= frame_length)
|
||||
and (coll_start <= fstartH)
|
||||
and (coll_end >= fendH)), (
|
||||
"{} missing frames. Use repair to render all frames"
|
||||
).format(__name__)
|
||||
|
||||
|
|
|
|||
26
openpype/hosts/photoshop/plugins/lib.py
Normal file
26
openpype/hosts/photoshop/plugins/lib.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import re
|
||||
|
||||
|
||||
def get_unique_layer_name(layers, asset_name, subset_name):
|
||||
"""
|
||||
Gets all layer names and if 'asset_name_subset_name' is present, it
|
||||
increases suffix by 1 (eg. creates unique layer name - for Loader)
|
||||
Args:
|
||||
layers (list) of dict with layers info (name, id etc.)
|
||||
asset_name (string):
|
||||
subset_name (string):
|
||||
|
||||
Returns:
|
||||
(string): name_00X (without version)
|
||||
"""
|
||||
name = "{}_{}".format(asset_name, subset_name)
|
||||
names = {}
|
||||
for layer in layers:
|
||||
layer_name = re.sub(r'_\d{3}$', '', layer.name)
|
||||
if layer_name in names.keys():
|
||||
names[layer_name] = names[layer_name] + 1
|
||||
else:
|
||||
names[layer_name] = 1
|
||||
occurrences = names.get(name, 0)
|
||||
|
||||
return "{}_{:0>3d}".format(name, occurrences + 1)
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
from avalon import api, photoshop
|
||||
import os
|
||||
import re
|
||||
|
||||
from avalon import api, photoshop
|
||||
|
||||
from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
|
||||
|
||||
stub = photoshop.stub()
|
||||
|
||||
|
||||
|
|
@ -15,8 +17,9 @@ class ImageLoader(api.Loader):
|
|||
representations = ["*"]
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
layer_name = self._get_unique_layer_name(context["asset"]["name"],
|
||||
name)
|
||||
layer_name = get_unique_layer_name(stub.get_layers(),
|
||||
context["asset"]["name"],
|
||||
name)
|
||||
with photoshop.maintained_selection():
|
||||
layer = stub.import_smart_object(self.fname, layer_name)
|
||||
|
||||
|
|
@ -69,25 +72,3 @@ class ImageLoader(api.Loader):
|
|||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
||||
def _get_unique_layer_name(self, asset_name, subset_name):
|
||||
"""
|
||||
Gets all layer names and if 'name' is present in them, increases
|
||||
suffix by 1 (eg. creates unique layer name - for Loader)
|
||||
Args:
|
||||
name (string): in format asset_subset
|
||||
|
||||
Returns:
|
||||
(string): name_00X (without version)
|
||||
"""
|
||||
name = "{}_{}".format(asset_name, subset_name)
|
||||
names = {}
|
||||
for layer in stub.get_layers():
|
||||
layer_name = re.sub(r'_\d{3}$', '', layer.name)
|
||||
if layer_name in names.keys():
|
||||
names[layer_name] = names[layer_name] + 1
|
||||
else:
|
||||
names[layer_name] = 1
|
||||
occurrences = names.get(name, 0)
|
||||
|
||||
return "{}_{:0>3d}".format(name, occurrences + 1)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import os
|
||||
|
||||
from avalon import api
|
||||
from avalon import photoshop
|
||||
from avalon.pipeline import get_representation_path_from_context
|
||||
from avalon.vendor import qargparse
|
||||
|
||||
from openpype.lib import Anatomy
|
||||
from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
|
||||
|
||||
stub = photoshop.stub()
|
||||
|
||||
|
||||
class ImageFromSequenceLoader(api.Loader):
|
||||
""" Load specifing image from sequence
|
||||
|
||||
Used only as quick load of reference file from a sequence.
|
||||
|
||||
Plain ImageLoader picks first frame from sequence.
|
||||
|
||||
Loads only existing files - currently not possible to limit loaders
|
||||
to single select - multiselect. If user selects multiple repres, list
|
||||
for all of them is provided, but selection is only single file.
|
||||
This loader will be triggered multiple times, but selected name will
|
||||
match only to proper path.
|
||||
|
||||
Loader doesnt do containerization as there is currently no data model
|
||||
of 'frame of rendered files' (only rendered sequence), update would be
|
||||
difficult.
|
||||
"""
|
||||
|
||||
families = ["render"]
|
||||
representations = ["*"]
|
||||
options = []
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
if data.get("frame"):
|
||||
self.fname = os.path.join(os.path.dirname(self.fname),
|
||||
data["frame"])
|
||||
if not os.path.exists(self.fname):
|
||||
return
|
||||
|
||||
stub = photoshop.stub()
|
||||
layer_name = get_unique_layer_name(stub.get_layers(),
|
||||
context["asset"]["name"],
|
||||
name)
|
||||
|
||||
with photoshop.maintained_selection():
|
||||
layer = stub.import_smart_object(self.fname, layer_name)
|
||||
|
||||
self[:] = [layer]
|
||||
namespace = namespace or layer_name
|
||||
|
||||
return namespace
|
||||
|
||||
@classmethod
|
||||
def get_options(cls, repre_contexts):
|
||||
"""
|
||||
Returns list of files for selected 'repre_contexts'.
|
||||
|
||||
It returns only files with same extension as in context as it is
|
||||
expected that context points to sequence of frames.
|
||||
|
||||
Returns:
|
||||
(list) of qargparse.Choice
|
||||
"""
|
||||
files = []
|
||||
for context in repre_contexts:
|
||||
fname = get_representation_path_from_context(context)
|
||||
_, file_extension = os.path.splitext(fname)
|
||||
|
||||
for file_name in os.listdir(os.path.dirname(fname)):
|
||||
if not file_name.endswith(file_extension):
|
||||
continue
|
||||
files.append(file_name)
|
||||
|
||||
# return selection only if there is something
|
||||
if not files or len(files) <= 1:
|
||||
return []
|
||||
|
||||
return [
|
||||
qargparse.Choice(
|
||||
"frame",
|
||||
label="Select specific file",
|
||||
items=files,
|
||||
default=0,
|
||||
help="Which frame should be loaded?"
|
||||
)
|
||||
]
|
||||
|
||||
def update(self, container, representation):
|
||||
"""No update possible, not containerized."""
|
||||
pass
|
||||
|
||||
def remove(self, container):
|
||||
"""No update possible, not containerized."""
|
||||
pass
|
||||
|
||||
|
|
@ -34,7 +34,6 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
|
|||
|
||||
# presets
|
||||
batch_extensions = ["edl", "xml", "psd"]
|
||||
default_families = ["ftrack"]
|
||||
|
||||
def process(self, context):
|
||||
# get json paths from os and load them
|
||||
|
|
@ -213,10 +212,6 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
|
|||
subset = in_data["subset"]
|
||||
# If instance data already contain families then use it
|
||||
instance_families = in_data.get("families") or []
|
||||
# Make sure default families are in instance
|
||||
for default_family in self.default_families or []:
|
||||
if default_family not in instance_families:
|
||||
instance_families.append(default_family)
|
||||
|
||||
instance = context.create_instance(subset)
|
||||
instance.data.update(
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ class CollectInstances(pyblish.api.InstancePlugin):
|
|||
subsets = {
|
||||
"referenceMain": {
|
||||
"family": "review",
|
||||
"families": ["clip", "ftrack"],
|
||||
"families": ["clip"],
|
||||
"extensions": [".mp4"]
|
||||
},
|
||||
"audioMain": {
|
||||
"family": "audio",
|
||||
"families": ["clip", "ftrack"],
|
||||
"families": ["clip"],
|
||||
"extensions": [".wav"],
|
||||
},
|
||||
"shotMain": {
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
"""
|
||||
Requires:
|
||||
Nothing
|
||||
|
||||
Provides:
|
||||
Instance
|
||||
"""
|
||||
|
||||
import pyblish.api
|
||||
import logging
|
||||
|
||||
|
||||
log = logging.getLogger("collector")
|
||||
|
||||
|
||||
class CollectMatchmovePublish(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Collector with only one reason for its existence - remove 'ftrack'
|
||||
family implicitly added by Standalone Publisher
|
||||
"""
|
||||
|
||||
label = "Collect Matchmove - SA Publish"
|
||||
order = pyblish.api.CollectorOrder
|
||||
families = ["matchmove"]
|
||||
hosts = ["standalonepublisher"]
|
||||
|
||||
def process(self, instance):
|
||||
if "ftrack" in instance.data["families"]:
|
||||
instance.data["families"].remove("ftrack")
|
||||
|
|
@ -66,7 +66,6 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
|
|||
else:
|
||||
# Convert to jpeg if not yet
|
||||
full_input_path = os.path.join(thumbnail_repre["stagingDir"], file)
|
||||
full_input_path = '"{}"'.format(full_input_path)
|
||||
self.log.info("input {}".format(full_input_path))
|
||||
|
||||
full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1]
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@ class ValidateFrameRange(pyblish.api.InstancePlugin):
|
|||
self.log.warning("Cannot check for extension {}".format(ext))
|
||||
return
|
||||
|
||||
frames = len(instance.data.get("representations", [None])[0]["files"])
|
||||
files = instance.data.get("representations", [None])[0]["files"]
|
||||
if isinstance(files, str):
|
||||
files = [files]
|
||||
frames = len(files)
|
||||
|
||||
err_msg = "Frame duration from DB:'{}' ". format(int(duration)) +\
|
||||
" doesn't match number of files:'{}'".format(frames) +\
|
||||
|
|
|
|||
|
|
@ -58,18 +58,14 @@ class CreateRenderlayer(plugin.Creator):
|
|||
# Get currently selected layers
|
||||
layers_data = lib.layers_data()
|
||||
|
||||
group_ids = set()
|
||||
for layer in layers_data:
|
||||
if layer["selected"]:
|
||||
group_ids.add(layer["group_id"])
|
||||
|
||||
selected_layers = [
|
||||
layer
|
||||
for layer in layers_data
|
||||
if layer["selected"]
|
||||
]
|
||||
# Return layer name if only one is selected
|
||||
if len(group_ids) == 1:
|
||||
group_id = list(group_ids)[0]
|
||||
groups_data = lib.groups_data()
|
||||
for group in groups_data:
|
||||
if group["group_id"] == group_id:
|
||||
return group["name"]
|
||||
if len(selected_layers) == 1:
|
||||
return selected_layers[0]["name"]
|
||||
|
||||
# Use defaults
|
||||
if cls.defaults:
|
||||
|
|
|
|||
|
|
@ -103,8 +103,6 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
instance.data["layers"] = copy.deepcopy(
|
||||
context.data["layersData"]
|
||||
)
|
||||
# Add ftrack family
|
||||
instance.data["families"].append("ftrack")
|
||||
|
||||
elif family == "renderLayer":
|
||||
instance = self.create_render_layer_instance(
|
||||
|
|
@ -186,9 +184,6 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
instance_data["layers"] = group_layers
|
||||
|
||||
# Add ftrack family
|
||||
instance_data["families"].append("ftrack")
|
||||
|
||||
return context.create_instance(**instance_data)
|
||||
|
||||
def create_render_pass_instance(self, context, instance_data):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import json
|
||||
import tempfile
|
||||
|
||||
import pyblish.api
|
||||
import avalon.api
|
||||
|
|
@ -153,9 +154,45 @@ class CollectWorkfileData(pyblish.api.ContextPlugin):
|
|||
"sceneMarkIn": int(mark_in_frame),
|
||||
"sceneMarkInState": mark_in_state == "set",
|
||||
"sceneMarkOut": int(mark_out_frame),
|
||||
"sceneMarkOutState": mark_out_state == "set"
|
||||
"sceneMarkOutState": mark_out_state == "set",
|
||||
"sceneBgColor": self._get_bg_color()
|
||||
}
|
||||
self.log.debug(
|
||||
"Scene data: {}".format(json.dumps(scene_data, indent=4))
|
||||
)
|
||||
context.data.update(scene_data)
|
||||
|
||||
def _get_bg_color(self):
|
||||
"""Background color set on scene.
|
||||
|
||||
Is important for review exporting where scene bg color is used as
|
||||
background.
|
||||
"""
|
||||
output_file = tempfile.NamedTemporaryFile(
|
||||
mode="w", prefix="a_tvp_", suffix=".txt", delete=False
|
||||
)
|
||||
output_file.close()
|
||||
output_filepath = output_file.name.replace("\\", "/")
|
||||
george_script_lines = [
|
||||
# Variable containing full path to output file
|
||||
"output_path = \"{}\"".format(output_filepath),
|
||||
"tv_background",
|
||||
"bg_color = result",
|
||||
# Write data to output file
|
||||
(
|
||||
"tv_writetextfile"
|
||||
" \"strict\" \"append\" '\"'output_path'\"' bg_color"
|
||||
)
|
||||
]
|
||||
|
||||
george_script = "\n".join(george_script_lines)
|
||||
lib.execute_george_through_file(george_script)
|
||||
|
||||
with open(output_filepath, "r") as stream:
|
||||
data = stream.read()
|
||||
|
||||
os.remove(output_filepath)
|
||||
data = data.strip()
|
||||
if not data:
|
||||
return None
|
||||
return data.split(" ")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import shutil
|
||||
import copy
|
||||
import tempfile
|
||||
|
||||
import pyblish.api
|
||||
|
|
@ -13,6 +14,9 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
hosts = ["tvpaint"]
|
||||
families = ["review", "renderPass", "renderLayer"]
|
||||
|
||||
# Modifiable with settings
|
||||
review_bg = [255, 255, 255, 255]
|
||||
|
||||
def process(self, instance):
|
||||
self.log.info(
|
||||
"* Processing instance \"{}\"".format(instance.data["label"])
|
||||
|
|
@ -53,6 +57,8 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
handle_start = instance.context.data["handleStart"]
|
||||
handle_end = instance.context.data["handleEnd"]
|
||||
|
||||
scene_bg_color = instance.context.data["sceneBgColor"]
|
||||
|
||||
# --- Fallbacks ----------------------------------------------------
|
||||
# This is required if validations of ranges are ignored.
|
||||
# - all of this code won't change processing if range to render
|
||||
|
|
@ -120,7 +126,8 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
|
||||
if instance.data["family"] == "review":
|
||||
output_filenames, thumbnail_fullpath = self.render_review(
|
||||
filename_template, output_dir, mark_in, mark_out
|
||||
filename_template, output_dir, mark_in, mark_out,
|
||||
scene_bg_color
|
||||
)
|
||||
else:
|
||||
# Render output
|
||||
|
|
@ -241,7 +248,9 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
for path in repre_filepaths
|
||||
]
|
||||
|
||||
def render_review(self, filename_template, output_dir, mark_in, mark_out):
|
||||
def render_review(
|
||||
self, filename_template, output_dir, mark_in, mark_out, scene_bg_color
|
||||
):
|
||||
""" Export images from TVPaint using `tv_savesequence` command.
|
||||
|
||||
Args:
|
||||
|
|
@ -252,6 +261,8 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
output_dir (str): Directory where files will be stored.
|
||||
mark_in (int): Starting frame index from which export will begin.
|
||||
mark_out (int): On which frame index export will end.
|
||||
scene_bg_color (list): Bg color set in scene. Result of george
|
||||
script command `tv_background`.
|
||||
|
||||
Retruns:
|
||||
tuple: With 2 items first is list of filenames second is path to
|
||||
|
|
@ -263,7 +274,11 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
filename_template.format(frame=mark_in)
|
||||
)
|
||||
|
||||
bg_color = self._get_review_bg_color()
|
||||
|
||||
george_script_lines = [
|
||||
# Change bg color to color from settings
|
||||
"tv_background \"color\" {} {} {}".format(*bg_color),
|
||||
"tv_SaveMode \"PNG\"",
|
||||
"export_path = \"{}\"".format(
|
||||
first_frame_filepath.replace("\\", "/")
|
||||
|
|
@ -272,6 +287,18 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
mark_in, mark_out
|
||||
)
|
||||
]
|
||||
if scene_bg_color:
|
||||
# Change bg color back to previous scene bg color
|
||||
_scene_bg_color = copy.deepcopy(scene_bg_color)
|
||||
bg_type = _scene_bg_color.pop(0)
|
||||
orig_color_command = [
|
||||
"tv_background",
|
||||
"\"{}\"".format(bg_type)
|
||||
]
|
||||
orig_color_command.extend(_scene_bg_color)
|
||||
|
||||
george_script_lines.append(" ".join(orig_color_command))
|
||||
|
||||
lib.execute_george_through_file("\n".join(george_script_lines))
|
||||
|
||||
first_frame_filepath = None
|
||||
|
|
@ -291,12 +318,13 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
if first_frame_filepath is None:
|
||||
first_frame_filepath = filepath
|
||||
|
||||
thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg")
|
||||
thumbnail_filepath = None
|
||||
if first_frame_filepath and os.path.exists(first_frame_filepath):
|
||||
thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg")
|
||||
source_img = Image.open(first_frame_filepath)
|
||||
thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255))
|
||||
thumbnail_obj.paste(source_img)
|
||||
thumbnail_obj.save(thumbnail_filepath)
|
||||
if source_img.mode.lower() != "rgb":
|
||||
source_img = source_img.convert("RGB")
|
||||
source_img.save(thumbnail_filepath)
|
||||
|
||||
return output_filenames, thumbnail_filepath
|
||||
|
||||
|
|
@ -392,12 +420,35 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
if thumbnail_src_filepath and os.path.exists(thumbnail_src_filepath):
|
||||
source_img = Image.open(thumbnail_src_filepath)
|
||||
thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg")
|
||||
thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255))
|
||||
thumbnail_obj.paste(source_img)
|
||||
thumbnail_obj.save(thumbnail_filepath)
|
||||
# Composite background only on rgba images
|
||||
# - just making sure
|
||||
if source_img.mode.lower() == "rgba":
|
||||
bg_color = self._get_review_bg_color()
|
||||
self.log.debug("Adding thumbnail background color {}.".format(
|
||||
" ".join([str(val) for val in bg_color])
|
||||
))
|
||||
bg_image = Image.new("RGBA", source_img.size, bg_color)
|
||||
thumbnail_obj = Image.alpha_composite(bg_image, source_img)
|
||||
thumbnail_obj.convert("RGB").save(thumbnail_filepath)
|
||||
|
||||
else:
|
||||
self.log.info((
|
||||
"Source for thumbnail has mode \"{}\" (Expected: RGBA)."
|
||||
" Can't use thubmanail background color."
|
||||
).format(source_img.mode))
|
||||
source_img.save(thumbnail_filepath)
|
||||
|
||||
return output_filenames, thumbnail_filepath
|
||||
|
||||
def _get_review_bg_color(self):
|
||||
red = green = blue = 255
|
||||
if self.review_bg:
|
||||
if len(self.review_bg) == 4:
|
||||
red, green, blue, _ = self.review_bg
|
||||
elif len(self.review_bg) == 3:
|
||||
red, green, blue = self.review_bg
|
||||
return (red, green, blue)
|
||||
|
||||
def _render_layer(
|
||||
self,
|
||||
layer,
|
||||
|
|
|
|||
|
|
@ -733,6 +733,9 @@ class Templates:
|
|||
continue
|
||||
default_key_values[key] = templates.pop(key)
|
||||
|
||||
# Pop "others" key before before expected keys are processed
|
||||
other_templates = templates.pop("others") or {}
|
||||
|
||||
keys_by_subkey = {}
|
||||
for sub_key, sub_value in templates.items():
|
||||
key_values = {}
|
||||
|
|
@ -740,7 +743,6 @@ class Templates:
|
|||
key_values.update(sub_value)
|
||||
keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values)
|
||||
|
||||
other_templates = templates.get("others") or {}
|
||||
for sub_key, sub_value in other_templates.items():
|
||||
if sub_key in keys_by_subkey:
|
||||
log.warning((
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ class Application:
|
|||
if group.enabled:
|
||||
enabled = data.get("enabled", True)
|
||||
self.enabled = enabled
|
||||
self.use_python_2 = data["use_python_2"]
|
||||
self.use_python_2 = data.get("use_python_2", False)
|
||||
|
||||
self.label = data.get("variant_label") or name
|
||||
self.full_name = "/".join((group.name, name))
|
||||
|
|
@ -191,26 +191,32 @@ class Application:
|
|||
self.full_label = full_label
|
||||
self._environment = data.get("environment") or {}
|
||||
|
||||
arguments = data.get("arguments")
|
||||
if isinstance(arguments, dict):
|
||||
arguments = arguments.get(platform.system().lower())
|
||||
|
||||
if not arguments:
|
||||
arguments = []
|
||||
self.arguments = arguments
|
||||
|
||||
if "executables" not in data:
|
||||
self.executables = [
|
||||
UndefinedApplicationExecutable()
|
||||
]
|
||||
return
|
||||
|
||||
_executables = data["executables"]
|
||||
if isinstance(_executables, dict):
|
||||
_executables = _executables.get(platform.system().lower())
|
||||
|
||||
if not _executables:
|
||||
_executables = []
|
||||
|
||||
elif isinstance(_executables, dict):
|
||||
_executables = _executables.get(platform.system().lower()) or []
|
||||
|
||||
_arguments = data["arguments"]
|
||||
if not _arguments:
|
||||
_arguments = []
|
||||
|
||||
elif isinstance(_arguments, dict):
|
||||
_arguments = _arguments.get(platform.system().lower()) or []
|
||||
|
||||
executables = []
|
||||
for executable in _executables:
|
||||
executables.append(ApplicationExecutable(executable))
|
||||
|
||||
self.executables = executables
|
||||
self.arguments = _arguments
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}> - {}".format(self.__class__.__name__, self.full_name)
|
||||
|
|
@ -454,6 +460,12 @@ class ApplicationExecutable:
|
|||
if os.path.exists(_executable):
|
||||
executable = _executable
|
||||
|
||||
# Try to format executable with environments
|
||||
try:
|
||||
executable = executable.format(**os.environ)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.executable_path = executable
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -484,6 +496,27 @@ class ApplicationExecutable:
|
|||
return bool(self._realpath())
|
||||
|
||||
|
||||
class UndefinedApplicationExecutable(ApplicationExecutable):
|
||||
"""Some applications do not require executable path from settings.
|
||||
|
||||
In that case this class is used to "fake" existing executable.
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}>".format(self.__class__.__name__)
|
||||
|
||||
def as_args(self):
|
||||
return []
|
||||
|
||||
def exists(self):
|
||||
return True
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class LaunchHook:
|
||||
"""Abstract base class of launch hook."""
|
||||
|
|
@ -1126,6 +1159,9 @@ def prepare_host_environments(data, implementation_envs=True):
|
|||
def apply_project_environments_value(project_name, env, project_settings=None):
|
||||
"""Apply project specific environments on passed environments.
|
||||
|
||||
The enviornments are applied on passed `env` argument value so it is not
|
||||
required to apply changes back.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project for which environemnts should be
|
||||
received.
|
||||
|
|
@ -1134,6 +1170,9 @@ def apply_project_environments_value(project_name, env, project_settings=None):
|
|||
project_settings (dict): Project settings for passed project name.
|
||||
Optional if project settings are already prepared.
|
||||
|
||||
Returns:
|
||||
dict: Passed env values with applied project environments.
|
||||
|
||||
Raises:
|
||||
KeyError: If project settings do not contain keys for project specific
|
||||
environments.
|
||||
|
|
@ -1144,10 +1183,9 @@ def apply_project_environments_value(project_name, env, project_settings=None):
|
|||
project_settings = get_project_settings(project_name)
|
||||
|
||||
env_value = project_settings["global"]["project_environments"]
|
||||
if not env_value:
|
||||
return env
|
||||
parsed = acre.parse(env_value)
|
||||
return _merge_env(parsed, env)
|
||||
if env_value:
|
||||
env.update(_merge_env(acre.parse(env_value), env))
|
||||
return env
|
||||
|
||||
|
||||
def prepare_context_environments(data):
|
||||
|
|
@ -1176,9 +1214,8 @@ def prepare_context_environments(data):
|
|||
|
||||
# Load project specific environments
|
||||
project_name = project_doc["name"]
|
||||
data["env"] = apply_project_environments_value(
|
||||
project_name, data["env"]
|
||||
)
|
||||
# Apply project specific environments on current env value
|
||||
apply_project_environments_value(project_name, data["env"])
|
||||
|
||||
app = data["app"]
|
||||
workdir_data = get_workdir_data(
|
||||
|
|
|
|||
|
|
@ -89,8 +89,13 @@ def ffprobe_streams(path_to_file, logger=None):
|
|||
|
||||
popen_stdout, popen_stderr = popen.communicate()
|
||||
if popen_stdout:
|
||||
logger.debug("ffprobe stdout: {}".format(popen_stdout))
|
||||
logger.debug("FFprobe stdout:\n{}".format(
|
||||
popen_stdout.decode("utf-8")
|
||||
))
|
||||
|
||||
if popen_stderr:
|
||||
logger.debug("ffprobe stderr: {}".format(popen_stderr))
|
||||
logger.warning("FFprobe stderr:\n{}".format(
|
||||
popen_stderr.decode("utf-8")
|
||||
))
|
||||
|
||||
return json.loads(popen_stdout)["streams"]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import collections
|
|||
import datetime
|
||||
|
||||
import ftrack_api
|
||||
from openpype.modules.ftrack.lib import BaseEvent
|
||||
from openpype.modules.ftrack.lib import (
|
||||
BaseEvent,
|
||||
query_custom_attributes
|
||||
)
|
||||
|
||||
|
||||
class PushFrameValuesToTaskEvent(BaseEvent):
|
||||
|
|
@ -55,10 +58,6 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
if entity_info.get("entityType") != "task":
|
||||
continue
|
||||
|
||||
# Skip `Task` entity type
|
||||
if entity_info["entity_type"].lower() == "task":
|
||||
continue
|
||||
|
||||
# Care only about changes of status
|
||||
changes = entity_info.get("changes")
|
||||
if not changes:
|
||||
|
|
@ -74,6 +73,14 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
if project_id is None:
|
||||
continue
|
||||
|
||||
# Skip `Task` entity type if parent didn't change
|
||||
if entity_info["entity_type"].lower() == "task":
|
||||
if (
|
||||
"parent_id" not in changes
|
||||
or changes["parent_id"]["new"] is None
|
||||
):
|
||||
continue
|
||||
|
||||
if project_id not in entities_info_by_project_id:
|
||||
entities_info_by_project_id[project_id] = []
|
||||
entities_info_by_project_id[project_id].append(entity_info)
|
||||
|
|
@ -117,11 +124,24 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
))
|
||||
return
|
||||
|
||||
interest_attributes = set(interest_attributes)
|
||||
interest_entity_types = set(interest_entity_types)
|
||||
|
||||
# Separate value changes and task parent changes
|
||||
_entities_info = []
|
||||
task_parent_changes = []
|
||||
for entity_info in entities_info:
|
||||
if entity_info["entity_type"].lower() == "task":
|
||||
task_parent_changes.append(entity_info)
|
||||
else:
|
||||
_entities_info.append(entity_info)
|
||||
entities_info = _entities_info
|
||||
|
||||
# Filter entities info with changes
|
||||
interesting_data, changed_keys_by_object_id = self.filter_changes(
|
||||
session, event, entities_info, interest_attributes
|
||||
)
|
||||
if not interesting_data:
|
||||
if not interesting_data and not task_parent_changes:
|
||||
return
|
||||
|
||||
# Prepare object types
|
||||
|
|
@ -131,6 +151,289 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
name_low = object_type["name"].lower()
|
||||
object_types_by_name[name_low] = object_type
|
||||
|
||||
# NOTE it would be nice to check if `interesting_data` do not contain
|
||||
# value changs of tasks that were created or moved
|
||||
# - it is a complex way how to find out
|
||||
if interesting_data:
|
||||
self.process_attribute_changes(
|
||||
session, object_types_by_name,
|
||||
interesting_data, changed_keys_by_object_id,
|
||||
interest_entity_types, interest_attributes
|
||||
)
|
||||
|
||||
if task_parent_changes:
|
||||
self.process_task_parent_change(
|
||||
session, object_types_by_name, task_parent_changes,
|
||||
interest_entity_types, interest_attributes
|
||||
)
|
||||
|
||||
def process_task_parent_change(
|
||||
self, session, object_types_by_name, task_parent_changes,
|
||||
interest_entity_types, interest_attributes
|
||||
):
|
||||
"""Push custom attribute values if task parent has changed.
|
||||
|
||||
Parent is changed if task is created or if is moved under different
|
||||
entity. We don't care about all task changes only about those that
|
||||
have it's parent in interest types (from settings).
|
||||
|
||||
Tasks hierarchical value should be unset or set based on parents
|
||||
real hierarchical value and non hierarchical custom attribute value
|
||||
should be set to hierarchical value.
|
||||
"""
|
||||
# Store task ids which were created or moved under parent with entity
|
||||
# type defined in settings (interest_entity_types).
|
||||
task_ids = set()
|
||||
# Store parent ids of matching task ids
|
||||
matching_parent_ids = set()
|
||||
# Store all entity ids of all entities to be able query hierarchical
|
||||
# values.
|
||||
whole_hierarchy_ids = set()
|
||||
# Store parent id of each entity id
|
||||
parent_id_by_entity_id = {}
|
||||
for entity_info in task_parent_changes:
|
||||
# Ignore entities with less parents than 2
|
||||
# NOTE entity itself is also part of "parents" value
|
||||
parents = entity_info.get("parents") or []
|
||||
if len(parents) < 2:
|
||||
continue
|
||||
|
||||
parent_info = parents[1]
|
||||
# Check if parent has entity type we care about.
|
||||
if parent_info["entity_type"] not in interest_entity_types:
|
||||
continue
|
||||
|
||||
task_ids.add(entity_info["entityId"])
|
||||
matching_parent_ids.add(parent_info["entityId"])
|
||||
|
||||
# Store whole hierarchi of task entity
|
||||
prev_id = None
|
||||
for item in parents:
|
||||
item_id = item["entityId"]
|
||||
whole_hierarchy_ids.add(item_id)
|
||||
|
||||
if prev_id is None:
|
||||
prev_id = item_id
|
||||
continue
|
||||
|
||||
parent_id_by_entity_id[prev_id] = item_id
|
||||
if item["entityType"] == "show":
|
||||
break
|
||||
prev_id = item_id
|
||||
|
||||
# Just skip if nothing is interesting for our settings
|
||||
if not matching_parent_ids:
|
||||
return
|
||||
|
||||
# Query object type ids of parent ids for custom attribute
|
||||
# definitions query
|
||||
entities = session.query(
|
||||
"select object_type_id from TypedContext where id in ({})".format(
|
||||
self.join_query_keys(matching_parent_ids)
|
||||
)
|
||||
)
|
||||
|
||||
# Prepare task object id
|
||||
task_object_id = object_types_by_name["task"]["id"]
|
||||
|
||||
# All object ids for which we're querying custom attribute definitions
|
||||
object_type_ids = set()
|
||||
object_type_ids.add(task_object_id)
|
||||
for entity in entities:
|
||||
object_type_ids.add(entity["object_type_id"])
|
||||
|
||||
attrs_by_obj_id, hier_attrs = self.attrs_configurations(
|
||||
session, object_type_ids, interest_attributes
|
||||
)
|
||||
|
||||
# Skip if all task attributes are not available
|
||||
task_attrs = attrs_by_obj_id.get(task_object_id)
|
||||
if not task_attrs:
|
||||
return
|
||||
|
||||
# Skip attributes that is not in both hierarchical and nonhierarchical
|
||||
# TODO be able to push values if hierarchical is available
|
||||
for key in interest_attributes:
|
||||
if key not in hier_attrs:
|
||||
task_attrs.pop(key, None)
|
||||
|
||||
elif key not in task_attrs:
|
||||
hier_attrs.pop(key)
|
||||
|
||||
# Skip if nothing remained
|
||||
if not task_attrs:
|
||||
return
|
||||
|
||||
# Do some preparations for custom attribute values query
|
||||
attr_key_by_id = {}
|
||||
nonhier_id_by_key = {}
|
||||
hier_attr_ids = []
|
||||
for key, attr_id in hier_attrs.items():
|
||||
attr_key_by_id[attr_id] = key
|
||||
hier_attr_ids.append(attr_id)
|
||||
|
||||
conf_ids = list(hier_attr_ids)
|
||||
for key, attr_id in task_attrs.items():
|
||||
attr_key_by_id[attr_id] = key
|
||||
nonhier_id_by_key[key] = attr_id
|
||||
conf_ids.append(attr_id)
|
||||
|
||||
# Query custom attribute values
|
||||
# - result does not contain values for all entities only result of
|
||||
# query callback to ftrack server
|
||||
result = query_custom_attributes(
|
||||
session, conf_ids, whole_hierarchy_ids
|
||||
)
|
||||
|
||||
# Prepare variables where result will be stored
|
||||
# - hierachical values should not contain attribute with value by
|
||||
# default
|
||||
hier_values_by_entity_id = {
|
||||
entity_id: {}
|
||||
for entity_id in whole_hierarchy_ids
|
||||
}
|
||||
# - real values of custom attributes
|
||||
values_by_entity_id = {
|
||||
entity_id: {
|
||||
attr_id: None
|
||||
for attr_id in conf_ids
|
||||
}
|
||||
for entity_id in whole_hierarchy_ids
|
||||
}
|
||||
for item in result:
|
||||
attr_id = item["configuration_id"]
|
||||
entity_id = item["entity_id"]
|
||||
value = item["value"]
|
||||
|
||||
values_by_entity_id[entity_id][attr_id] = value
|
||||
|
||||
if attr_id in hier_attr_ids and value is not None:
|
||||
hier_values_by_entity_id[entity_id][attr_id] = value
|
||||
|
||||
# Prepare values for all task entities
|
||||
# - going through all parents and storing first value value
|
||||
# - store None to those that are already known that do not have set
|
||||
# value at all
|
||||
for task_id in tuple(task_ids):
|
||||
for attr_id in hier_attr_ids:
|
||||
entity_ids = []
|
||||
value = None
|
||||
entity_id = task_id
|
||||
while value is None:
|
||||
entity_value = hier_values_by_entity_id[entity_id]
|
||||
if attr_id in entity_value:
|
||||
value = entity_value[attr_id]
|
||||
if value is None:
|
||||
break
|
||||
|
||||
if value is None:
|
||||
entity_ids.append(entity_id)
|
||||
|
||||
entity_id = parent_id_by_entity_id.get(entity_id)
|
||||
if entity_id is None:
|
||||
break
|
||||
|
||||
for entity_id in entity_ids:
|
||||
hier_values_by_entity_id[entity_id][attr_id] = value
|
||||
|
||||
# Prepare changes to commit
|
||||
changes = []
|
||||
for task_id in tuple(task_ids):
|
||||
parent_id = parent_id_by_entity_id[task_id]
|
||||
for attr_id in hier_attr_ids:
|
||||
attr_key = attr_key_by_id[attr_id]
|
||||
nonhier_id = nonhier_id_by_key[attr_key]
|
||||
|
||||
# Real value of hierarchical attribute on parent
|
||||
# - If is none then should be unset
|
||||
real_parent_value = values_by_entity_id[parent_id][attr_id]
|
||||
# Current hierarchical value of a task
|
||||
# - Will be compared to real parent value
|
||||
hier_value = hier_values_by_entity_id[task_id][attr_id]
|
||||
|
||||
# Parent value that can be inherited from it's parent entity
|
||||
parent_value = hier_values_by_entity_id[parent_id][attr_id]
|
||||
# Task value of nonhierarchical custom attribute
|
||||
nonhier_value = values_by_entity_id[task_id][nonhier_id]
|
||||
|
||||
if real_parent_value != hier_value:
|
||||
changes.append({
|
||||
"new_value": real_parent_value,
|
||||
"attr_id": attr_id,
|
||||
"entity_id": task_id,
|
||||
"attr_key": attr_key
|
||||
})
|
||||
|
||||
if parent_value != nonhier_value:
|
||||
changes.append({
|
||||
"new_value": parent_value,
|
||||
"attr_id": nonhier_id,
|
||||
"entity_id": task_id,
|
||||
"attr_key": attr_key
|
||||
})
|
||||
|
||||
self._commit_changes(session, changes)
|
||||
|
||||
def _commit_changes(self, session, changes):
|
||||
uncommited_changes = False
|
||||
for idx, item in enumerate(changes):
|
||||
new_value = item["new_value"]
|
||||
attr_id = item["attr_id"]
|
||||
entity_id = item["entity_id"]
|
||||
attr_key = item["attr_key"]
|
||||
|
||||
entity_key = collections.OrderedDict()
|
||||
entity_key["configuration_id"] = attr_id
|
||||
entity_key["entity_id"] = entity_id
|
||||
self._cached_changes.append({
|
||||
"attr_key": attr_key,
|
||||
"entity_id": entity_id,
|
||||
"value": new_value,
|
||||
"time": datetime.datetime.now()
|
||||
})
|
||||
if new_value is None:
|
||||
op = ftrack_api.operation.DeleteEntityOperation(
|
||||
"CustomAttributeValue",
|
||||
entity_key
|
||||
)
|
||||
else:
|
||||
op = ftrack_api.operation.UpdateEntityOperation(
|
||||
"ContextCustomAttributeValue",
|
||||
entity_key,
|
||||
"value",
|
||||
ftrack_api.symbol.NOT_SET,
|
||||
new_value
|
||||
)
|
||||
|
||||
session.recorded_operations.push(op)
|
||||
self.log.info((
|
||||
"Changing Custom Attribute \"{}\" to value"
|
||||
" \"{}\" on entity: {}"
|
||||
).format(attr_key, new_value, entity_id))
|
||||
|
||||
if (idx + 1) % 20 == 0:
|
||||
uncommited_changes = False
|
||||
try:
|
||||
session.commit()
|
||||
except Exception:
|
||||
session.rollback()
|
||||
self.log.warning(
|
||||
"Changing of values failed.", exc_info=True
|
||||
)
|
||||
else:
|
||||
uncommited_changes = True
|
||||
if uncommited_changes:
|
||||
try:
|
||||
session.commit()
|
||||
except Exception:
|
||||
session.rollback()
|
||||
self.log.warning("Changing of values failed.", exc_info=True)
|
||||
|
||||
def process_attribute_changes(
|
||||
self, session, object_types_by_name,
|
||||
interesting_data, changed_keys_by_object_id,
|
||||
interest_entity_types, interest_attributes
|
||||
):
|
||||
# Prepare task object id
|
||||
task_object_id = object_types_by_name["task"]["id"]
|
||||
|
||||
|
|
@ -216,13 +519,13 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
task_entity_ids.add(task_id)
|
||||
parent_id_by_task_id[task_id] = task_entity["parent_id"]
|
||||
|
||||
self.finalize(
|
||||
self.finalize_attribute_changes(
|
||||
session, interesting_data,
|
||||
changed_keys, attrs_by_obj_id, hier_attrs,
|
||||
task_entity_ids, parent_id_by_task_id
|
||||
)
|
||||
|
||||
def finalize(
|
||||
def finalize_attribute_changes(
|
||||
self, session, interesting_data,
|
||||
changed_keys, attrs_by_obj_id, hier_attrs,
|
||||
task_entity_ids, parent_id_by_task_id
|
||||
|
|
@ -248,6 +551,7 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
session, attr_ids, entity_ids, task_entity_ids, hier_attrs
|
||||
)
|
||||
|
||||
changes = []
|
||||
for entity_id, current_values in current_values_by_id.items():
|
||||
parent_id = parent_id_by_task_id.get(entity_id)
|
||||
if not parent_id:
|
||||
|
|
@ -272,39 +576,13 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
if new_value == old_value:
|
||||
continue
|
||||
|
||||
entity_key = collections.OrderedDict()
|
||||
entity_key["configuration_id"] = attr_id
|
||||
entity_key["entity_id"] = entity_id
|
||||
self._cached_changes.append({
|
||||
"attr_key": attr_key,
|
||||
changes.append({
|
||||
"new_value": new_value,
|
||||
"attr_id": attr_id,
|
||||
"entity_id": entity_id,
|
||||
"value": new_value,
|
||||
"time": datetime.datetime.now()
|
||||
"attr_key": attr_key
|
||||
})
|
||||
if new_value is None:
|
||||
op = ftrack_api.operation.DeleteEntityOperation(
|
||||
"CustomAttributeValue",
|
||||
entity_key
|
||||
)
|
||||
else:
|
||||
op = ftrack_api.operation.UpdateEntityOperation(
|
||||
"ContextCustomAttributeValue",
|
||||
entity_key,
|
||||
"value",
|
||||
ftrack_api.symbol.NOT_SET,
|
||||
new_value
|
||||
)
|
||||
|
||||
session.recorded_operations.push(op)
|
||||
self.log.info((
|
||||
"Changing Custom Attribute \"{}\" to value"
|
||||
" \"{}\" on entity: {}"
|
||||
).format(attr_key, new_value, entity_id))
|
||||
try:
|
||||
session.commit()
|
||||
except Exception:
|
||||
session.rollback()
|
||||
self.log.warning("Changing of values failed.", exc_info=True)
|
||||
self._commit_changes(session, changes)
|
||||
|
||||
def filter_changes(
|
||||
self, session, event, entities_info, interest_attributes
|
||||
|
|
|
|||
|
|
@ -66,15 +66,7 @@ class VersionToTaskStatus(BaseEvent):
|
|||
))
|
||||
return
|
||||
|
||||
_status_mapping = event_settings["mapping"]
|
||||
if not _status_mapping:
|
||||
self.log.debug(
|
||||
"Project \"{}\" does not have set mapping for {}".format(
|
||||
project_name, self.__class__.__name__
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
_status_mapping = event_settings["mapping"] or {}
|
||||
status_mapping = {
|
||||
key.lower(): value
|
||||
for key, value in _status_mapping.items()
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ from .custom_attributes import (
|
|||
default_custom_attributes_definition,
|
||||
app_definitions_from_app_manager,
|
||||
tool_definitions_from_app_manager,
|
||||
get_openpype_attr
|
||||
get_openpype_attr,
|
||||
query_custom_attributes
|
||||
)
|
||||
|
||||
from . import avalon_sync
|
||||
|
|
@ -37,6 +38,7 @@ __all__ = (
|
|||
"app_definitions_from_app_manager",
|
||||
"tool_definitions_from_app_manager",
|
||||
"get_openpype_attr",
|
||||
"query_custom_attributes",
|
||||
|
||||
"avalon_sync",
|
||||
|
||||
|
|
|
|||
|
|
@ -402,16 +402,18 @@ class SyncEntitiesFactory:
|
|||
items = []
|
||||
items.append({
|
||||
"type": "label",
|
||||
"value": "# Can't access Custom attribute <{}>".format(
|
||||
CUST_ATTR_ID_KEY
|
||||
)
|
||||
"value": (
|
||||
"# Can't access Custom attribute: <b>\"{}\"</b>"
|
||||
).format(CUST_ATTR_ID_KEY)
|
||||
})
|
||||
items.append({
|
||||
"type": "label",
|
||||
"value": (
|
||||
"<p>- Check if user \"{}\" has permissions"
|
||||
" to access the Custom attribute</p>"
|
||||
).format(self._api_key)
|
||||
"<p>- Check if your User and API key has permissions"
|
||||
" to access the Custom attribute."
|
||||
"<br>Username:\"{}\""
|
||||
"<br>API key:\"{}\"</p>"
|
||||
).format(self._api_user, self._api_key)
|
||||
})
|
||||
items.append({
|
||||
"type": "label",
|
||||
|
|
|
|||
|
|
@ -81,3 +81,60 @@ def get_openpype_attr(session, split_hierarchical=True, query_keys=None):
|
|||
return custom_attributes, hier_custom_attributes
|
||||
|
||||
return custom_attributes
|
||||
|
||||
|
||||
def join_query_keys(keys):
|
||||
"""Helper to join keys to query."""
|
||||
return ",".join(["\"{}\"".format(key) for key in keys])
|
||||
|
||||
|
||||
def query_custom_attributes(session, conf_ids, entity_ids, table_name=None):
|
||||
"""Query custom attribute values from ftrack database.
|
||||
|
||||
Using ftrack call method result may differ based on used table name and
|
||||
version of ftrack server.
|
||||
|
||||
Args:
|
||||
session(ftrack_api.Session): Connected ftrack session.
|
||||
conf_id(list, set, tuple): Configuration(attribute) ids which are
|
||||
queried.
|
||||
entity_ids(list, set, tuple): Entity ids for which are values queried.
|
||||
table_name(str): Table nam from which values are queried. Not
|
||||
recommended to change until you know what it means.
|
||||
"""
|
||||
output = []
|
||||
# Just skip
|
||||
if not conf_ids or not entity_ids:
|
||||
return output
|
||||
|
||||
if table_name is None:
|
||||
table_name = "ContextCustomAttributeValue"
|
||||
|
||||
# Prepare values to query
|
||||
attributes_joined = join_query_keys(conf_ids)
|
||||
attributes_len = len(conf_ids)
|
||||
|
||||
# Query values in chunks
|
||||
chunk_size = int(5000 / attributes_len)
|
||||
# Make sure entity_ids is `list` for chunk selection
|
||||
entity_ids = list(entity_ids)
|
||||
for idx in range(0, len(entity_ids), chunk_size):
|
||||
entity_ids_joined = join_query_keys(
|
||||
entity_ids[idx:idx + chunk_size]
|
||||
)
|
||||
|
||||
call_expr = [{
|
||||
"action": "query",
|
||||
"expression": (
|
||||
"select value, entity_id from {}"
|
||||
" where entity_id in ({}) and configuration_id in ({})"
|
||||
).format(table_name, entity_ids_joined, attributes_joined)
|
||||
}]
|
||||
if hasattr(session, "call"):
|
||||
[result] = session.call(call_expr)
|
||||
else:
|
||||
[result] = session._call(call_expr)
|
||||
|
||||
for item in result["data"]:
|
||||
output.append(item)
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -1,29 +1,107 @@
|
|||
"""
|
||||
Requires:
|
||||
none
|
||||
|
||||
Provides:
|
||||
instance -> families ([])
|
||||
"""
|
||||
import pyblish.api
|
||||
import avalon.api
|
||||
|
||||
from openpype.lib.plugin_tools import filter_profiles
|
||||
|
||||
|
||||
class CollectFtrackFamilies(pyblish.api.InstancePlugin):
|
||||
"""Collect family for ftrack publishing
|
||||
|
||||
Add ftrack family to those instance that should be published to ftrack
|
||||
|
||||
class CollectFtrackFamily(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Adds explicitly 'ftrack' to families to upload instance to FTrack.
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.3
|
||||
label = 'Add ftrack family'
|
||||
families = ["model",
|
||||
"setdress",
|
||||
"model",
|
||||
"animation",
|
||||
"look",
|
||||
"rig",
|
||||
"camera"
|
||||
]
|
||||
hosts = ["maya"]
|
||||
Uses selection by combination of hosts/families/tasks names via
|
||||
profiles resolution.
|
||||
|
||||
Triggered everywhere, checks instance against configured.
|
||||
|
||||
Checks advanced filtering which works on 'families' not on main
|
||||
'family', as some variants dynamically resolves addition of ftrack
|
||||
based on 'families' (editorial drives it by presence of 'review')
|
||||
"""
|
||||
label = "Collect Ftrack Family"
|
||||
order = pyblish.api.CollectorOrder + 0.4998
|
||||
|
||||
profiles = None
|
||||
|
||||
def process(self, instance):
|
||||
if not self.profiles:
|
||||
self.log.warning("No profiles present for adding Ftrack family")
|
||||
return
|
||||
|
||||
# make ftrack publishable
|
||||
if instance.data.get('families'):
|
||||
instance.data['families'].append('ftrack')
|
||||
task_name = instance.data.get("task",
|
||||
avalon.api.Session["AVALON_TASK"])
|
||||
host_name = avalon.api.Session["AVALON_APP"]
|
||||
family = instance.data["family"]
|
||||
|
||||
filtering_criteria = {
|
||||
"hosts": host_name,
|
||||
"families": family,
|
||||
"tasks": task_name
|
||||
}
|
||||
profile = filter_profiles(self.profiles, filtering_criteria,
|
||||
logger=self.log)
|
||||
|
||||
if profile:
|
||||
families = instance.data.get("families")
|
||||
add_ftrack_family = profile["add_ftrack_family"]
|
||||
|
||||
additional_filters = profile.get("additional_filters")
|
||||
if additional_filters:
|
||||
add_ftrack_family = self._get_add_ftrack_f_from_addit_filters(
|
||||
additional_filters,
|
||||
families,
|
||||
add_ftrack_family
|
||||
)
|
||||
|
||||
if add_ftrack_family:
|
||||
self.log.debug("Adding ftrack family for '{}'".
|
||||
format(instance.data.get("family")))
|
||||
|
||||
if families and "ftrack" not in families:
|
||||
instance.data["families"].append("ftrack")
|
||||
else:
|
||||
instance.data["families"] = ["ftrack"]
|
||||
else:
|
||||
instance.data['families'] = ['ftrack']
|
||||
self.log.debug("Instance '{}' doesn't match any profile".format(
|
||||
instance.data.get("family")))
|
||||
|
||||
def _get_add_ftrack_f_from_addit_filters(self,
|
||||
additional_filters,
|
||||
families,
|
||||
add_ftrack_family):
|
||||
"""
|
||||
Compares additional filters - working on instance's families.
|
||||
|
||||
Triggered for more detailed filtering when main family matches,
|
||||
but content of 'families' actually matter.
|
||||
(For example 'review' in 'families' should result in adding to
|
||||
Ftrack)
|
||||
|
||||
Args:
|
||||
additional_filters (dict) - from Setting
|
||||
families (list) - subfamilies
|
||||
add_ftrack_family (bool) - add ftrack to families if True
|
||||
"""
|
||||
override_filter = None
|
||||
override_filter_value = -1
|
||||
for additional_filter in additional_filters:
|
||||
filter_families = set(additional_filter["families"])
|
||||
valid = filter_families <= set(families) # issubset
|
||||
if not valid:
|
||||
continue
|
||||
|
||||
value = len(filter_families)
|
||||
if value > override_filter_value:
|
||||
override_filter = additional_filter
|
||||
override_filter_value = value
|
||||
|
||||
if override_filter:
|
||||
add_ftrack_family = override_filter["add_ftrack_family"]
|
||||
|
||||
return add_ftrack_family
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ class LocalSettingsAction(PypeModule, ITrayAction):
|
|||
|
||||
# Tray attributes
|
||||
self.settings_window = None
|
||||
self._first_trigger = True
|
||||
|
||||
def connect_with_modules(self, *_a, **_kw):
|
||||
return
|
||||
|
|
@ -153,6 +154,9 @@ class LocalSettingsAction(PypeModule, ITrayAction):
|
|||
self.settings_window.raise_()
|
||||
self.settings_window.activateWindow()
|
||||
|
||||
# Reset content if was not visible
|
||||
if not was_visible:
|
||||
# Do not reset if it's first trigger of action
|
||||
if self._first_trigger:
|
||||
self._first_trigger = False
|
||||
elif not was_visible:
|
||||
# Reset content if was not visible
|
||||
self.settings_window.reset()
|
||||
|
|
|
|||
|
|
@ -975,20 +975,52 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
|
||||
# NOTE Skipped using instance's resolution
|
||||
full_input_path_single_file = temp_data["full_input_path_single_file"]
|
||||
input_data = ffprobe_streams(
|
||||
full_input_path_single_file, self.log
|
||||
)[0]
|
||||
input_width = int(input_data["width"])
|
||||
input_height = int(input_data["height"])
|
||||
try:
|
||||
streams = ffprobe_streams(
|
||||
full_input_path_single_file, self.log
|
||||
)
|
||||
except Exception:
|
||||
raise AssertionError((
|
||||
"FFprobe couldn't read information about input file: \"{}\""
|
||||
).format(full_input_path_single_file))
|
||||
|
||||
# Try to find first stream with defined 'width' and 'height'
|
||||
# - this is to avoid order of streams where audio can be as first
|
||||
# - there may be a better way (checking `codec_type`?)
|
||||
input_width = None
|
||||
input_height = None
|
||||
for stream in streams:
|
||||
if "width" in stream and "height" in stream:
|
||||
input_width = int(stream["width"])
|
||||
input_height = int(stream["height"])
|
||||
break
|
||||
|
||||
# Raise exception of any stream didn't define input resolution
|
||||
if input_width is None:
|
||||
raise AssertionError((
|
||||
"FFprobe couldn't read resolution from input file: \"{}\""
|
||||
).format(full_input_path_single_file))
|
||||
|
||||
# NOTE Setting only one of `width` or `heigth` is not allowed
|
||||
# - settings value can't have None but has value of 0
|
||||
output_width = output_def.get("width") or None
|
||||
output_height = output_def.get("height") or None
|
||||
|
||||
# Overscal color
|
||||
overscan_color_value = "black"
|
||||
overscan_color = output_def.get("overscan_color")
|
||||
if overscan_color:
|
||||
bg_red, bg_green, bg_blue, _ = overscan_color
|
||||
overscan_color_value = "#{0:0>2X}{1:0>2X}{2:0>2X}".format(
|
||||
bg_red, bg_green, bg_blue
|
||||
)
|
||||
self.log.debug("Overscan color: `{}`".format(overscan_color_value))
|
||||
|
||||
# Convert overscan value video filters
|
||||
overscan_crop = output_def.get("overscan_crop")
|
||||
overscan = OverscanCrop(input_width, input_height, overscan_crop)
|
||||
overscan = OverscanCrop(
|
||||
input_width, input_height, overscan_crop, overscan_color_value
|
||||
)
|
||||
overscan_crop_filters = overscan.video_filters()
|
||||
# Add overscan filters to filters if are any and modify input
|
||||
# resolution by it's values
|
||||
|
|
@ -1158,9 +1190,10 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
"scale={}x{}:flags=lanczos".format(
|
||||
width_scale, height_scale
|
||||
),
|
||||
"pad={}:{}:{}:{}:black".format(
|
||||
"pad={}:{}:{}:{}:{}".format(
|
||||
output_width, output_height,
|
||||
width_half_pad, height_half_pad
|
||||
width_half_pad, height_half_pad,
|
||||
overscan_color_value
|
||||
),
|
||||
"setsar=1"
|
||||
])
|
||||
|
|
@ -1707,12 +1740,15 @@ class OverscanCrop:
|
|||
item_regex = re.compile(r"([\+\-])?([0-9]+)(.+)?")
|
||||
relative_source_regex = re.compile(r"%([\+\-])")
|
||||
|
||||
def __init__(self, input_width, input_height, string_value):
|
||||
def __init__(
|
||||
self, input_width, input_height, string_value, overscal_color=None
|
||||
):
|
||||
# Make sure that is not None
|
||||
string_value = string_value or ""
|
||||
|
||||
self.input_width = input_width
|
||||
self.input_height = input_height
|
||||
self.overscal_color = overscal_color
|
||||
|
||||
width, height = self._convert_string_to_values(string_value)
|
||||
self._width_value = width
|
||||
|
|
@ -1767,16 +1803,22 @@ class OverscanCrop:
|
|||
|
||||
elif width >= self.input_width and height >= self.input_height:
|
||||
output.append(
|
||||
"pad={}:{}:(iw-ow)/2:(ih-oh)/2".format(width, height)
|
||||
"pad={}:{}:(iw-ow)/2:(ih-oh)/2:{}".format(
|
||||
width, height, self.overscal_color
|
||||
)
|
||||
)
|
||||
|
||||
elif width > self.input_width and height < self.input_height:
|
||||
output.append("crop=iw:{}".format(height))
|
||||
output.append("pad={}:ih:(iw-ow)/2:(ih-oh)/2".format(width))
|
||||
output.append("pad={}:ih:(iw-ow)/2:(ih-oh)/2:{}".format(
|
||||
width, self.overscal_color
|
||||
))
|
||||
|
||||
elif width < self.input_width and height > self.input_height:
|
||||
output.append("crop={}:ih".format(width))
|
||||
output.append("pad=iw:{}:(iw-ow)/2:(ih-oh)/2".format(height))
|
||||
output.append("pad=iw:{}:(iw-ow)/2:(ih-oh)/2:{}".format(
|
||||
height, self.overscal_color
|
||||
))
|
||||
|
||||
return output
|
||||
|
||||
|
|
|
|||
|
|
@ -26,9 +26,23 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
slate_path = inst_data.get("slateFrame")
|
||||
ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg")
|
||||
|
||||
slate_stream = openpype.lib.ffprobe_streams(slate_path, self.log)[0]
|
||||
slate_width = slate_stream["width"]
|
||||
slate_height = slate_stream["height"]
|
||||
slate_streams = openpype.lib.ffprobe_streams(slate_path, self.log)
|
||||
# Try to find first stream with defined 'width' and 'height'
|
||||
# - this is to avoid order of streams where audio can be as first
|
||||
# - there may be a better way (checking `codec_type`?)+
|
||||
slate_width = None
|
||||
slate_height = None
|
||||
for slate_stream in slate_streams:
|
||||
if "width" in slate_stream and "height" in slate_stream:
|
||||
slate_width = int(slate_stream["width"])
|
||||
slate_height = int(slate_stream["height"])
|
||||
break
|
||||
|
||||
# Raise exception of any stream didn't define input resolution
|
||||
if slate_width is None:
|
||||
raise AssertionError((
|
||||
"FFprobe couldn't read resolution from input file: \"{}\""
|
||||
).format(slate_path))
|
||||
|
||||
if "reviewToWidth" in inst_data:
|
||||
use_legacy_code = True
|
||||
|
|
@ -309,16 +323,29 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
)
|
||||
return codec_args
|
||||
|
||||
codec_name = streams[0].get("codec_name")
|
||||
# Try to find first stream that is not an audio
|
||||
no_audio_stream = None
|
||||
for stream in streams:
|
||||
if stream.get("codec_type") != "audio":
|
||||
no_audio_stream = stream
|
||||
break
|
||||
|
||||
if no_audio_stream is None:
|
||||
self.log.warning((
|
||||
"Couldn't find stream that is not an audio from file \"{}\""
|
||||
).format(full_input_path))
|
||||
return codec_args
|
||||
|
||||
codec_name = no_audio_stream.get("codec_name")
|
||||
if codec_name:
|
||||
codec_args.append("-codec:v {}".format(codec_name))
|
||||
|
||||
profile_name = streams[0].get("profile")
|
||||
profile_name = no_audio_stream.get("profile")
|
||||
if profile_name:
|
||||
profile_name = profile_name.replace(" ", "_").lower()
|
||||
codec_args.append("-profile:v {}".format(profile_name))
|
||||
|
||||
pix_fmt = streams[0].get("pix_fmt")
|
||||
pix_fmt = no_audio_stream.get("pix_fmt")
|
||||
if pix_fmt:
|
||||
codec_args.append("-pix_fmt {}".format(pix_fmt))
|
||||
return codec_args
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@
|
|||
{
|
||||
"name": "colorspace",
|
||||
"value": "linear"
|
||||
},
|
||||
{
|
||||
"name": "create_directories",
|
||||
"value": "True"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -114,6 +118,10 @@
|
|||
{
|
||||
"name": "colorspace",
|
||||
"value": "linear"
|
||||
},
|
||||
{
|
||||
"name": "create_directories",
|
||||
"value": "True"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,6 +200,88 @@
|
|||
}
|
||||
},
|
||||
"publish": {
|
||||
"CollectFtrackFamily": {
|
||||
"enabled": true,
|
||||
"profiles": [
|
||||
{
|
||||
"hosts": [
|
||||
"standalonepublisher"
|
||||
],
|
||||
"families": [],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": true,
|
||||
"advanced_filtering": []
|
||||
},
|
||||
{
|
||||
"hosts": [
|
||||
"standalonepublisher"
|
||||
],
|
||||
"families": [
|
||||
"matchmove",
|
||||
"shot"
|
||||
],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": false,
|
||||
"advanced_filtering": []
|
||||
},
|
||||
{
|
||||
"hosts": [
|
||||
"standalonepublisher"
|
||||
],
|
||||
"families": [
|
||||
"review",
|
||||
"plate"
|
||||
],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": false,
|
||||
"advanced_filtering": [
|
||||
{
|
||||
"families": [
|
||||
"clip",
|
||||
"review"
|
||||
],
|
||||
"add_ftrack_family": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hosts": [
|
||||
"maya"
|
||||
],
|
||||
"families": [
|
||||
"model",
|
||||
"setdress",
|
||||
"animation",
|
||||
"look",
|
||||
"rig",
|
||||
"camera"
|
||||
],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": true,
|
||||
"advanced_filtering": []
|
||||
},
|
||||
{
|
||||
"hosts": [
|
||||
"tvpaint"
|
||||
],
|
||||
"families": [
|
||||
"renderPass"
|
||||
],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": false,
|
||||
"advanced_filtering": []
|
||||
},
|
||||
{
|
||||
"hosts": [
|
||||
"tvpaint"
|
||||
],
|
||||
"families": [],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": true,
|
||||
"advanced_filtering": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"IntegrateFtrackNote": {
|
||||
"enabled": true,
|
||||
"note_with_intent_template": "{intent}: {comment}",
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@
|
|||
]
|
||||
},
|
||||
"overscan_crop": "",
|
||||
"overscan_color": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255
|
||||
],
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"bg_color": [
|
||||
|
|
@ -226,6 +232,17 @@
|
|||
],
|
||||
"tasks": [],
|
||||
"template": "{family}{Task}_{Render_layer}_{Render_pass}"
|
||||
},
|
||||
{
|
||||
"families": [
|
||||
"review",
|
||||
"workfile"
|
||||
],
|
||||
"hosts": [
|
||||
"tvpaint"
|
||||
],
|
||||
"tasks": [],
|
||||
"template": "{family}{Task}"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
".*"
|
||||
]
|
||||
},
|
||||
"ValidateContainers": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
},
|
||||
"ValidateSceneSettings": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
|
|
|
|||
9
openpype/settings/defaults/project_settings/houdini.json
Normal file
9
openpype/settings/defaults/project_settings/houdini.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"publish": {
|
||||
"ValidateContainers": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -127,6 +127,11 @@
|
|||
"CollectMayaRender": {
|
||||
"sync_workfile_version": false
|
||||
},
|
||||
"ValidateContainers": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
},
|
||||
"ValidateShaderName": {
|
||||
"enabled": false,
|
||||
"regex": "(?P<asset>.*)_(.*)_SHD"
|
||||
|
|
|
|||
|
|
@ -13,13 +13,19 @@
|
|||
"fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}"
|
||||
},
|
||||
"CreateWritePrerender": {
|
||||
"fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}"
|
||||
"fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}",
|
||||
"use_range_limit": true
|
||||
}
|
||||
},
|
||||
"publish": {
|
||||
"PreCollectNukeInstances": {
|
||||
"sync_workfile_version": true
|
||||
},
|
||||
"ValidateContainers": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
},
|
||||
"ValidateKnobs": {
|
||||
"enabled": false,
|
||||
"knobs": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@
|
|||
}
|
||||
},
|
||||
"publish": {
|
||||
"ValidateContainers": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
},
|
||||
"ExtractImage": {
|
||||
"formats": [
|
||||
"png",
|
||||
|
|
|
|||
|
|
@ -105,16 +105,23 @@
|
|||
"label": "Render",
|
||||
"family": "render",
|
||||
"icon": "image",
|
||||
"defaults": ["Animation", "Lighting", "Lookdev", "Compositing"],
|
||||
"defaults": [
|
||||
"Animation",
|
||||
"Lighting",
|
||||
"Lookdev",
|
||||
"Compositing"
|
||||
],
|
||||
"help": "Rendered images or video files"
|
||||
},
|
||||
"create_mov_batch": {
|
||||
"name": "mov_batch",
|
||||
"label": "Batch Mov",
|
||||
"family": "render_mov_batch",
|
||||
"icon": "image",
|
||||
"defaults": ["Main"],
|
||||
"help": "Process multiple Mov files and publish them for layout and comp."
|
||||
"name": "mov_batch",
|
||||
"label": "Batch Mov",
|
||||
"family": "render_mov_batch",
|
||||
"icon": "image",
|
||||
"defaults": [
|
||||
"Main"
|
||||
],
|
||||
"help": "Process multiple Mov files and publish them for layout and comp."
|
||||
},
|
||||
"__dynamic_keys_labels__": {
|
||||
"create_workfile": "Workfile",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"publish": {
|
||||
"ExtractSequence": {
|
||||
"review_bg": [
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255
|
||||
]
|
||||
},
|
||||
"ValidateProjectSettings": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
|
|
|
|||
|
|
@ -807,7 +807,6 @@
|
|||
"environment": {},
|
||||
"variants": {
|
||||
"2-83": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Blender Foundation\\Blender 2.83\\blender.exe"
|
||||
|
|
@ -829,7 +828,6 @@
|
|||
"environment": {}
|
||||
},
|
||||
"2-90": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Blender Foundation\\Blender 2.90\\blender.exe"
|
||||
|
|
@ -851,7 +849,6 @@
|
|||
"environment": {}
|
||||
},
|
||||
"2-91": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Blender Foundation\\Blender 2.91\\blender.exe"
|
||||
|
|
@ -891,7 +888,6 @@
|
|||
"20": {
|
||||
"enabled": true,
|
||||
"variant_label": "20",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
|
|
@ -907,7 +903,6 @@
|
|||
"17": {
|
||||
"enabled": true,
|
||||
"variant_label": "17",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [
|
||||
|
|
@ -932,7 +927,6 @@
|
|||
"environment": {},
|
||||
"variants": {
|
||||
"animation_11-64bits": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\TVPaint Developpement\\TVPaint Animation 11 (64bits)\\TVPaint Animation 11 (64bits).exe"
|
||||
|
|
@ -948,7 +942,6 @@
|
|||
"environment": {}
|
||||
},
|
||||
"animation_11-32bits": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files (x86)\\TVPaint Developpement\\TVPaint Animation 11 (32bits)\\TVPaint Animation 11 (32bits).exe"
|
||||
|
|
@ -982,7 +975,6 @@
|
|||
"2020": {
|
||||
"enabled": true,
|
||||
"variant_label": "2020",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Adobe\\Adobe Photoshop 2020\\Photoshop.exe"
|
||||
|
|
@ -1000,7 +992,6 @@
|
|||
"2021": {
|
||||
"enabled": true,
|
||||
"variant_label": "2021",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Adobe\\Adobe Photoshop 2021\\Photoshop.exe"
|
||||
|
|
@ -1030,7 +1021,6 @@
|
|||
"2020": {
|
||||
"enabled": true,
|
||||
"variant_label": "2020",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
""
|
||||
|
|
@ -1048,7 +1038,6 @@
|
|||
"2021": {
|
||||
"enabled": true,
|
||||
"variant_label": "2021",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Adobe\\Adobe After Effects 2021\\Support Files\\AfterFX.exe"
|
||||
|
|
@ -1101,16 +1090,6 @@
|
|||
"variants": {
|
||||
"4-26": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"environment": {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ from .color_entity import ColorEntity
|
|||
from .enum_entity import (
|
||||
BaseEnumEntity,
|
||||
EnumEntity,
|
||||
HostsEnumEntity,
|
||||
AppsEnumEntity,
|
||||
ToolsEnumEntity,
|
||||
TaskTypeEnumEntity,
|
||||
|
|
@ -153,6 +154,7 @@ __all__ = (
|
|||
|
||||
"BaseEnumEntity",
|
||||
"EnumEntity",
|
||||
"HostsEnumEntity",
|
||||
"AppsEnumEntity",
|
||||
"ToolsEnumEntity",
|
||||
"TaskTypeEnumEntity",
|
||||
|
|
|
|||
|
|
@ -279,6 +279,11 @@ class BaseItemEntity(BaseEntity):
|
|||
self, "Dynamic entity can't require restart."
|
||||
)
|
||||
|
||||
@abstractproperty
|
||||
def root_key(self):
|
||||
"""Root is represented as this dictionary key."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_override_state(self, state):
|
||||
"""Set override state and trigger it on children.
|
||||
|
|
@ -866,6 +871,10 @@ class ItemEntity(BaseItemEntity):
|
|||
"""Call save on root item."""
|
||||
self.root_item.save()
|
||||
|
||||
@property
|
||||
def root_key(self):
|
||||
return self.root_item.root_key
|
||||
|
||||
def schema_validations(self):
|
||||
if not self.label and self.use_label_wrap:
|
||||
reason = (
|
||||
|
|
@ -885,7 +894,11 @@ class ItemEntity(BaseItemEntity):
|
|||
|
||||
def create_schema_object(self, *args, **kwargs):
|
||||
"""Reference method for creation of entities defined in RootEntity."""
|
||||
return self.root_item.create_schema_object(*args, **kwargs)
|
||||
return self.schema_hub.create_schema_object(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def schema_hub(self):
|
||||
return self.root_item.schema_hub
|
||||
|
||||
def get_entity_from_path(self, path):
|
||||
return self.root_item.get_entity_from_path(path)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ class ColorEntity(InputEntity):
|
|||
def _item_initalization(self):
|
||||
self.valid_value_types = (list, )
|
||||
self.value_on_not_set = [0, 0, 0, 255]
|
||||
self.use_alpha = self.schema_data.get("use_alpha", True)
|
||||
|
||||
def set_override_state(self, *args, **kwargs):
|
||||
super(ColorEntity, self).set_override_state(*args, **kwargs)
|
||||
value = self._current_value
|
||||
if (
|
||||
not self.use_alpha
|
||||
and isinstance(value, list)
|
||||
and len(value) == 4
|
||||
):
|
||||
value[3] = 255
|
||||
|
||||
def convert_to_valid_type(self, value):
|
||||
"""Conversion to valid type.
|
||||
|
|
@ -51,4 +62,8 @@ class ColorEntity(InputEntity):
|
|||
).format(value)
|
||||
raise BaseInvalidValueType(reason, self.path)
|
||||
new_value.append(item)
|
||||
|
||||
# Make sure
|
||||
if not self.use_alpha:
|
||||
new_value[3] = 255
|
||||
return new_value
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import copy
|
||||
import collections
|
||||
|
||||
from .lib import (
|
||||
WRAPPER_TYPES,
|
||||
|
|
@ -138,7 +139,16 @@ class DictImmutableKeysEntity(ItemEntity):
|
|||
method when handling gui wrappers.
|
||||
"""
|
||||
added_children = []
|
||||
for children_schema in schema_data["children"]:
|
||||
children_deque = collections.deque()
|
||||
for _children_schema in schema_data["children"]:
|
||||
children_schemas = self.schema_hub.resolve_schema_data(
|
||||
_children_schema
|
||||
)
|
||||
for children_schema in children_schemas:
|
||||
children_deque.append(children_schema)
|
||||
|
||||
while children_deque:
|
||||
children_schema = children_deque.popleft()
|
||||
if children_schema["type"] in WRAPPER_TYPES:
|
||||
_children_schema = copy.deepcopy(children_schema)
|
||||
wrapper_children = self._add_children(
|
||||
|
|
|
|||
|
|
@ -101,6 +101,78 @@ class EnumEntity(BaseEnumEntity):
|
|||
super(EnumEntity, self).schema_validations()
|
||||
|
||||
|
||||
class HostsEnumEntity(BaseEnumEntity):
|
||||
"""Enumeration of host names.
|
||||
|
||||
Enum items are hardcoded in definition of the entity.
|
||||
|
||||
Hosts enum can have defined empty value as valid option which is
|
||||
represented by empty string. Schema key to set this option is
|
||||
`use_empty_value` (true/false). And to set label of empty value set
|
||||
`empty_label` (string).
|
||||
|
||||
Enum can have single and multiselection.
|
||||
|
||||
NOTE:
|
||||
Host name is not the same as application name. Host name defines
|
||||
implementation instead of application name.
|
||||
"""
|
||||
schema_types = ["hosts-enum"]
|
||||
|
||||
def _item_initalization(self):
|
||||
self.multiselection = self.schema_data.get("multiselection", True)
|
||||
self.use_empty_value = self.schema_data.get(
|
||||
"use_empty_value", not self.multiselection
|
||||
)
|
||||
custom_labels = self.schema_data.get("custom_labels") or {}
|
||||
|
||||
host_names = [
|
||||
"aftereffects",
|
||||
"blender",
|
||||
"celaction",
|
||||
"fusion",
|
||||
"harmony",
|
||||
"hiero",
|
||||
"houdini",
|
||||
"maya",
|
||||
"nuke",
|
||||
"photoshop",
|
||||
"resolve",
|
||||
"tvpaint",
|
||||
"unreal"
|
||||
]
|
||||
if self.use_empty_value:
|
||||
host_names.insert(0, "")
|
||||
# Add default label for empty value if not available
|
||||
if "" not in custom_labels:
|
||||
custom_labels[""] = "< without host >"
|
||||
|
||||
# These are hardcoded there is not list of available host in OpenPype
|
||||
enum_items = []
|
||||
valid_keys = set()
|
||||
for key in host_names:
|
||||
label = custom_labels.get(key, key)
|
||||
valid_keys.add(key)
|
||||
enum_items.append({key: label})
|
||||
|
||||
self.enum_items = enum_items
|
||||
self.valid_keys = valid_keys
|
||||
|
||||
if self.multiselection:
|
||||
self.valid_value_types = (list, )
|
||||
self.value_on_not_set = []
|
||||
else:
|
||||
for key in valid_keys:
|
||||
if self.value_on_not_set is NOT_SET:
|
||||
self.value_on_not_set = key
|
||||
break
|
||||
|
||||
self.valid_value_types = (STRING_TYPE, )
|
||||
|
||||
# GUI attribute
|
||||
self.placeholder = self.schema_data.get("placeholder")
|
||||
|
||||
|
||||
class AppsEnumEntity(BaseEnumEntity):
|
||||
schema_types = ["apps-enum"]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
import re
|
||||
import json
|
||||
import copy
|
||||
import inspect
|
||||
|
||||
from .exceptions import (
|
||||
SchemaTemplateMissingKeys,
|
||||
|
|
@ -25,335 +26,6 @@ TEMPLATE_METADATA_KEYS = (
|
|||
template_key_pattern = re.compile(r"(\{.*?[^{0]*\})")
|
||||
|
||||
|
||||
def _pop_metadata_item(template):
|
||||
found_idx = None
|
||||
for idx, item in enumerate(template):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
for key in TEMPLATE_METADATA_KEYS:
|
||||
if key in item:
|
||||
found_idx = idx
|
||||
break
|
||||
|
||||
if found_idx is not None:
|
||||
break
|
||||
|
||||
metadata_item = {}
|
||||
if found_idx is not None:
|
||||
metadata_item = template.pop(found_idx)
|
||||
return metadata_item
|
||||
|
||||
|
||||
def _fill_schema_template_data(
|
||||
template, template_data, skip_paths, required_keys=None, missing_keys=None
|
||||
):
|
||||
first = False
|
||||
if required_keys is None:
|
||||
first = True
|
||||
|
||||
if "skip_paths" in template_data:
|
||||
skip_paths = template_data["skip_paths"]
|
||||
if not isinstance(skip_paths, list):
|
||||
skip_paths = [skip_paths]
|
||||
|
||||
# Cleanup skip paths (skip empty values)
|
||||
skip_paths = [path for path in skip_paths if path]
|
||||
|
||||
required_keys = set()
|
||||
missing_keys = set()
|
||||
|
||||
# Copy template data as content may change
|
||||
template = copy.deepcopy(template)
|
||||
|
||||
# Get metadata item from template
|
||||
metadata_item = _pop_metadata_item(template)
|
||||
|
||||
# Check for default values for template data
|
||||
default_values = metadata_item.get(DEFAULT_VALUES_KEY) or {}
|
||||
|
||||
for key, value in default_values.items():
|
||||
if key not in template_data:
|
||||
template_data[key] = value
|
||||
|
||||
if not template:
|
||||
output = template
|
||||
|
||||
elif isinstance(template, list):
|
||||
# Store paths by first part if path
|
||||
# - None value says that whole key should be skipped
|
||||
skip_paths_by_first_key = {}
|
||||
for path in skip_paths:
|
||||
parts = path.split("/")
|
||||
key = parts.pop(0)
|
||||
if key not in skip_paths_by_first_key:
|
||||
skip_paths_by_first_key[key] = []
|
||||
|
||||
value = "/".join(parts)
|
||||
skip_paths_by_first_key[key].append(value or None)
|
||||
|
||||
output = []
|
||||
for item in template:
|
||||
# Get skip paths for children item
|
||||
_skip_paths = []
|
||||
if not isinstance(item, dict):
|
||||
pass
|
||||
|
||||
elif item.get("type") in WRAPPER_TYPES:
|
||||
_skip_paths = copy.deepcopy(skip_paths)
|
||||
|
||||
elif skip_paths_by_first_key:
|
||||
# Check if this item should be skipped
|
||||
key = item.get("key")
|
||||
if key and key in skip_paths_by_first_key:
|
||||
_skip_paths = skip_paths_by_first_key[key]
|
||||
# Skip whole item if None is in skip paths value
|
||||
if None in _skip_paths:
|
||||
continue
|
||||
|
||||
output_item = _fill_schema_template_data(
|
||||
item, template_data, _skip_paths, required_keys, missing_keys
|
||||
)
|
||||
if output_item:
|
||||
output.append(output_item)
|
||||
|
||||
elif isinstance(template, dict):
|
||||
output = {}
|
||||
for key, value in template.items():
|
||||
output[key] = _fill_schema_template_data(
|
||||
value, template_data, skip_paths, required_keys, missing_keys
|
||||
)
|
||||
if output.get("type") in WRAPPER_TYPES and not output.get("children"):
|
||||
return {}
|
||||
|
||||
elif isinstance(template, STRING_TYPE):
|
||||
# TODO find much better way how to handle filling template data
|
||||
template = template.replace("{{", "__dbcb__").replace("}}", "__decb__")
|
||||
for replacement_string in template_key_pattern.findall(template):
|
||||
key = str(replacement_string[1:-1])
|
||||
required_keys.add(key)
|
||||
if key not in template_data:
|
||||
missing_keys.add(key)
|
||||
continue
|
||||
|
||||
value = template_data[key]
|
||||
if replacement_string == template:
|
||||
# Replace the value with value from templates data
|
||||
# - with this is possible to set value with different type
|
||||
template = value
|
||||
else:
|
||||
# Only replace the key in string
|
||||
template = template.replace(replacement_string, value)
|
||||
|
||||
output = template.replace("__dbcb__", "{").replace("__decb__", "}")
|
||||
|
||||
else:
|
||||
output = template
|
||||
|
||||
if first and missing_keys:
|
||||
raise SchemaTemplateMissingKeys(missing_keys, required_keys)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _fill_schema_template(child_data, schema_collection, schema_templates):
|
||||
template_name = child_data["name"]
|
||||
template = schema_templates.get(template_name)
|
||||
if template is None:
|
||||
if template_name in schema_collection:
|
||||
raise KeyError((
|
||||
"Schema \"{}\" is used as `schema_template`"
|
||||
).format(template_name))
|
||||
raise KeyError("Schema template \"{}\" was not found".format(
|
||||
template_name
|
||||
))
|
||||
|
||||
# Default value must be dictionary (NOT list)
|
||||
# - empty list would not add any item if `template_data` are not filled
|
||||
template_data = child_data.get("template_data") or {}
|
||||
if isinstance(template_data, dict):
|
||||
template_data = [template_data]
|
||||
|
||||
skip_paths = child_data.get("skip_paths") or []
|
||||
if isinstance(skip_paths, STRING_TYPE):
|
||||
skip_paths = [skip_paths]
|
||||
|
||||
output = []
|
||||
for single_template_data in template_data:
|
||||
try:
|
||||
filled_child = _fill_schema_template_data(
|
||||
template, single_template_data, skip_paths
|
||||
)
|
||||
|
||||
except SchemaTemplateMissingKeys as exc:
|
||||
raise SchemaTemplateMissingKeys(
|
||||
exc.missing_keys, exc.required_keys, template_name
|
||||
)
|
||||
|
||||
for item in filled_child:
|
||||
filled_item = _fill_inner_schemas(
|
||||
item, schema_collection, schema_templates
|
||||
)
|
||||
if filled_item["type"] == "schema_template":
|
||||
output.extend(_fill_schema_template(
|
||||
filled_item, schema_collection, schema_templates
|
||||
))
|
||||
else:
|
||||
output.append(filled_item)
|
||||
return output
|
||||
|
||||
|
||||
def _fill_inner_schemas(schema_data, schema_collection, schema_templates):
|
||||
if schema_data["type"] == "schema":
|
||||
raise ValueError("First item in schema data can't be schema.")
|
||||
|
||||
children_key = "children"
|
||||
object_type_key = "object_type"
|
||||
for item_key in (children_key, object_type_key):
|
||||
children = schema_data.get(item_key)
|
||||
if not children:
|
||||
continue
|
||||
|
||||
if object_type_key == item_key:
|
||||
if not isinstance(children, dict):
|
||||
continue
|
||||
children = [children]
|
||||
|
||||
new_children = []
|
||||
for child in children:
|
||||
child_type = child["type"]
|
||||
if child_type == "schema":
|
||||
schema_name = child["name"]
|
||||
if schema_name not in schema_collection:
|
||||
if schema_name in schema_templates:
|
||||
raise KeyError((
|
||||
"Schema template \"{}\" is used as `schema`"
|
||||
).format(schema_name))
|
||||
raise KeyError(
|
||||
"Schema \"{}\" was not found".format(schema_name)
|
||||
)
|
||||
|
||||
filled_child = _fill_inner_schemas(
|
||||
schema_collection[schema_name],
|
||||
schema_collection,
|
||||
schema_templates
|
||||
)
|
||||
|
||||
elif child_type in ("template", "schema_template"):
|
||||
for filled_child in _fill_schema_template(
|
||||
child, schema_collection, schema_templates
|
||||
):
|
||||
new_children.append(filled_child)
|
||||
continue
|
||||
|
||||
else:
|
||||
filled_child = _fill_inner_schemas(
|
||||
child, schema_collection, schema_templates
|
||||
)
|
||||
|
||||
new_children.append(filled_child)
|
||||
|
||||
if item_key == object_type_key:
|
||||
if len(new_children) != 1:
|
||||
raise KeyError((
|
||||
"Failed to fill object type with type: {} | name {}"
|
||||
).format(
|
||||
child_type, str(child.get("name"))
|
||||
))
|
||||
new_children = new_children[0]
|
||||
|
||||
schema_data[item_key] = new_children
|
||||
return schema_data
|
||||
|
||||
|
||||
# TODO reimplement logic inside entities
|
||||
def validate_environment_groups_uniquenes(
|
||||
schema_data, env_groups=None, keys=None
|
||||
):
|
||||
is_first = False
|
||||
if env_groups is None:
|
||||
is_first = True
|
||||
env_groups = {}
|
||||
keys = []
|
||||
|
||||
my_keys = copy.deepcopy(keys)
|
||||
key = schema_data.get("key")
|
||||
if key:
|
||||
my_keys.append(key)
|
||||
|
||||
env_group_key = schema_data.get("env_group_key")
|
||||
if env_group_key:
|
||||
if env_group_key not in env_groups:
|
||||
env_groups[env_group_key] = []
|
||||
env_groups[env_group_key].append("/".join(my_keys))
|
||||
|
||||
children = schema_data.get("children")
|
||||
if not children:
|
||||
return
|
||||
|
||||
for child in children:
|
||||
validate_environment_groups_uniquenes(
|
||||
child, env_groups, copy.deepcopy(my_keys)
|
||||
)
|
||||
|
||||
if is_first:
|
||||
invalid = {}
|
||||
for env_group_key, key_paths in env_groups.items():
|
||||
if len(key_paths) > 1:
|
||||
invalid[env_group_key] = key_paths
|
||||
|
||||
if invalid:
|
||||
raise SchemaDuplicatedEnvGroupKeys(invalid)
|
||||
|
||||
|
||||
def validate_schema(schema_data):
|
||||
validate_environment_groups_uniquenes(schema_data)
|
||||
|
||||
|
||||
def get_gui_schema(subfolder, main_schema_name):
|
||||
dirpath = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"schemas",
|
||||
subfolder
|
||||
)
|
||||
loaded_schemas = {}
|
||||
loaded_schema_templates = {}
|
||||
for root, _, filenames in os.walk(dirpath):
|
||||
for filename in filenames:
|
||||
basename, ext = os.path.splitext(filename)
|
||||
if ext != ".json":
|
||||
continue
|
||||
|
||||
filepath = os.path.join(root, filename)
|
||||
with open(filepath, "r") as json_stream:
|
||||
try:
|
||||
schema_data = json.load(json_stream)
|
||||
except Exception as exc:
|
||||
raise ValueError((
|
||||
"Unable to parse JSON file {}\n{}"
|
||||
).format(filepath, str(exc)))
|
||||
if isinstance(schema_data, list):
|
||||
loaded_schema_templates[basename] = schema_data
|
||||
else:
|
||||
loaded_schemas[basename] = schema_data
|
||||
|
||||
main_schema = _fill_inner_schemas(
|
||||
loaded_schemas[main_schema_name],
|
||||
loaded_schemas,
|
||||
loaded_schema_templates
|
||||
)
|
||||
validate_schema(main_schema)
|
||||
return main_schema
|
||||
|
||||
|
||||
def get_studio_settings_schema():
|
||||
return get_gui_schema("system_schema", "schema_main")
|
||||
|
||||
|
||||
def get_project_settings_schema():
|
||||
return get_gui_schema("projects_schema", "schema_main")
|
||||
|
||||
|
||||
class OverrideStateItem:
|
||||
"""Object used as item for `OverrideState` enum.
|
||||
|
||||
|
|
@ -426,3 +98,506 @@ class OverrideState:
|
|||
DEFAULTS = OverrideStateItem(0, "Defaults")
|
||||
STUDIO = OverrideStateItem(1, "Studio overrides")
|
||||
PROJECT = OverrideStateItem(2, "Project Overrides")
|
||||
|
||||
|
||||
class SchemasHub:
|
||||
def __init__(self, schema_subfolder, reset=True):
|
||||
self._schema_subfolder = schema_subfolder
|
||||
|
||||
self._loaded_types = {}
|
||||
self._gui_types = tuple()
|
||||
|
||||
self._crashed_on_load = {}
|
||||
self._loaded_templates = {}
|
||||
self._loaded_schemas = {}
|
||||
|
||||
# It doesn't make sence to reload types on each reset as they can't be
|
||||
# changed
|
||||
self._load_types()
|
||||
|
||||
# Trigger reset
|
||||
if reset:
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self._load_schemas()
|
||||
|
||||
@property
|
||||
def gui_types(self):
|
||||
return self._gui_types
|
||||
|
||||
def get_schema(self, schema_name):
|
||||
"""Get schema definition data by it's name.
|
||||
|
||||
Returns:
|
||||
dict: Copy of schema loaded from json files.
|
||||
|
||||
Raises:
|
||||
KeyError: When schema name is stored in loaded templates or json
|
||||
file was not possible to parse or when schema name was not
|
||||
found.
|
||||
"""
|
||||
if schema_name not in self._loaded_schemas:
|
||||
if schema_name in self._loaded_templates:
|
||||
raise KeyError((
|
||||
"Template \"{}\" is used as `schema`"
|
||||
).format(schema_name))
|
||||
|
||||
elif schema_name in self._crashed_on_load:
|
||||
crashed_item = self._crashed_on_load[schema_name]
|
||||
raise KeyError(
|
||||
"Unable to parse schema file \"{}\". {}".format(
|
||||
crashed_item["filpath"], crashed_item["message"]
|
||||
)
|
||||
)
|
||||
|
||||
raise KeyError(
|
||||
"Schema \"{}\" was not found".format(schema_name)
|
||||
)
|
||||
return copy.deepcopy(self._loaded_schemas[schema_name])
|
||||
|
||||
def get_template(self, template_name):
|
||||
"""Get template definition data by it's name.
|
||||
|
||||
Returns:
|
||||
list: Copy of template items loaded from json files.
|
||||
|
||||
Raises:
|
||||
KeyError: When template name is stored in loaded schemas or json
|
||||
file was not possible to parse or when template name was not
|
||||
found.
|
||||
"""
|
||||
if template_name not in self._loaded_templates:
|
||||
if template_name in self._loaded_schemas:
|
||||
raise KeyError((
|
||||
"Schema \"{}\" is used as `template`"
|
||||
).format(template_name))
|
||||
|
||||
elif template_name in self._crashed_on_load:
|
||||
crashed_item = self._crashed_on_load[template_name]
|
||||
raise KeyError(
|
||||
"Unable to parse templace file \"{}\". {}".format(
|
||||
crashed_item["filpath"], crashed_item["message"]
|
||||
)
|
||||
)
|
||||
|
||||
raise KeyError(
|
||||
"Template \"{}\" was not found".format(template_name)
|
||||
)
|
||||
return copy.deepcopy(self._loaded_templates[template_name])
|
||||
|
||||
def resolve_schema_data(self, schema_data):
|
||||
"""Resolve single item schema data as few types can be expanded.
|
||||
|
||||
This is mainly for 'schema' and 'template' types. Type 'schema' does
|
||||
not have entity representation and 'template' may contain more than one
|
||||
output schemas.
|
||||
|
||||
In other cases is retuned passed schema item in list.
|
||||
|
||||
Goal is to have schema and template resolving at one place.
|
||||
|
||||
Returns:
|
||||
list: Resolved schema data.
|
||||
"""
|
||||
schema_type = schema_data["type"]
|
||||
if schema_type not in ("schema", "template", "schema_template"):
|
||||
return [schema_data]
|
||||
|
||||
if schema_type == "schema":
|
||||
return self.resolve_schema_data(
|
||||
self.get_schema(schema_data["name"])
|
||||
)
|
||||
|
||||
template_name = schema_data["name"]
|
||||
template_def = self.get_template(template_name)
|
||||
|
||||
filled_template = self._fill_template(
|
||||
schema_data, template_def
|
||||
)
|
||||
return filled_template
|
||||
|
||||
def create_schema_object(self, schema_data, *args, **kwargs):
|
||||
"""Create entity for passed schema data.
|
||||
|
||||
Args:
|
||||
schema_data(dict): Schema definition of settings entity.
|
||||
|
||||
Returns:
|
||||
ItemEntity: Created entity for passed schema data item.
|
||||
|
||||
Raises:
|
||||
ValueError: When 'schema', 'template' or any of wrapper types are
|
||||
passed.
|
||||
KeyError: When type of passed schema is not known.
|
||||
"""
|
||||
schema_type = schema_data["type"]
|
||||
if schema_type in ("schema", "template", "schema_template"):
|
||||
raise ValueError(
|
||||
"Got unresolved schema data of type \"{}\"".format(schema_type)
|
||||
)
|
||||
|
||||
if schema_type in WRAPPER_TYPES:
|
||||
raise ValueError((
|
||||
"Function `create_schema_object` can't create entities"
|
||||
" of any wrapper type. Got type: \"{}\""
|
||||
).format(schema_type))
|
||||
|
||||
klass = self._loaded_types.get(schema_type)
|
||||
if not klass:
|
||||
raise KeyError("Unknown type \"{}\"".format(schema_type))
|
||||
|
||||
return klass(schema_data, *args, **kwargs)
|
||||
|
||||
def _load_types(self):
|
||||
"""Prepare entity types for cretion of their objects.
|
||||
|
||||
Currently all classes in `openpype.settings.entities` that inherited
|
||||
from `BaseEntity` are stored as loaded types. GUI types are stored to
|
||||
separated attribute to not mess up api access of entities.
|
||||
|
||||
TODOs:
|
||||
Add more dynamic way how to add custom types from anywhere and
|
||||
better handling of abstract classes. Skipping them is dangerous.
|
||||
"""
|
||||
|
||||
from openpype.settings import entities
|
||||
|
||||
# Define known abstract classes
|
||||
known_abstract_classes = (
|
||||
entities.BaseEntity,
|
||||
entities.BaseItemEntity,
|
||||
entities.ItemEntity,
|
||||
entities.EndpointEntity,
|
||||
entities.InputEntity,
|
||||
entities.BaseEnumEntity
|
||||
)
|
||||
|
||||
self._loaded_types = {}
|
||||
_gui_types = []
|
||||
for attr in dir(entities):
|
||||
item = getattr(entities, attr)
|
||||
# Filter classes
|
||||
if not inspect.isclass(item):
|
||||
continue
|
||||
|
||||
# Skip classes that do not inherit from BaseEntity
|
||||
if not issubclass(item, entities.BaseEntity):
|
||||
continue
|
||||
|
||||
# Skip class that is abstract by design
|
||||
if item in known_abstract_classes:
|
||||
continue
|
||||
|
||||
if inspect.isabstract(item):
|
||||
# Create an object to get crash and get traceback
|
||||
item()
|
||||
|
||||
# Backwards compatibility
|
||||
# Single entity may have multiple schema types
|
||||
for schema_type in item.schema_types:
|
||||
self._loaded_types[schema_type] = item
|
||||
|
||||
if item.gui_type:
|
||||
_gui_types.append(item)
|
||||
self._gui_types = tuple(_gui_types)
|
||||
|
||||
def _load_schemas(self):
|
||||
"""Load schema definitions from json files."""
|
||||
|
||||
# Refresh all affecting variables
|
||||
self._crashed_on_load = {}
|
||||
self._loaded_templates = {}
|
||||
self._loaded_schemas = {}
|
||||
|
||||
dirpath = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"schemas",
|
||||
self._schema_subfolder
|
||||
)
|
||||
loaded_schemas = {}
|
||||
loaded_templates = {}
|
||||
for root, _, filenames in os.walk(dirpath):
|
||||
for filename in filenames:
|
||||
basename, ext = os.path.splitext(filename)
|
||||
if ext != ".json":
|
||||
continue
|
||||
|
||||
filepath = os.path.join(root, filename)
|
||||
with open(filepath, "r") as json_stream:
|
||||
try:
|
||||
schema_data = json.load(json_stream)
|
||||
except Exception as exc:
|
||||
msg = str(exc)
|
||||
print("Unable to parse JSON file {}\n{}".format(
|
||||
filepath, msg
|
||||
))
|
||||
self._crashed_on_load[basename] = {
|
||||
"filepath": filepath,
|
||||
"message": msg
|
||||
}
|
||||
continue
|
||||
|
||||
if basename in self._crashed_on_load:
|
||||
crashed_item = self._crashed_on_load[basename]
|
||||
raise KeyError((
|
||||
"Duplicated filename \"{}\"."
|
||||
" One of them crashed on load \"{}\" {}"
|
||||
).format(
|
||||
filename,
|
||||
crashed_item["filpath"],
|
||||
crashed_item["message"]
|
||||
))
|
||||
|
||||
if isinstance(schema_data, list):
|
||||
if basename in loaded_templates:
|
||||
raise KeyError(
|
||||
"Duplicated template filename \"{}\"".format(
|
||||
filename
|
||||
)
|
||||
)
|
||||
loaded_templates[basename] = schema_data
|
||||
else:
|
||||
if basename in loaded_schemas:
|
||||
raise KeyError(
|
||||
"Duplicated schema filename \"{}\"".format(
|
||||
filename
|
||||
)
|
||||
)
|
||||
loaded_schemas[basename] = schema_data
|
||||
|
||||
self._loaded_templates = loaded_templates
|
||||
self._loaded_schemas = loaded_schemas
|
||||
|
||||
def _fill_template(self, child_data, template_def):
|
||||
"""Fill template based on schema definition and template definition.
|
||||
|
||||
Based on `child_data` is `template_def` modified and result is
|
||||
returned.
|
||||
|
||||
Template definition may have defined data to fill which
|
||||
should be filled with data from child data.
|
||||
|
||||
Child data may contain more than one output definition of an template.
|
||||
|
||||
Child data can define paths to skip. Path is full path of an item
|
||||
which won't be returned.
|
||||
|
||||
TODO:
|
||||
Be able to handle wrapper items here.
|
||||
|
||||
Args:
|
||||
child_data(dict): Schema data of template item.
|
||||
template_def(dict): Template definition that will be filled with
|
||||
child_data.
|
||||
|
||||
Returns:
|
||||
list: Resolved template always returns list of schemas.
|
||||
"""
|
||||
template_name = child_data["name"]
|
||||
|
||||
# Default value must be dictionary (NOT list)
|
||||
# - empty list would not add any item if `template_data` are not filled
|
||||
template_data = child_data.get("template_data") or {}
|
||||
if isinstance(template_data, dict):
|
||||
template_data = [template_data]
|
||||
|
||||
skip_paths = child_data.get("skip_paths") or []
|
||||
if isinstance(skip_paths, STRING_TYPE):
|
||||
skip_paths = [skip_paths]
|
||||
|
||||
output = []
|
||||
for single_template_data in template_data:
|
||||
try:
|
||||
output.extend(self._fill_template_data(
|
||||
template_def, single_template_data, skip_paths
|
||||
))
|
||||
|
||||
except SchemaTemplateMissingKeys as exc:
|
||||
raise SchemaTemplateMissingKeys(
|
||||
exc.missing_keys, exc.required_keys, template_name
|
||||
)
|
||||
return output
|
||||
|
||||
def _fill_template_data(
|
||||
self,
|
||||
template,
|
||||
template_data,
|
||||
skip_paths,
|
||||
required_keys=None,
|
||||
missing_keys=None
|
||||
):
|
||||
"""Fill template values with data from schema data.
|
||||
|
||||
Template has more abilities than schemas. It is expected that template
|
||||
will be used at multiple places (but may not). Schema represents
|
||||
exactly one entity and it's children but template may represent more
|
||||
entities.
|
||||
|
||||
Template can have "keys to fill" from their definition. Some key may be
|
||||
required and some may be optional because template has their default
|
||||
values defined.
|
||||
|
||||
Template also have ability to "skip paths" which means to skip entities
|
||||
from it's content. A template can be used across multiple places with
|
||||
different requirements.
|
||||
|
||||
Raises:
|
||||
SchemaTemplateMissingKeys: When fill data do not contain all
|
||||
required keys for template.
|
||||
"""
|
||||
first = False
|
||||
if required_keys is None:
|
||||
first = True
|
||||
|
||||
if "skip_paths" in template_data:
|
||||
skip_paths = template_data["skip_paths"]
|
||||
if not isinstance(skip_paths, list):
|
||||
skip_paths = [skip_paths]
|
||||
|
||||
# Cleanup skip paths (skip empty values)
|
||||
skip_paths = [path for path in skip_paths if path]
|
||||
|
||||
required_keys = set()
|
||||
missing_keys = set()
|
||||
|
||||
# Copy template data as content may change
|
||||
template = copy.deepcopy(template)
|
||||
|
||||
# Get metadata item from template
|
||||
metadata_item = self._pop_metadata_item(template)
|
||||
|
||||
# Check for default values for template data
|
||||
default_values = metadata_item.get(DEFAULT_VALUES_KEY) or {}
|
||||
|
||||
for key, value in default_values.items():
|
||||
if key not in template_data:
|
||||
template_data[key] = value
|
||||
|
||||
if not template:
|
||||
output = template
|
||||
|
||||
elif isinstance(template, list):
|
||||
# Store paths by first part if path
|
||||
# - None value says that whole key should be skipped
|
||||
skip_paths_by_first_key = {}
|
||||
for path in skip_paths:
|
||||
parts = path.split("/")
|
||||
key = parts.pop(0)
|
||||
if key not in skip_paths_by_first_key:
|
||||
skip_paths_by_first_key[key] = []
|
||||
|
||||
value = "/".join(parts)
|
||||
skip_paths_by_first_key[key].append(value or None)
|
||||
|
||||
output = []
|
||||
for item in template:
|
||||
# Get skip paths for children item
|
||||
_skip_paths = []
|
||||
if not isinstance(item, dict):
|
||||
pass
|
||||
|
||||
elif item.get("type") in WRAPPER_TYPES:
|
||||
_skip_paths = copy.deepcopy(skip_paths)
|
||||
|
||||
elif skip_paths_by_first_key:
|
||||
# Check if this item should be skipped
|
||||
key = item.get("key")
|
||||
if key and key in skip_paths_by_first_key:
|
||||
_skip_paths = skip_paths_by_first_key[key]
|
||||
# Skip whole item if None is in skip paths value
|
||||
if None in _skip_paths:
|
||||
continue
|
||||
|
||||
output_item = self._fill_template_data(
|
||||
item,
|
||||
template_data,
|
||||
_skip_paths,
|
||||
required_keys,
|
||||
missing_keys
|
||||
)
|
||||
if output_item:
|
||||
output.append(output_item)
|
||||
|
||||
elif isinstance(template, dict):
|
||||
output = {}
|
||||
for key, value in template.items():
|
||||
output[key] = self._fill_template_data(
|
||||
value,
|
||||
template_data,
|
||||
skip_paths,
|
||||
required_keys,
|
||||
missing_keys
|
||||
)
|
||||
if (
|
||||
output.get("type") in WRAPPER_TYPES
|
||||
and not output.get("children")
|
||||
):
|
||||
return {}
|
||||
|
||||
elif isinstance(template, STRING_TYPE):
|
||||
# TODO find much better way how to handle filling template data
|
||||
template = (
|
||||
template
|
||||
.replace("{{", "__dbcb__")
|
||||
.replace("}}", "__decb__")
|
||||
)
|
||||
full_replacement = False
|
||||
for replacement_string in template_key_pattern.findall(template):
|
||||
key = str(replacement_string[1:-1])
|
||||
required_keys.add(key)
|
||||
if key not in template_data:
|
||||
missing_keys.add(key)
|
||||
continue
|
||||
|
||||
value = template_data[key]
|
||||
if replacement_string == template:
|
||||
# Replace the value with value from templates data
|
||||
# - with this is possible to set value with different type
|
||||
template = value
|
||||
full_replacement = True
|
||||
else:
|
||||
# Only replace the key in string
|
||||
template = template.replace(replacement_string, value)
|
||||
|
||||
if not full_replacement:
|
||||
output = (
|
||||
template
|
||||
.replace("__dbcb__", "{")
|
||||
.replace("__decb__", "}")
|
||||
)
|
||||
else:
|
||||
output = template
|
||||
|
||||
else:
|
||||
output = template
|
||||
|
||||
if first and missing_keys:
|
||||
raise SchemaTemplateMissingKeys(missing_keys, required_keys)
|
||||
|
||||
return output
|
||||
|
||||
def _pop_metadata_item(self, template_def):
|
||||
"""Pop template metadata from template definition.
|
||||
|
||||
Template metadata may define default values if are not passed from
|
||||
schema data.
|
||||
"""
|
||||
|
||||
found_idx = None
|
||||
for idx, item in enumerate(template_def):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
for key in TEMPLATE_METADATA_KEYS:
|
||||
if key in item:
|
||||
found_idx = idx
|
||||
break
|
||||
|
||||
if found_idx is not None:
|
||||
break
|
||||
|
||||
metadata_item = {}
|
||||
if found_idx is not None:
|
||||
metadata_item = template_def.pop(found_idx)
|
||||
return metadata_item
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import json
|
||||
import copy
|
||||
import inspect
|
||||
import collections
|
||||
|
||||
from abc import abstractmethod
|
||||
|
||||
|
|
@ -10,8 +10,7 @@ from .lib import (
|
|||
NOT_SET,
|
||||
WRAPPER_TYPES,
|
||||
OverrideState,
|
||||
get_studio_settings_schema,
|
||||
get_project_settings_schema
|
||||
SchemasHub
|
||||
)
|
||||
from .exceptions import (
|
||||
SchemaError,
|
||||
|
|
@ -53,7 +52,12 @@ class RootEntity(BaseItemEntity):
|
|||
"""
|
||||
schema_types = ["root"]
|
||||
|
||||
def __init__(self, schema_data, reset):
|
||||
def __init__(self, schema_hub, reset, main_schema_name=None):
|
||||
self.schema_hub = schema_hub
|
||||
if not main_schema_name:
|
||||
main_schema_name = "schema_main"
|
||||
schema_data = schema_hub.get_schema(main_schema_name)
|
||||
|
||||
super(RootEntity, self).__init__(schema_data)
|
||||
self._require_restart_callbacks = []
|
||||
self._item_ids_require_restart = set()
|
||||
|
|
@ -130,7 +134,17 @@ class RootEntity(BaseItemEntity):
|
|||
|
||||
def _add_children(self, schema_data, first=True):
|
||||
added_children = []
|
||||
for children_schema in schema_data["children"]:
|
||||
children_deque = collections.deque()
|
||||
for _children_schema in schema_data["children"]:
|
||||
children_schemas = self.schema_hub.resolve_schema_data(
|
||||
_children_schema
|
||||
)
|
||||
for children_schema in children_schemas:
|
||||
children_deque.append(children_schema)
|
||||
|
||||
while children_deque:
|
||||
children_schema = children_deque.popleft()
|
||||
|
||||
if children_schema["type"] in WRAPPER_TYPES:
|
||||
_children_schema = copy.deepcopy(children_schema)
|
||||
wrapper_children = self._add_children(
|
||||
|
|
@ -143,11 +157,13 @@ class RootEntity(BaseItemEntity):
|
|||
child_obj = self.create_schema_object(children_schema, self)
|
||||
self.children.append(child_obj)
|
||||
added_children.append(child_obj)
|
||||
if isinstance(child_obj, self._gui_types):
|
||||
if isinstance(child_obj, self.schema_hub.gui_types):
|
||||
continue
|
||||
|
||||
if child_obj.key in self.non_gui_children:
|
||||
raise KeyError("Duplicated key \"{}\"".format(child_obj.key))
|
||||
raise KeyError(
|
||||
"Duplicated key \"{}\"".format(child_obj.key)
|
||||
)
|
||||
self.non_gui_children[child_obj.key] = child_obj
|
||||
|
||||
if not first:
|
||||
|
|
@ -160,9 +176,6 @@ class RootEntity(BaseItemEntity):
|
|||
# Store `self` to `root_item` for children entities
|
||||
self.root_item = self
|
||||
|
||||
self._loaded_types = None
|
||||
self._gui_types = None
|
||||
|
||||
# Children are stored by key as keys are immutable and are defined by
|
||||
# schema
|
||||
self.valid_value_types = (dict, )
|
||||
|
|
@ -189,11 +202,10 @@ class RootEntity(BaseItemEntity):
|
|||
if not KEY_REGEX.match(key):
|
||||
raise InvalidKeySymbols(self.path, key)
|
||||
|
||||
@abstractmethod
|
||||
def get_entity_from_path(self, path):
|
||||
"""Return system settings entity."""
|
||||
raise NotImplementedError((
|
||||
"Method `get_entity_from_path` not available for \"{}\""
|
||||
).format(self.__class__.__name__))
|
||||
"""Return entity matching passed path."""
|
||||
pass
|
||||
|
||||
def create_schema_object(self, schema_data, *args, **kwargs):
|
||||
"""Create entity by entered schema data.
|
||||
|
|
@ -201,54 +213,9 @@ class RootEntity(BaseItemEntity):
|
|||
Available entities are loaded on first run. Children entities can call
|
||||
this method.
|
||||
"""
|
||||
if self._loaded_types is None:
|
||||
# Load available entities
|
||||
from openpype.settings import entities
|
||||
|
||||
# Define known abstract classes
|
||||
known_abstract_classes = (
|
||||
entities.BaseEntity,
|
||||
entities.BaseItemEntity,
|
||||
entities.ItemEntity,
|
||||
entities.EndpointEntity,
|
||||
entities.InputEntity,
|
||||
entities.BaseEnumEntity
|
||||
)
|
||||
|
||||
self._loaded_types = {}
|
||||
_gui_types = []
|
||||
for attr in dir(entities):
|
||||
item = getattr(entities, attr)
|
||||
# Filter classes
|
||||
if not inspect.isclass(item):
|
||||
continue
|
||||
|
||||
# Skip classes that do not inherit from BaseEntity
|
||||
if not issubclass(item, entities.BaseEntity):
|
||||
continue
|
||||
|
||||
# Skip class that is abstract by design
|
||||
if item in known_abstract_classes:
|
||||
continue
|
||||
|
||||
if inspect.isabstract(item):
|
||||
# Create an object to get crash and get traceback
|
||||
item()
|
||||
|
||||
# Backwards compatibility
|
||||
# Single entity may have multiple schema types
|
||||
for schema_type in item.schema_types:
|
||||
self._loaded_types[schema_type] = item
|
||||
|
||||
if item.gui_type:
|
||||
_gui_types.append(item)
|
||||
self._gui_types = tuple(_gui_types)
|
||||
|
||||
klass = self._loaded_types.get(schema_data["type"])
|
||||
if not klass:
|
||||
raise KeyError("Unknown type \"{}\"".format(schema_data["type"]))
|
||||
|
||||
return klass(schema_data, *args, **kwargs)
|
||||
return self.schema_hub.create_schema_object(
|
||||
schema_data, *args, **kwargs
|
||||
)
|
||||
|
||||
def set_override_state(self, state):
|
||||
"""Set override state and trigger it on children.
|
||||
|
|
@ -491,18 +458,32 @@ class SystemSettings(RootEntity):
|
|||
schema_data (dict): Pass schema data to entity. This is for development
|
||||
and debugging purposes.
|
||||
"""
|
||||
def __init__(
|
||||
self, set_studio_state=True, reset=True, schema_data=None
|
||||
):
|
||||
if schema_data is None:
|
||||
# Load system schemas
|
||||
schema_data = get_studio_settings_schema()
|
||||
root_key = SYSTEM_SETTINGS_KEY
|
||||
|
||||
super(SystemSettings, self).__init__(schema_data, reset)
|
||||
def __init__(
|
||||
self, set_studio_state=True, reset=True, schema_hub=None
|
||||
):
|
||||
if schema_hub is None:
|
||||
# Load system schemas
|
||||
schema_hub = SchemasHub("system_schema")
|
||||
|
||||
super(SystemSettings, self).__init__(schema_hub, reset)
|
||||
|
||||
if set_studio_state:
|
||||
self.set_studio_state()
|
||||
|
||||
def get_entity_from_path(self, path):
|
||||
"""Return system settings entity."""
|
||||
path_parts = path.split("/")
|
||||
first_part = path_parts[0]
|
||||
output = self
|
||||
if first_part == self.root_key:
|
||||
path_parts.pop(0)
|
||||
|
||||
for path_part in path_parts:
|
||||
output = output[path_part]
|
||||
return output
|
||||
|
||||
def _reset_values(self):
|
||||
default_value = get_default_settings()[SYSTEM_SETTINGS_KEY]
|
||||
for key, child_obj in self.non_gui_children.items():
|
||||
|
|
@ -600,22 +581,24 @@ class ProjectSettings(RootEntity):
|
|||
schema_data (dict): Pass schema data to entity. This is for development
|
||||
and debugging purposes.
|
||||
"""
|
||||
root_key = PROJECT_SETTINGS_KEY
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
project_name=None,
|
||||
change_state=True,
|
||||
reset=True,
|
||||
schema_data=None
|
||||
schema_hub=None
|
||||
):
|
||||
self._project_name = project_name
|
||||
|
||||
self._system_settings_entity = None
|
||||
|
||||
if schema_data is None:
|
||||
if schema_hub is None:
|
||||
# Load system schemas
|
||||
schema_data = get_project_settings_schema()
|
||||
schema_hub = SchemasHub("projects_schema")
|
||||
|
||||
super(ProjectSettings, self).__init__(schema_data, reset)
|
||||
super(ProjectSettings, self).__init__(schema_hub, reset)
|
||||
|
||||
if change_state:
|
||||
if self.project_name is None:
|
||||
|
|
|
|||
|
|
@ -272,6 +272,25 @@
|
|||
}
|
||||
```
|
||||
|
||||
### hosts-enum
|
||||
- enumeration of available hosts
|
||||
- multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`)
|
||||
- it is possible to add empty value (represented with empty string) with setting `"use_empty_value"` to `True` (Default: `False`)
|
||||
- it is possible to set `"custom_labels"` for host names where key `""` is empty value (Default: `{}`)
|
||||
```
|
||||
{
|
||||
"key": "host",
|
||||
"label": "Host name",
|
||||
"type": "hosts-enum",
|
||||
"multiselection": false,
|
||||
"use_empty_value": true,
|
||||
"custom_labels": {
|
||||
"": "N/A",
|
||||
"nuke": "Nuke"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Inputs for setting value using Pure inputs
|
||||
- these inputs also have required `"key"`
|
||||
- attribute `"label"` is required in few conditions
|
||||
|
|
|
|||
|
|
@ -82,6 +82,10 @@
|
|||
"type": "schema",
|
||||
"name": "schema_project_hiero"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_project_houdini"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_project_blender"
|
||||
|
|
|
|||
|
|
@ -604,6 +604,82 @@
|
|||
"key": "publish",
|
||||
"label": "Publish plugins",
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"key": "CollectFtrackFamily",
|
||||
"label": "Collect Ftrack Family",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"collapsible": true,
|
||||
"key": "profiles",
|
||||
"label": "Profiles",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"key": "hosts",
|
||||
"label": "Host names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"key": "families",
|
||||
"label": "Families",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
"label": "Task names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"key": "add_ftrack_family",
|
||||
"label": "Add Ftrack Family",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"collapsible": true,
|
||||
"key": "advanced_filtering",
|
||||
"label": "Advanced adding if additional families present",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"key": "families",
|
||||
"label": "Additional Families",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"key": "add_ftrack_family",
|
||||
"label": "Add Ftrack Family",
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,16 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_publish_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "ValidateContainers",
|
||||
"label": "ValidateContainers"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "houdini",
|
||||
"label": "Houdini",
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "publish",
|
||||
"label": "Publish plugins",
|
||||
"children": [
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_publish_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "ValidateContainers",
|
||||
"label": "ValidateContainers"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -77,6 +77,11 @@
|
|||
"type": "text",
|
||||
"key": "fpath_template",
|
||||
"label": "Path template"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "use_range_limit",
|
||||
"label": "Use Frame range limit by default"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,16 @@
|
|||
"key": "publish",
|
||||
"label": "Publish plugins",
|
||||
"children": [
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_publish_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "ValidateContainers",
|
||||
"label": "ValidateContainers"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
@ -50,7 +60,7 @@
|
|||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@
|
|||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "hosts-enum",
|
||||
"key": "hosts",
|
||||
"label": "Host names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,25 @@
|
|||
"key": "publish",
|
||||
"label": "Publish plugins",
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "ExtractSequence",
|
||||
"label": "ExtractSequence",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Review BG color</b> is used for whole scene review and for thumbnails."
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
"key": "review_bg",
|
||||
"label": "Review BG color",
|
||||
"use_alpha": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_publish_plugin",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
"key": "imageio",
|
||||
"label": "Color Management and Output Formats",
|
||||
"is_file": true,
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "hiero",
|
||||
|
|
@ -15,6 +14,7 @@
|
|||
"type": "dict",
|
||||
"label": "Workfile",
|
||||
"collapsible": false,
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "form",
|
||||
|
|
@ -89,6 +89,7 @@
|
|||
"type": "dict",
|
||||
"label": "Colorspace on Inputs by regex detection",
|
||||
"collapsible": true,
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "list",
|
||||
|
|
@ -123,6 +124,7 @@
|
|||
"type": "dict",
|
||||
"label": "Viewer",
|
||||
"collapsible": false,
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
|
|
@ -136,6 +138,7 @@
|
|||
"type": "dict",
|
||||
"label": "Workfile",
|
||||
"collapsible": false,
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "form",
|
||||
|
|
@ -233,6 +236,7 @@
|
|||
"type": "dict",
|
||||
"label": "Nodes",
|
||||
"collapsible": true,
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "requiredNodes",
|
||||
|
|
@ -335,6 +339,7 @@
|
|||
"type": "dict",
|
||||
"label": "Colorspace on Inputs by regex detection",
|
||||
"collapsible": true,
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "list",
|
||||
|
|
|
|||
|
|
@ -90,10 +90,10 @@
|
|||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "hosts-enum",
|
||||
"key": "hosts",
|
||||
"label": "Hosts",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
|
|
@ -182,6 +182,16 @@
|
|||
"key": "overscan_crop",
|
||||
"label": "Overscan crop"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Overscan color is used when input aspect ratio is not same as output aspect ratio."
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
"label": "Overscan color",
|
||||
"key": "overscan_color",
|
||||
"use_alpha": false
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Width and Height must be both set to higher value than 0 else source resolution is used."
|
||||
|
|
@ -348,10 +358,10 @@
|
|||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "hosts-enum",
|
||||
"key": "hosts",
|
||||
"label": "Hosts",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
|
|
@ -482,10 +492,10 @@
|
|||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "hosts-enum",
|
||||
"key": "hosts",
|
||||
"label": "Hosts",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@
|
|||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "hosts-enum",
|
||||
"key": "hosts",
|
||||
"label": "Hosts",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
|
|
@ -75,10 +75,10 @@
|
|||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"type": "hosts-enum",
|
||||
"key": "hosts",
|
||||
"label": "Hosts",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,16 @@
|
|||
"type": "label",
|
||||
"label": "Validators"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_publish_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "ValidateContainers",
|
||||
"label": "ValidateContainers"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,16 @@
|
|||
"type": "label",
|
||||
"label": "Validators"
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_publish_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "ValidateContainers",
|
||||
"label": "ValidateContainers"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -29,11 +29,13 @@
|
|||
"template_data": [
|
||||
{
|
||||
"app_variant_label": "2020",
|
||||
"app_variant": "2020"
|
||||
"app_variant": "2020",
|
||||
"variant_skip_paths": ["use_python_2"]
|
||||
},
|
||||
{
|
||||
"app_variant_label": "2021",
|
||||
"app_variant": "2021"
|
||||
"app_variant": "2021",
|
||||
"variant_skip_paths": ["use_python_2"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@
|
|||
"children": [
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_host_variant_items"
|
||||
"name": "template_host_variant_items",
|
||||
"skip_paths": ["use_python_2"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,11 +29,13 @@
|
|||
"template_data": [
|
||||
{
|
||||
"app_variant_label": "20",
|
||||
"app_variant": "20"
|
||||
"app_variant": "20",
|
||||
"variant_skip_paths": ["use_python_2"]
|
||||
},
|
||||
{
|
||||
"app_variant_label": "17",
|
||||
"app_variant": "17"
|
||||
"app_variant": "17",
|
||||
"variant_skip_paths": ["use_python_2"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,11 +29,13 @@
|
|||
"template_data": [
|
||||
{
|
||||
"app_variant_label": "2020",
|
||||
"app_variant": "2020"
|
||||
"app_variant": "2020",
|
||||
"variant_skip_paths": ["use_python_2"]
|
||||
},
|
||||
{
|
||||
"app_variant_label": "2021",
|
||||
"app_variant": "2021"
|
||||
"app_variant": "2021",
|
||||
"variant_skip_paths": ["use_python_2"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@
|
|||
"children": [
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_host_variant_items"
|
||||
"name": "template_host_variant_items",
|
||||
"skip_paths": ["use_python_2"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,12 @@
|
|||
"children": [
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_host_variant_items"
|
||||
"name": "template_host_variant_items",
|
||||
"skip_paths": [
|
||||
"executables",
|
||||
"separator",
|
||||
"arguments"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,25 +14,11 @@
|
|||
"roles": ["developer"]
|
||||
},
|
||||
{
|
||||
"type": "enum",
|
||||
"type": "hosts-enum",
|
||||
"key": "host_name",
|
||||
"label": "Host implementation",
|
||||
"enum_items": [
|
||||
{ "": "< without host >" },
|
||||
{ "aftereffects": "aftereffects" },
|
||||
{ "blender": "blender" },
|
||||
{ "celaction": "celaction" },
|
||||
{ "fusion": "fusion" },
|
||||
{ "harmony": "harmony" },
|
||||
{ "hiero": "hiero" },
|
||||
{ "houdini": "houdini" },
|
||||
{ "maya": "maya" },
|
||||
{ "nuke": "nuke" },
|
||||
{ "photoshop": "photoshop" },
|
||||
{ "resolve": "resolve" },
|
||||
{ "tvpaint": "tvpaint" },
|
||||
{ "unreal": "unreal" }
|
||||
],
|
||||
"multiselection": false,
|
||||
"use_empty_value": true,
|
||||
"roles": ["developer"]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
[
|
||||
{
|
||||
"__default_values__": {
|
||||
"variant_skip_paths": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "{app_variant}",
|
||||
|
|
@ -19,7 +24,8 @@
|
|||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_host_variant_items"
|
||||
"name": "template_host_variant_items",
|
||||
"skip_paths": "{variant_skip_paths}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"placeholder": "Executable path"
|
||||
},
|
||||
{
|
||||
"key": "separator",
|
||||
"type":"separator"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -532,7 +532,11 @@ def apply_local_settings_on_system_settings(system_settings, local_settings):
|
|||
|
||||
variants = system_settings["applications"][app_group_name]["variants"]
|
||||
for app_name, app_value in value.items():
|
||||
if not app_value or app_name not in variants:
|
||||
if (
|
||||
not app_value
|
||||
or app_name not in variants
|
||||
or "executables" not in variants[app_name]
|
||||
):
|
||||
continue
|
||||
|
||||
executable = app_value.get("executable")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ provides a bridge between the file-based project inventory and configuration.
|
|||
"""
|
||||
|
||||
import os
|
||||
from Qt import QtGui
|
||||
from Qt import QtGui, QtCore
|
||||
from avalon.vendor import qtawesome
|
||||
from openpype.api import resources
|
||||
|
||||
|
|
@ -23,6 +23,51 @@ ICON_CACHE = {}
|
|||
NOT_FOUND = type("NotFound", (object, ), {})
|
||||
|
||||
|
||||
class ProjectHandler(QtCore.QObject):
|
||||
"""Handler of project model and current project in Launcher tool.
|
||||
|
||||
Helps to organize two separate widgets handling current project selection.
|
||||
|
||||
It is easier to trigger project change callbacks from one place than from
|
||||
multiple differect places without proper handling or sequence changes.
|
||||
|
||||
Args:
|
||||
dbcon(AvalonMongoDB): Mongo connection with Session.
|
||||
model(ProjectModel): Object of projects model which is shared across
|
||||
all widgets using projects. Arg dbcon should be used as source for
|
||||
the model.
|
||||
"""
|
||||
# Project list will be refreshed each 10000 msecs
|
||||
# - this is not part of helper implementation but should be used by widgets
|
||||
# that may require reshing of projects
|
||||
refresh_interval = 10000
|
||||
|
||||
# Signal emmited when project has changed
|
||||
project_changed = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, dbcon, model):
|
||||
super(ProjectHandler, self).__init__()
|
||||
# Store project model for usage
|
||||
self.model = model
|
||||
# Store dbcon
|
||||
self.dbcon = dbcon
|
||||
|
||||
self.current_project = dbcon.Session.get("AVALON_PROJECT")
|
||||
|
||||
def set_project(self, project_name):
|
||||
# Change current project of this handler
|
||||
self.current_project = project_name
|
||||
# Change session project to take effect for other widgets using the
|
||||
# dbcon object.
|
||||
self.dbcon.Session["AVALON_PROJECT"] = project_name
|
||||
|
||||
# Trigger change signal when everything is updated to new project
|
||||
self.project_changed.emit(project_name)
|
||||
|
||||
def refresh_model(self):
|
||||
self.model.refresh()
|
||||
|
||||
|
||||
def get_action_icon(action):
|
||||
icon_name = action.icon
|
||||
if not icon_name:
|
||||
|
|
|
|||
|
|
@ -325,19 +325,59 @@ class ProjectModel(QtGui.QStandardItemModel):
|
|||
|
||||
self.hide_invisible = False
|
||||
self.project_icon = qtawesome.icon("fa.map", color="white")
|
||||
self._project_names = set()
|
||||
|
||||
def refresh(self):
|
||||
self.clear()
|
||||
self.beginResetModel()
|
||||
|
||||
project_names = set()
|
||||
for project_doc in self.get_projects():
|
||||
item = QtGui.QStandardItem(self.project_icon, project_doc["name"])
|
||||
self.appendRow(item)
|
||||
project_names.add(project_doc["name"])
|
||||
|
||||
self.endResetModel()
|
||||
origin_project_names = set(self._project_names)
|
||||
self._project_names = project_names
|
||||
|
||||
project_names_to_remove = origin_project_names - project_names
|
||||
if project_names_to_remove:
|
||||
row_counts = {}
|
||||
continuous = None
|
||||
for row in range(self.rowCount()):
|
||||
index = self.index(row, 0)
|
||||
index_name = index.data(QtCore.Qt.DisplayRole)
|
||||
if index_name in project_names_to_remove:
|
||||
if continuous is None:
|
||||
continuous = row
|
||||
row_counts[continuous] = 0
|
||||
row_counts[continuous] += 1
|
||||
else:
|
||||
continuous = None
|
||||
|
||||
for row in reversed(sorted(row_counts.keys())):
|
||||
count = row_counts[row]
|
||||
self.removeRows(row, count)
|
||||
|
||||
continuous = None
|
||||
row_counts = {}
|
||||
for idx, project_name in enumerate(sorted(project_names)):
|
||||
if project_name in origin_project_names:
|
||||
continuous = None
|
||||
continue
|
||||
|
||||
if continuous is None:
|
||||
continuous = idx
|
||||
row_counts[continuous] = []
|
||||
|
||||
row_counts[continuous].append(project_name)
|
||||
|
||||
for row in reversed(sorted(row_counts.keys())):
|
||||
items = []
|
||||
for project_name in row_counts[row]:
|
||||
item = QtGui.QStandardItem(self.project_icon, project_name)
|
||||
items.append(item)
|
||||
|
||||
self.invisibleRootItem().insertRows(row, items)
|
||||
|
||||
def get_projects(self):
|
||||
project_docs = []
|
||||
|
||||
for project_doc in sorted(
|
||||
self.dbcon.projects(), key=lambda x: x["name"]
|
||||
):
|
||||
|
|
|
|||
|
|
@ -20,22 +20,15 @@ from .constants import (
|
|||
|
||||
|
||||
class ProjectBar(QtWidgets.QWidget):
|
||||
project_changed = QtCore.Signal(int)
|
||||
|
||||
def __init__(self, dbcon, parent=None):
|
||||
def __init__(self, project_handler, parent=None):
|
||||
super(ProjectBar, self).__init__(parent)
|
||||
|
||||
self.dbcon = dbcon
|
||||
|
||||
model = ProjectModel(dbcon)
|
||||
model.hide_invisible = True
|
||||
|
||||
project_combobox = QtWidgets.QComboBox(self)
|
||||
# Change delegate so stylysheets are applied
|
||||
project_delegate = QtWidgets.QStyledItemDelegate(project_combobox)
|
||||
project_combobox.setItemDelegate(project_delegate)
|
||||
|
||||
project_combobox.setModel(model)
|
||||
project_combobox.setModel(project_handler.model)
|
||||
project_combobox.setRootModelIndex(QtCore.QModelIndex())
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
|
|
@ -47,21 +40,43 @@ class ProjectBar(QtWidgets.QWidget):
|
|||
QtWidgets.QSizePolicy.Maximum
|
||||
)
|
||||
|
||||
self.model = model
|
||||
refresh_timer = QtCore.QTimer()
|
||||
refresh_timer.setInterval(project_handler.refresh_interval)
|
||||
|
||||
self.project_handler = project_handler
|
||||
self.project_delegate = project_delegate
|
||||
self.project_combobox = project_combobox
|
||||
|
||||
# Initialize
|
||||
self.refresh()
|
||||
self.refresh_timer = refresh_timer
|
||||
|
||||
# Signals
|
||||
self.project_combobox.currentIndexChanged.connect(self.project_changed)
|
||||
refresh_timer.timeout.connect(self._on_refresh_timeout)
|
||||
self.project_combobox.currentIndexChanged.connect(self.on_index_change)
|
||||
project_handler.project_changed.connect(self._on_project_change)
|
||||
|
||||
# Set current project by default if it's set.
|
||||
project_name = self.dbcon.Session.get("AVALON_PROJECT")
|
||||
project_name = project_handler.current_project
|
||||
if project_name:
|
||||
self.set_project(project_name)
|
||||
|
||||
def showEvent(self, event):
|
||||
if not self.refresh_timer.isActive():
|
||||
self.refresh_timer.start()
|
||||
super(ProjectBar, self).showEvent(event)
|
||||
|
||||
def _on_refresh_timeout(self):
|
||||
if not self.isVisible():
|
||||
# Stop timer if widget is not visible
|
||||
self.refresh_timer.stop()
|
||||
|
||||
elif self.isActiveWindow():
|
||||
# Refresh projects if window is active
|
||||
self.project_handler.refresh_model()
|
||||
|
||||
def _on_project_change(self, project_name):
|
||||
if self.get_current_project() == project_name:
|
||||
return
|
||||
self.set_project(project_name)
|
||||
|
||||
def get_current_project(self):
|
||||
return self.project_combobox.currentText()
|
||||
|
||||
|
|
@ -69,27 +84,18 @@ class ProjectBar(QtWidgets.QWidget):
|
|||
index = self.project_combobox.findText(project_name)
|
||||
if index < 0:
|
||||
# Try refresh combobox model
|
||||
self.project_combobox.blockSignals(True)
|
||||
self.model.refresh()
|
||||
self.project_combobox.blockSignals(False)
|
||||
|
||||
self.project_handler.refresh_model()
|
||||
index = self.project_combobox.findText(project_name)
|
||||
|
||||
if index >= 0:
|
||||
self.project_combobox.setCurrentIndex(index)
|
||||
|
||||
def refresh(self):
|
||||
prev_project_name = self.get_current_project()
|
||||
def on_index_change(self, idx):
|
||||
if not self.isVisible():
|
||||
return
|
||||
|
||||
# Refresh without signals
|
||||
self.project_combobox.blockSignals(True)
|
||||
|
||||
self.model.refresh()
|
||||
self.set_project(prev_project_name)
|
||||
|
||||
self.project_combobox.blockSignals(False)
|
||||
|
||||
self.project_changed.emit(self.project_combobox.currentIndex())
|
||||
project_name = self.get_current_project()
|
||||
self.project_handler.set_project(project_name)
|
||||
|
||||
|
||||
class ActionBar(QtWidgets.QWidget):
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from avalon.tools import lib as tools_lib
|
|||
from avalon.tools.widgets import AssetWidget
|
||||
from avalon.vendor import qtawesome
|
||||
from .models import ProjectModel
|
||||
from .lib import get_action_label
|
||||
from .lib import get_action_label, ProjectHandler
|
||||
from .widgets import (
|
||||
ProjectBar,
|
||||
ActionBar,
|
||||
|
|
@ -89,37 +89,49 @@ class ProjectIconView(QtWidgets.QListView):
|
|||
|
||||
class ProjectsPanel(QtWidgets.QWidget):
|
||||
"""Projects Page"""
|
||||
|
||||
project_clicked = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, dbcon, parent=None):
|
||||
def __init__(self, project_handler, parent=None):
|
||||
super(ProjectsPanel, self).__init__(parent=parent)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
self.dbcon = dbcon
|
||||
self.dbcon.install()
|
||||
|
||||
view = ProjectIconView(parent=self)
|
||||
view.setSelectionMode(QtWidgets.QListView.NoSelection)
|
||||
flick = FlickCharm(parent=self)
|
||||
flick.activateOn(view)
|
||||
model = ProjectModel(self.dbcon)
|
||||
model.hide_invisible = True
|
||||
model.refresh()
|
||||
view.setModel(model)
|
||||
|
||||
view.setModel(project_handler.model)
|
||||
|
||||
layout.addWidget(view)
|
||||
|
||||
refresh_timer = QtCore.QTimer()
|
||||
refresh_timer.setInterval(project_handler.refresh_interval)
|
||||
|
||||
refresh_timer.timeout.connect(self._on_refresh_timeout)
|
||||
view.clicked.connect(self.on_clicked)
|
||||
|
||||
self.model = model
|
||||
self.view = view
|
||||
self.refresh_timer = refresh_timer
|
||||
self.project_handler = project_handler
|
||||
|
||||
def on_clicked(self, index):
|
||||
if index.isValid():
|
||||
project_name = index.data(QtCore.Qt.DisplayRole)
|
||||
self.project_clicked.emit(project_name)
|
||||
self.project_handler.set_project(project_name)
|
||||
|
||||
def showEvent(self, event):
|
||||
self.project_handler.refresh_model()
|
||||
if not self.refresh_timer.isActive():
|
||||
self.refresh_timer.start()
|
||||
super(ProjectsPanel, self).showEvent(event)
|
||||
|
||||
def _on_refresh_timeout(self):
|
||||
if not self.isVisible():
|
||||
# Stop timer if widget is not visible
|
||||
self.refresh_timer.stop()
|
||||
|
||||
elif self.isActiveWindow():
|
||||
# Refresh projects if window is active
|
||||
self.project_handler.refresh_model()
|
||||
|
||||
|
||||
class AssetsPanel(QtWidgets.QWidget):
|
||||
|
|
@ -127,7 +139,7 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
back_clicked = QtCore.Signal()
|
||||
session_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, dbcon, parent=None):
|
||||
def __init__(self, project_handler, dbcon, parent=None):
|
||||
super(AssetsPanel, self).__init__(parent=parent)
|
||||
|
||||
self.dbcon = dbcon
|
||||
|
|
@ -142,7 +154,7 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
btn_back = QtWidgets.QPushButton(project_bar_widget)
|
||||
btn_back.setIcon(btn_back_icon)
|
||||
|
||||
project_bar = ProjectBar(self.dbcon, project_bar_widget)
|
||||
project_bar = ProjectBar(project_handler, project_bar_widget)
|
||||
|
||||
layout.addWidget(btn_back)
|
||||
layout.addWidget(project_bar)
|
||||
|
|
@ -185,24 +197,19 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
layout.addWidget(body)
|
||||
|
||||
# signals
|
||||
project_bar.project_changed.connect(self.on_project_changed)
|
||||
project_handler.project_changed.connect(self.on_project_changed)
|
||||
assets_widget.selection_changed.connect(self.on_asset_changed)
|
||||
assets_widget.refreshed.connect(self.on_asset_changed)
|
||||
tasks_widget.task_changed.connect(self.on_task_change)
|
||||
|
||||
btn_back.clicked.connect(self.back_clicked)
|
||||
|
||||
self.project_handler = project_handler
|
||||
self.project_bar = project_bar
|
||||
self.assets_widget = assets_widget
|
||||
self.tasks_widget = tasks_widget
|
||||
self._btn_back = btn_back
|
||||
|
||||
# Force initial refresh for the assets since we might not be
|
||||
# trigging a Project switch if we click the project that was set
|
||||
# prior to launching the Launcher
|
||||
# todo: remove this behavior when AVALON_PROJECT is not required
|
||||
assets_widget.refresh()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(AssetsPanel, self).showEvent(event)
|
||||
|
||||
|
|
@ -211,19 +218,7 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
btn_size = self.project_bar.height()
|
||||
self._btn_back.setFixedSize(QtCore.QSize(btn_size, btn_size))
|
||||
|
||||
def set_project(self, project):
|
||||
before = self.project_bar.get_current_project()
|
||||
if before == project:
|
||||
self.assets_widget.refresh()
|
||||
return
|
||||
|
||||
self.project_bar.set_project(project)
|
||||
self.on_project_changed()
|
||||
|
||||
def on_project_changed(self):
|
||||
project_name = self.project_bar.get_current_project()
|
||||
self.dbcon.Session["AVALON_PROJECT"] = project_name
|
||||
|
||||
self.session_changed.emit()
|
||||
|
||||
self.assets_widget.refresh()
|
||||
|
|
@ -232,11 +227,8 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
"""Callback on asset selection changed
|
||||
|
||||
This updates the task view.
|
||||
|
||||
"""
|
||||
|
||||
print("Asset changed..")
|
||||
|
||||
asset_name = None
|
||||
asset_silo = None
|
||||
|
||||
|
|
@ -276,6 +268,8 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
|
||||
class LauncherWindow(QtWidgets.QDialog):
|
||||
"""Launcher interface"""
|
||||
# Refresh actions each 10000msecs
|
||||
actions_refresh_timeout = 10000
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(LauncherWindow, self).__init__(parent)
|
||||
|
|
@ -298,8 +292,12 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint
|
||||
)
|
||||
|
||||
project_panel = ProjectsPanel(self.dbcon)
|
||||
asset_panel = AssetsPanel(self.dbcon)
|
||||
project_model = ProjectModel(self.dbcon)
|
||||
project_model.hide_invisible = True
|
||||
project_handler = ProjectHandler(self.dbcon, project_model)
|
||||
|
||||
project_panel = ProjectsPanel(project_handler)
|
||||
asset_panel = AssetsPanel(project_handler, self.dbcon)
|
||||
|
||||
page_slider = SlidePageWidget()
|
||||
page_slider.addWidget(project_panel)
|
||||
|
|
@ -344,6 +342,12 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
actions_refresh_timer = QtCore.QTimer()
|
||||
actions_refresh_timer.setInterval(self.actions_refresh_timeout)
|
||||
|
||||
self.actions_refresh_timer = actions_refresh_timer
|
||||
self.project_handler = project_handler
|
||||
|
||||
self.message_label = message_label
|
||||
self.project_panel = project_panel
|
||||
self.asset_panel = asset_panel
|
||||
|
|
@ -353,23 +357,21 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
self._page = 0
|
||||
|
||||
# signals
|
||||
actions_refresh_timer.timeout.connect(self._on_action_timer)
|
||||
actions_bar.action_clicked.connect(self.on_action_clicked)
|
||||
action_history.trigger_history.connect(self.on_history_action)
|
||||
project_panel.project_clicked.connect(self.on_project_clicked)
|
||||
project_handler.project_changed.connect(self.on_project_change)
|
||||
asset_panel.back_clicked.connect(self.on_back_clicked)
|
||||
asset_panel.session_changed.connect(self.on_session_changed)
|
||||
|
||||
# todo: Simplify this callback connection
|
||||
asset_panel.project_bar.project_changed.connect(
|
||||
self.on_project_changed
|
||||
)
|
||||
|
||||
self.resize(520, 740)
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
# TODO implement refresh/reset which will trigger updating
|
||||
self.discover_actions()
|
||||
if not self.actions_refresh_timer.isActive():
|
||||
self.actions_refresh_timer.start()
|
||||
self.discover_actions()
|
||||
|
||||
super(LauncherWindow, self).showEvent(event)
|
||||
|
||||
def set_page(self, page):
|
||||
current = self.page_slider.currentIndex()
|
||||
|
|
@ -385,13 +387,6 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
QtCore.QTimer.singleShot(5000, lambda: self.message_label.setText(""))
|
||||
self.log.debug(message)
|
||||
|
||||
def on_project_changed(self):
|
||||
project_name = self.asset_panel.project_bar.get_current_project()
|
||||
self.dbcon.Session["AVALON_PROJECT"] = project_name
|
||||
|
||||
# Update the Action plug-ins available for the current project
|
||||
self.discover_actions()
|
||||
|
||||
def on_session_changed(self):
|
||||
self.filter_actions()
|
||||
|
||||
|
|
@ -402,17 +397,23 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
def filter_actions(self):
|
||||
self.actions_bar.filter_actions()
|
||||
|
||||
def on_project_clicked(self, project_name):
|
||||
self.dbcon.Session["AVALON_PROJECT"] = project_name
|
||||
# Refresh projects
|
||||
self.asset_panel.set_project(project_name)
|
||||
def _on_action_timer(self):
|
||||
if not self.isVisible():
|
||||
# Stop timer if widget is not visible
|
||||
self.actions_refresh_timer.stop()
|
||||
|
||||
elif self.isActiveWindow():
|
||||
# Refresh projects if window is active
|
||||
self.discover_actions()
|
||||
|
||||
def on_project_change(self, project_name):
|
||||
# Update the Action plug-ins available for the current project
|
||||
self.set_page(1)
|
||||
self.discover_actions()
|
||||
|
||||
def on_back_clicked(self):
|
||||
self.dbcon.Session["AVALON_PROJECT"] = None
|
||||
self.project_handler.set_project(None)
|
||||
self.set_page(0)
|
||||
self.project_panel.model.refresh() # Refresh projects
|
||||
self.discover_actions()
|
||||
|
||||
def on_action_clicked(self, action):
|
||||
|
|
|
|||
|
|
@ -121,6 +121,9 @@ class AppGroupWidget(QtWidgets.QWidget):
|
|||
|
||||
widgets_by_variant_name = {}
|
||||
for variant_name, variant_entity in valid_variants.items():
|
||||
if "executables" not in variant_entity:
|
||||
continue
|
||||
|
||||
variant_widget = AppVariantWidget(
|
||||
group_label, variant_name, variant_entity, content_widget
|
||||
)
|
||||
|
|
@ -193,8 +196,12 @@ class LocalApplicationsWidgets(QtWidgets.QWidget):
|
|||
|
||||
# Create App group specific widget and store it by the key
|
||||
group_widget = AppGroupWidget(entity, self)
|
||||
self.widgets_by_group_name[key] = group_widget
|
||||
self.content_layout.addWidget(group_widget)
|
||||
if group_widget.widgets_by_variant_name:
|
||||
self.widgets_by_group_name[key] = group_widget
|
||||
self.content_layout.addWidget(group_widget)
|
||||
else:
|
||||
group_widget.setVisible(False)
|
||||
group_widget.deleteLater()
|
||||
|
||||
def update_local_settings(self, value):
|
||||
if not value:
|
||||
|
|
|
|||
|
|
@ -168,9 +168,6 @@ class LocalSettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
scroll_widget = QtWidgets.QScrollArea(self)
|
||||
scroll_widget.setObjectName("GroupWidget")
|
||||
settings_widget = LocalSettingsWidget(scroll_widget)
|
||||
|
||||
scroll_widget.setWidget(settings_widget)
|
||||
scroll_widget.setWidgetResizable(True)
|
||||
|
||||
footer = QtWidgets.QWidget(self)
|
||||
|
|
@ -191,7 +188,12 @@ class LocalSettingsWindow(QtWidgets.QWidget):
|
|||
save_btn.clicked.connect(self._on_save_clicked)
|
||||
reset_btn.clicked.connect(self._on_reset_clicked)
|
||||
|
||||
self.settings_widget = settings_widget
|
||||
# Do not create local settings widget in init phase as it's using
|
||||
# settings objects that must be OK to be able create this widget
|
||||
# - we want to show dialog if anything goes wrong
|
||||
# - without reseting nothing is shown
|
||||
self._settings_widget = None
|
||||
self._scroll_widget = scroll_widget
|
||||
self.reset_btn = reset_btn
|
||||
self.save_btn = save_btn
|
||||
|
||||
|
|
@ -203,13 +205,53 @@ class LocalSettingsWindow(QtWidgets.QWidget):
|
|||
def reset(self):
|
||||
if self._reset_on_show:
|
||||
self._reset_on_show = False
|
||||
value = get_local_settings()
|
||||
self.settings_widget.update_local_settings(value)
|
||||
|
||||
error_msg = None
|
||||
try:
|
||||
# Create settings widget if is not created yet
|
||||
if self._settings_widget is None:
|
||||
self._settings_widget = LocalSettingsWidget(
|
||||
self._scroll_widget
|
||||
)
|
||||
self._scroll_widget.setWidget(self._settings_widget)
|
||||
|
||||
value = get_local_settings()
|
||||
self._settings_widget.update_local_settings(value)
|
||||
|
||||
except Exception as exc:
|
||||
error_msg = str(exc)
|
||||
|
||||
crashed = error_msg is not None
|
||||
# Enable/Disable save button if crashed or not
|
||||
self.save_btn.setEnabled(not crashed)
|
||||
# Show/Hide settings widget if crashed or not
|
||||
if self._settings_widget:
|
||||
self._settings_widget.setVisible(not crashed)
|
||||
|
||||
if not crashed:
|
||||
return
|
||||
|
||||
# Show message with error
|
||||
title = "Something went wrong"
|
||||
msg = (
|
||||
"Bug: Loading of settings failed."
|
||||
" Please contact your project manager or OpenPype team."
|
||||
"\n\nError message:\n{}"
|
||||
).format(error_msg)
|
||||
|
||||
dialog = QtWidgets.QMessageBox(
|
||||
QtWidgets.QMessageBox.Critical,
|
||||
title,
|
||||
msg,
|
||||
QtWidgets.QMessageBox.Ok,
|
||||
self
|
||||
)
|
||||
dialog.exec_()
|
||||
|
||||
def _on_reset_clicked(self):
|
||||
self.reset()
|
||||
|
||||
def _on_save_clicked(self):
|
||||
value = self.settings_widget.settings_value()
|
||||
value = self._settings_widget.settings_value()
|
||||
save_local_settings(value)
|
||||
self.reset()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import json
|
||||
|
||||
from Qt import QtWidgets, QtGui, QtCore
|
||||
from openpype.tools.settings import CHILD_OFFSET
|
||||
from .widgets import ExpandingWidget
|
||||
|
|
@ -125,6 +127,117 @@ class BaseWidget(QtWidgets.QWidget):
|
|||
actions_mapping[action] = remove_from_project_override
|
||||
menu.addAction(action)
|
||||
|
||||
def _copy_value_actions(self, menu):
|
||||
def copy_value():
|
||||
mime_data = QtCore.QMimeData()
|
||||
|
||||
if self.entity.is_dynamic_item or self.entity.is_in_dynamic_item:
|
||||
entity_path = None
|
||||
else:
|
||||
entity_path = "/".join(
|
||||
[self.entity.root_key, self.entity.path]
|
||||
)
|
||||
|
||||
value = self.entity.value
|
||||
# Copy for settings tool
|
||||
settings_data = {
|
||||
"root_key": self.entity.root_key,
|
||||
"value": value,
|
||||
"path": entity_path
|
||||
}
|
||||
settings_encoded_data = QtCore.QByteArray()
|
||||
settings_stream = QtCore.QDataStream(
|
||||
settings_encoded_data, QtCore.QIODevice.WriteOnly
|
||||
)
|
||||
settings_stream.writeQString(json.dumps(settings_data))
|
||||
mime_data.setData(
|
||||
"application/copy_settings_value", settings_encoded_data
|
||||
)
|
||||
|
||||
# Copy as json
|
||||
json_encoded_data = None
|
||||
if isinstance(value, (dict, list)):
|
||||
json_encoded_data = QtCore.QByteArray()
|
||||
json_stream = QtCore.QDataStream(
|
||||
json_encoded_data, QtCore.QIODevice.WriteOnly
|
||||
)
|
||||
json_stream.writeQString(json.dumps(value))
|
||||
|
||||
mime_data.setData("application/json", json_encoded_data)
|
||||
|
||||
# Copy as text
|
||||
if json_encoded_data is None:
|
||||
# Store value as string
|
||||
mime_data.setText(str(value))
|
||||
else:
|
||||
# Store data as json string
|
||||
mime_data.setText(json.dumps(value, indent=4))
|
||||
|
||||
QtWidgets.QApplication.clipboard().setMimeData(mime_data)
|
||||
|
||||
action = QtWidgets.QAction("Copy", menu)
|
||||
return [(action, copy_value)]
|
||||
|
||||
def _paste_value_actions(self, menu):
|
||||
output = []
|
||||
# Allow paste of value only if were copied from this UI
|
||||
mime_data = QtWidgets.QApplication.clipboard().mimeData()
|
||||
mime_value = mime_data.data("application/copy_settings_value")
|
||||
# Skip if there is nothing to do
|
||||
if not mime_value:
|
||||
return output
|
||||
|
||||
settings_stream = QtCore.QDataStream(
|
||||
mime_value, QtCore.QIODevice.ReadOnly
|
||||
)
|
||||
mime_data_value_str = settings_stream.readQString()
|
||||
mime_data_value = json.loads(mime_data_value_str)
|
||||
|
||||
value = mime_data_value["value"]
|
||||
path = mime_data_value["path"]
|
||||
root_key = mime_data_value["root_key"]
|
||||
|
||||
# Try to find matching entity to be able paste values to same spot
|
||||
# - entity can't by dynamic or in dynamic item
|
||||
# - must be in same root entity as source copy
|
||||
# Can't copy system settings <-> project settings
|
||||
matching_entity = None
|
||||
if path and root_key == self.entity.root_key:
|
||||
try:
|
||||
matching_entity = self.entity.get_entity_from_path(path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _set_entity_value(_entity, _value):
|
||||
try:
|
||||
_entity.set(_value)
|
||||
except Exception:
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
dialog.setWindowTitle("Value does not match settings schema")
|
||||
dialog.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
dialog.setText((
|
||||
"Pasted value does not seem to match schema of destination"
|
||||
" settings entity."
|
||||
))
|
||||
dialog.exec_()
|
||||
|
||||
# Simple paste value method
|
||||
def paste_value():
|
||||
_set_entity_value(self.entity, value)
|
||||
|
||||
action = QtWidgets.QAction("Paste", menu)
|
||||
output.append((action, paste_value))
|
||||
|
||||
# Paste value to matchin entity
|
||||
def paste_value_to_path():
|
||||
_set_entity_value(matching_entity, value)
|
||||
|
||||
if matching_entity is not None:
|
||||
action = QtWidgets.QAction("Paste to same place", menu)
|
||||
output.append((action, paste_value_to_path))
|
||||
|
||||
return output
|
||||
|
||||
def show_actions_menu(self, event=None):
|
||||
if event and event.button() != QtCore.Qt.RightButton:
|
||||
return
|
||||
|
|
@ -144,6 +257,15 @@ class BaseWidget(QtWidgets.QWidget):
|
|||
self._add_to_project_override_action(menu, actions_mapping)
|
||||
self._remove_from_project_override_action(menu, actions_mapping)
|
||||
|
||||
ui_actions = []
|
||||
ui_actions.extend(self._copy_value_actions(menu))
|
||||
ui_actions.extend(self._paste_value_actions(menu))
|
||||
if ui_actions:
|
||||
menu.addSeparator()
|
||||
for action, callback in ui_actions:
|
||||
menu.addAction(action)
|
||||
actions_mapping[action] = callback
|
||||
|
||||
if not actions_mapping:
|
||||
action = QtWidgets.QAction("< No action >")
|
||||
actions_mapping[action] = None
|
||||
|
|
|
|||
|
|
@ -183,6 +183,12 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
footer_widget = QtWidgets.QWidget(configurations_widget)
|
||||
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
|
||||
refresh_icon = qtawesome.icon("fa.refresh", color="white")
|
||||
refresh_btn = QtWidgets.QPushButton(footer_widget)
|
||||
refresh_btn.setIcon(refresh_icon)
|
||||
|
||||
footer_layout.addWidget(refresh_btn, 0)
|
||||
|
||||
if self.user_role == "developer":
|
||||
self._add_developer_ui(footer_layout)
|
||||
|
||||
|
|
@ -205,8 +211,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
main_layout.addWidget(configurations_widget, 1)
|
||||
|
||||
save_btn.clicked.connect(self._save)
|
||||
refresh_btn.clicked.connect(self._on_refresh)
|
||||
|
||||
self.save_btn = save_btn
|
||||
self.refresh_btn = refresh_btn
|
||||
self.require_restart_label = require_restart_label
|
||||
self.scroll_widget = scroll_widget
|
||||
self.content_layout = content_layout
|
||||
|
|
@ -220,10 +228,6 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
def _add_developer_ui(self, footer_layout):
|
||||
refresh_icon = qtawesome.icon("fa.refresh", color="white")
|
||||
refresh_button = QtWidgets.QPushButton()
|
||||
refresh_button.setIcon(refresh_icon)
|
||||
|
||||
modify_defaults_widget = QtWidgets.QWidget()
|
||||
modify_defaults_checkbox = QtWidgets.QCheckBox(modify_defaults_widget)
|
||||
modify_defaults_checkbox.setChecked(self._hide_studio_overrides)
|
||||
|
|
@ -235,10 +239,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
modify_defaults_layout.addWidget(label_widget)
|
||||
modify_defaults_layout.addWidget(modify_defaults_checkbox)
|
||||
|
||||
footer_layout.addWidget(refresh_button, 0)
|
||||
footer_layout.addWidget(modify_defaults_widget, 0)
|
||||
|
||||
refresh_button.clicked.connect(self._on_refresh)
|
||||
modify_defaults_checkbox.stateChanged.connect(
|
||||
self._on_modify_defaults
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ class ColorWidget(InputWidget):
|
|||
self._dialog.open()
|
||||
return
|
||||
|
||||
dialog = ColorDialog(self.input_field.color(), self)
|
||||
dialog = ColorDialog(
|
||||
self.input_field.color(), self.entity.use_alpha, self
|
||||
)
|
||||
self._dialog = dialog
|
||||
|
||||
dialog.open()
|
||||
|
|
@ -120,12 +122,12 @@ class ColorViewer(QtWidgets.QWidget):
|
|||
|
||||
|
||||
class ColorDialog(QtWidgets.QDialog):
|
||||
def __init__(self, color=None, parent=None):
|
||||
def __init__(self, color=None, use_alpha=True, parent=None):
|
||||
super(ColorDialog, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("Color picker dialog")
|
||||
|
||||
picker_widget = ColorPickerWidget(color, self)
|
||||
picker_widget = ColorPickerWidget(color, use_alpha, self)
|
||||
|
||||
footer_widget = QtWidgets.QWidget(self)
|
||||
|
||||
|
|
|
|||
|
|
@ -944,10 +944,8 @@ class Window(QtWidgets.QMainWindow):
|
|||
split_widget.addWidget(tasks_widget)
|
||||
split_widget.addWidget(files_widget)
|
||||
split_widget.addWidget(side_panel)
|
||||
split_widget.setStretchFactor(0, 1)
|
||||
split_widget.setStretchFactor(1, 1)
|
||||
split_widget.setStretchFactor(2, 3)
|
||||
split_widget.setStretchFactor(3, 1)
|
||||
split_widget.setSizes([255, 160, 455, 175])
|
||||
|
||||
body_layout.addWidget(split_widget)
|
||||
|
||||
# Add top margin for tasks to align it visually with files as
|
||||
|
|
@ -976,7 +974,7 @@ class Window(QtWidgets.QMainWindow):
|
|||
# Force focus on the open button by default, required for Houdini.
|
||||
files_widget.btn_open.setFocus()
|
||||
|
||||
self.resize(1000, 600)
|
||||
self.resize(1200, 600)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Custom keyPressEvent.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.1.0-nightly.3"
|
||||
__version__ = "3.2.0-nightly.5"
|
||||
|
|
|
|||
|
|
@ -4,35 +4,6 @@ from Qt import QtWidgets, QtCore, QtGui
|
|||
from .color_view import draw_checkerboard_tile
|
||||
|
||||
|
||||
slide_style = """
|
||||
QSlider::groove:horizontal {
|
||||
background: qlineargradient(
|
||||
x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #000, stop: 1 #fff
|
||||
);
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QSlider::handle:horizontal {
|
||||
background: qlineargradient(
|
||||
x1:0, y1:0, x2:1, y2:1, stop:0 #ddd, stop:1 #bbb
|
||||
);
|
||||
border: 1px solid #777;
|
||||
width: 8px;
|
||||
margin-top: -1px;
|
||||
margin-bottom: -1px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QSlider::handle:horizontal:hover {
|
||||
background: qlineargradient(
|
||||
x1:0, y1:0, x2:1, y2:1, stop:0 #eee, stop:1 #ddd
|
||||
);
|
||||
border: 1px solid #444;ff
|
||||
border-radius: 4px;
|
||||
}"""
|
||||
|
||||
|
||||
class AlphaSlider(QtWidgets.QSlider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AlphaSlider, self).__init__(*args, **kwargs)
|
||||
|
|
@ -80,7 +51,7 @@ class AlphaSlider(QtWidgets.QSlider):
|
|||
|
||||
painter.fillRect(event.rect(), QtCore.Qt.transparent)
|
||||
|
||||
painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)
|
||||
painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
|
||||
rect = self.style().subControlRect(
|
||||
QtWidgets.QStyle.CC_Slider,
|
||||
opt,
|
||||
|
|
@ -135,19 +106,8 @@ class AlphaSlider(QtWidgets.QSlider):
|
|||
|
||||
painter.save()
|
||||
|
||||
gradient = QtGui.QRadialGradient()
|
||||
radius = handle_rect.height() / 2
|
||||
center_x = handle_rect.width() / 2 + handle_rect.x()
|
||||
center_y = handle_rect.height()
|
||||
gradient.setCenter(center_x, center_y)
|
||||
gradient.setCenterRadius(radius)
|
||||
gradient.setFocalPoint(center_x, center_y)
|
||||
|
||||
gradient.setColorAt(0.9, QtGui.QColor(127, 127, 127))
|
||||
gradient.setColorAt(1, QtCore.Qt.transparent)
|
||||
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.setBrush(gradient)
|
||||
painter.setBrush(QtGui.QColor(127, 127, 127))
|
||||
painter.drawEllipse(handle_rect)
|
||||
|
||||
painter.restore()
|
||||
|
|
|
|||
|
|
@ -17,19 +17,12 @@ from .color_inputs import (
|
|||
class ColorPickerWidget(QtWidgets.QWidget):
|
||||
color_changed = QtCore.Signal(QtGui.QColor)
|
||||
|
||||
def __init__(self, color=None, parent=None):
|
||||
def __init__(self, color=None, use_alpha=True, parent=None):
|
||||
super(ColorPickerWidget, self).__init__(parent)
|
||||
|
||||
# Color triangle
|
||||
color_triangle = QtColorTriangle(self)
|
||||
|
||||
alpha_slider_proxy = QtWidgets.QWidget(self)
|
||||
alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, alpha_slider_proxy)
|
||||
|
||||
alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy)
|
||||
alpha_slider_layout.setContentsMargins(5, 5, 5, 5)
|
||||
alpha_slider_layout.addWidget(alpha_slider, 1)
|
||||
|
||||
# Eye picked widget
|
||||
pick_widget = PickScreenColorWidget()
|
||||
pick_widget.setMaximumHeight(50)
|
||||
|
|
@ -47,8 +40,6 @@ class ColorPickerWidget(QtWidgets.QWidget):
|
|||
color_view = ColorViewer(self)
|
||||
color_view.setMaximumHeight(50)
|
||||
|
||||
alpha_inputs = AlphaInputs(self)
|
||||
|
||||
color_inputs_color = QtGui.QColor()
|
||||
col_inputs_by_label = [
|
||||
("HEX", HEXInputs(color_inputs_color, self)),
|
||||
|
|
@ -58,6 +49,7 @@ class ColorPickerWidget(QtWidgets.QWidget):
|
|||
]
|
||||
|
||||
layout = QtWidgets.QGridLayout(self)
|
||||
|
||||
empty_col = 1
|
||||
label_col = empty_col + 1
|
||||
input_col = label_col + 1
|
||||
|
|
@ -65,6 +57,9 @@ class ColorPickerWidget(QtWidgets.QWidget):
|
|||
empty_widget.setFixedWidth(10)
|
||||
layout.addWidget(empty_widget, 0, empty_col)
|
||||
|
||||
layout.setColumnStretch(0, 1)
|
||||
layout.setColumnStretch(input_col, 1)
|
||||
|
||||
row = 0
|
||||
layout.addWidget(btn_pick_color, row, label_col)
|
||||
layout.addWidget(color_view, row, input_col)
|
||||
|
|
@ -84,20 +79,41 @@ class ColorPickerWidget(QtWidgets.QWidget):
|
|||
layout.setRowStretch(row, 1)
|
||||
row += 1
|
||||
|
||||
layout.addWidget(alpha_slider_proxy, row, 0)
|
||||
alpha_label = None
|
||||
alpha_slider_proxy = None
|
||||
alpha_slider = None
|
||||
alpha_inputs = None
|
||||
if not use_alpha:
|
||||
color.setAlpha(255)
|
||||
else:
|
||||
alpha_inputs = AlphaInputs(self)
|
||||
alpha_label = QtWidgets.QLabel("Alpha", self)
|
||||
alpha_slider_proxy = QtWidgets.QWidget(self)
|
||||
alpha_slider = AlphaSlider(
|
||||
QtCore.Qt.Horizontal, alpha_slider_proxy
|
||||
)
|
||||
|
||||
alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy)
|
||||
alpha_slider_layout.setContentsMargins(5, 5, 5, 5)
|
||||
alpha_slider_layout.addWidget(alpha_slider, 1)
|
||||
|
||||
layout.addWidget(alpha_slider_proxy, row, 0)
|
||||
|
||||
layout.addWidget(alpha_label, row, label_col)
|
||||
layout.addWidget(alpha_inputs, row, input_col)
|
||||
|
||||
row += 1
|
||||
|
||||
layout.addWidget(QtWidgets.QLabel("Alpha", self), row, label_col)
|
||||
layout.addWidget(alpha_inputs, row, input_col)
|
||||
row += 1
|
||||
layout.setRowStretch(row, 1)
|
||||
|
||||
color_view.set_color(color_triangle.cur_color)
|
||||
|
||||
color_triangle.color_changed.connect(self.triangle_color_changed)
|
||||
alpha_slider.valueChanged.connect(self._on_alpha_slider_change)
|
||||
pick_widget.color_selected.connect(self.on_color_change)
|
||||
alpha_inputs.alpha_changed.connect(self._on_alpha_inputs_changed)
|
||||
btn_pick_color.released.connect(self.pick_color)
|
||||
if alpha_slider:
|
||||
alpha_slider.valueChanged.connect(self._on_alpha_slider_change)
|
||||
alpha_inputs.alpha_changed.connect(self._on_alpha_inputs_changed)
|
||||
|
||||
self.color_input_fields = color_input_fields
|
||||
self.color_inputs_color = color_inputs_color
|
||||
|
|
@ -131,7 +147,8 @@ class ColorPickerWidget(QtWidgets.QWidget):
|
|||
return self.color_view.color()
|
||||
|
||||
def set_color(self, color):
|
||||
self.alpha_inputs.set_alpha(color.alpha())
|
||||
if self.alpha_inputs:
|
||||
self.alpha_inputs.set_alpha(color.alpha())
|
||||
self.on_color_change(color)
|
||||
|
||||
def pick_color(self):
|
||||
|
|
@ -163,10 +180,10 @@ class ColorPickerWidget(QtWidgets.QWidget):
|
|||
|
||||
def alpha_changed(self, value):
|
||||
self.color_view.set_alpha(value)
|
||||
if self.alpha_slider.value() != value:
|
||||
if self.alpha_slider and self.alpha_slider.value() != value:
|
||||
self.alpha_slider.setValue(value)
|
||||
|
||||
if self.alpha_inputs.alpha_value != value:
|
||||
if self.alpha_inputs and self.alpha_inputs.alpha_value != value:
|
||||
self.alpha_inputs.set_alpha(value)
|
||||
|
||||
def _on_alpha_inputs_changed(self, value):
|
||||
|
|
|
|||
|
|
@ -241,7 +241,11 @@ class QtColorTriangle(QtWidgets.QWidget):
|
|||
|
||||
# Blit the static generated background with the hue gradient onto
|
||||
# the double buffer.
|
||||
buf = QtGui.QImage(self.bg_image.copy())
|
||||
buf = QtGui.QImage(
|
||||
self.bg_image.width(),
|
||||
self.bg_image.height(),
|
||||
QtGui.QImage.Format_RGB32
|
||||
)
|
||||
|
||||
# Draw the trigon
|
||||
# Find the color with only the hue, and max value and saturation
|
||||
|
|
@ -254,9 +258,21 @@ class QtColorTriangle(QtWidgets.QWidget):
|
|||
)
|
||||
|
||||
# Slow step: convert the image to a pixmap
|
||||
pix = QtGui.QPixmap.fromImage(buf)
|
||||
pix = self.bg_image.copy()
|
||||
pix_painter = QtGui.QPainter(pix)
|
||||
pix_painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
|
||||
pix_painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
|
||||
|
||||
trigon_path = QtGui.QPainterPath()
|
||||
trigon_path.moveTo(self.point_a)
|
||||
trigon_path.lineTo(self.point_b)
|
||||
trigon_path.lineTo(self.point_c)
|
||||
trigon_path.closeSubpath()
|
||||
pix_painter.setClipPath(trigon_path)
|
||||
|
||||
pix_painter.drawImage(0, 0, buf)
|
||||
|
||||
pix_painter.setClipping(False)
|
||||
|
||||
# Draw an outline of the triangle
|
||||
pix_painter.setPen(self._triangle_outline_pen)
|
||||
|
|
@ -724,27 +740,37 @@ class QtColorTriangle(QtWidgets.QWidget):
|
|||
lx = leftX[y]
|
||||
rx = rightX[y]
|
||||
|
||||
# if the xdist is 0, don't draw anything.
|
||||
xdist = rx - lx
|
||||
if xdist == 0.0:
|
||||
continue
|
||||
|
||||
lxi = int(floor(lx))
|
||||
rxi = int(floor(rx))
|
||||
rc = rightColors[y]
|
||||
lc = leftColors[y]
|
||||
|
||||
# if the xdist is 0, don't draw anything.
|
||||
xdist = rx - lx
|
||||
if xdist != 0.0:
|
||||
r = lc.r
|
||||
g = lc.g
|
||||
b = lc.b
|
||||
rdelta = (rc.r - r) / xdist
|
||||
gdelta = (rc.g - g) / xdist
|
||||
bdelta = (rc.b - b) / xdist
|
||||
r = lc.r
|
||||
g = lc.g
|
||||
b = lc.b
|
||||
rdelta = (rc.r - r) / xdist
|
||||
gdelta = (rc.g - g) / xdist
|
||||
bdelta = (rc.b - b) / xdist
|
||||
|
||||
# Inner loop 2. Draws the line from left to right.
|
||||
for x in range(lxi, rxi + 1):
|
||||
buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b)))
|
||||
r += rdelta
|
||||
g += gdelta
|
||||
b += bdelta
|
||||
# Draw 2 more pixels on left side for smoothing
|
||||
for x in range(lxi - 2, lxi):
|
||||
buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b)))
|
||||
|
||||
# Inner loop 2. Draws the line from left to right.
|
||||
for x in range(lxi, rxi):
|
||||
buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b)))
|
||||
r += rdelta
|
||||
g += gdelta
|
||||
b += bdelta
|
||||
|
||||
# Draw 2 more pixels on right side for smoothing
|
||||
for x in range(rxi, rxi + 3):
|
||||
buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b)))
|
||||
|
||||
def _radius_at(self, pos, rect):
|
||||
mousexdist = pos.x() - float(rect.center().x())
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ def draw_checkerboard_tile(piece_size=None, color_1=None, color_2=None):
|
|||
if piece_size is None:
|
||||
piece_size = 7
|
||||
|
||||
# Make sure piece size is not float
|
||||
piece_size = int(piece_size)
|
||||
if color_1 is None:
|
||||
color_1 = QtGui.QColor(188, 188, 188)
|
||||
|
||||
|
|
|
|||
2
poetry.lock
generated
2
poetry.lock
generated
|
|
@ -11,7 +11,7 @@ develop = false
|
|||
type = "git"
|
||||
url = "https://github.com/pypeclub/acre.git"
|
||||
reference = "master"
|
||||
resolved_reference = "efc1b8faa8f84568538b936688ae6f7604dd194c"
|
||||
resolved_reference = "68784b7eb5b7bb5f409b61ab31d4403878a3e1b7"
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit efde026e5aad72dac0e69848005419e2c4f067f2
|
||||
Subproject commit d8be0bdb37961e32243f1de0eb9696e86acf7443
|
||||
|
|
@ -92,6 +92,24 @@ def calculate_next_nightly(token="nightly"):
|
|||
next_tag = last_pre_v.bump_prerelease(token=token).__str__()
|
||||
return next_tag
|
||||
|
||||
def finalize_latest_nightly():
|
||||
last_prerelease, last_pre_tag = get_last_version("CI")
|
||||
last_pre_v = VersionInfo.parse(last_prerelease)
|
||||
last_pre_v_finalized = last_pre_v.finalize_version()
|
||||
# print(last_pre_v_finalized)
|
||||
|
||||
return last_pre_v_finalized.__str__()
|
||||
|
||||
def finalize_prerelease(prerelease):
|
||||
|
||||
if "/" in prerelease:
|
||||
prerelease = prerelease.split("/")[-1]
|
||||
|
||||
prerelease_v = VersionInfo.parse(prerelease)
|
||||
prerelease_v_finalized = prerelease_v.finalize_version()
|
||||
|
||||
return prerelease_v_finalized.__str__()
|
||||
|
||||
|
||||
def main():
|
||||
usage = "usage: %prog [options] arg"
|
||||
|
|
@ -102,12 +120,22 @@ def main():
|
|||
parser.add_option("-b", "--bump",
|
||||
dest="bump", action="store_true",
|
||||
help="Return if there is something to bump")
|
||||
parser.add_option("-v", "--version",
|
||||
dest="version", action="store",
|
||||
help="work with explicit version")
|
||||
parser.add_option("-r", "--release-latest",
|
||||
dest="releaselatest", action="store_true",
|
||||
help="finalize latest prerelease to a release")
|
||||
parser.add_option("-p", "--prerelease",
|
||||
dest="prerelease", action="store",
|
||||
help="define prerelease token")
|
||||
parser.add_option("-f", "--finalize",
|
||||
dest="finalize", action="store",
|
||||
help="define prerelease token")
|
||||
parser.add_option("-v", "--version",
|
||||
dest="version", action="store",
|
||||
help="work with explicit version")
|
||||
parser.add_option("-l", "--lastversion",
|
||||
dest="lastversion", action="store",
|
||||
help="work with explicit version")
|
||||
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
|
|
@ -124,6 +152,25 @@ def main():
|
|||
print(next_tag_v)
|
||||
bump_file_versions(next_tag_v)
|
||||
|
||||
if options.finalize:
|
||||
new_release = finalize_prerelease(options.finalize)
|
||||
print(new_release)
|
||||
bump_file_versions(new_release)
|
||||
|
||||
if options.lastversion:
|
||||
last_release, last_release_tag = get_last_version(options.lastversion)
|
||||
print(last_release_tag)
|
||||
|
||||
if options.releaselatest:
|
||||
new_release = finalize_latest_nightly()
|
||||
last_release, last_release_tag = get_last_version("release")
|
||||
|
||||
if VersionInfo.parse(new_release) > VersionInfo.parse(last_release):
|
||||
print(new_release)
|
||||
bump_file_versions(new_release)
|
||||
else:
|
||||
print("skip")
|
||||
|
||||
if options.prerelease:
|
||||
current_prerelease = VersionInfo.parse(options.prerelease)
|
||||
new_prerelease = current_prerelease.bump_prerelease().__str__()
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ All of them are Project based, eg. each project could have different configurati
|
|||
|
||||
Location: Settings > Project > AfterEffects
|
||||
|
||||

|
||||

|
||||
|
||||
## Publish plugins
|
||||
|
||||
|
|
|
|||
30
website/docs/admin_hosts_tvpaint.md
Normal file
30
website/docs/admin_hosts_tvpaint.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
id: admin_hosts_tvpaint
|
||||
title: TVPaint
|
||||
sidebar_label: TVPaint
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## Subset name templates
|
||||
Definition of possibile subset name templates in TVPaint integration.
|
||||
|
||||
### [Render Layer](artist_hosts_tvpaint#render-layer)
|
||||
Render layer has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**.
|
||||
|
||||
- Key **render_layer** is alias for variant (user's input).
|
||||
- For key **render_pass** is used predefined value `"Beauty"` (ATM value can't be changed).
|
||||
|
||||
### [Render pass](artist_hosts_tvpaint#render-pass)
|
||||
Render pass has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**.
|
||||
- Key **render_layer** is filled with value of **render_pass** from `renderLayer` group.
|
||||
- Key **render_pass** is alias for variant (user's input).
|
||||
|
||||
:::important Render Layer/Pass templates
|
||||
It is recommended to use same subset name template for both **renderLayer** and **renderPass** families.
|
||||
- Example template: `"{family}{Task}_{Render_layer}_{Render_pass}"`
|
||||
:::
|
||||
|
||||
### [Review](artist_hosts_tvpaint#review) and [Workfile](artist_hosts_tvpaint#workfile)
|
||||
Families **review** and **workfile** are not manually created but are automatically generated during publishing. That's why it is recommended to not use **variant** key in their subset name template.
|
||||
|
|
@ -45,7 +45,7 @@ In TVPaint you can find the Tools in OpenPype menu extension. The OpenPype Tools
|
|||
|
||||
|
||||
## Create
|
||||
In TVPaint you can create and publish **[Reviews](#review)**, **[Render Passes](#render-pass)**, and **[Render Layers](#render-layer)**.
|
||||
In TVPaint you can create and publish **[Reviews](#review)**, **[Workfile](#workfile)**, **[Render Passes](#render-pass)** and **[Render Layers](#render-layer)**.
|
||||
|
||||
You have the possibility to organize your layers by using `Color group`.
|
||||
|
||||
|
|
@ -67,26 +67,13 @@ OpenPype specifically never tries to guess what you want to publish from the sce
|
|||
|
||||
When you want to publish `review` or `render layer` or `render pass`, open the `Creator` through the Tools menu `Create` button.
|
||||
|
||||
### Review
|
||||
### Review
|
||||
`Review` renders the whole file as is and sends the resulting QuickTime to Ftrack.
|
||||
- Is automatically created during publishing.
|
||||
|
||||
<div class="row markdown">
|
||||
<div class="col col--6 markdown">
|
||||
|
||||
`Review` renders the whole file as is and sends the resulting QuickTime to Ftrack.
|
||||
|
||||
To create reviewable quicktime of your animation:
|
||||
|
||||
- select `Review` in the `Creator`
|
||||
- press `Create`
|
||||
- When you run [publish](#publish), file will be rendered and converted to quicktime.`
|
||||
|
||||
</div>
|
||||
<div class="col col--6 markdown">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
</div>
|
||||
### Workfile
|
||||
`Workfile` stores the source workfile as is during publishing (e.g. for backup).
|
||||
- Is automatically created during publishing.
|
||||
|
||||
### Render Layer
|
||||
|
||||
|
|
|
|||
BIN
website/docs/assets/ftrack/ftrack-collect-advanced.png
Normal file
BIN
website/docs/assets/ftrack/ftrack-collect-advanced.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue