diff --git a/.github/workflows/nightly_merge.yml b/.github/workflows/nightly_merge.yml new file mode 100644 index 0000000000..676b00a351 --- /dev/null +++ b/.github/workflows/nightly_merge.yml @@ -0,0 +1,23 @@ +name: Nightly Merge + +on: + schedule: + - cron: '21 3 * * 3,6' + workflow_dispatch: + +jobs: + develop-to-main: + + runs-on: ubuntu-latest + + steps: + - name: 🚛 Checkout Code + uses: actions/checkout@v2 + + - name: Merge development -> main + uses: devmasx/merge-branch@v1.3.1 + with: + type: now + from_branch: develop + target_branch: main + github_token: ${{ secrets.TOKEN }} \ No newline at end of file diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 0000000000..0e6c7b5da9 --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,93 @@ +name: Nightly Prerelease + +on: + push: + branches: [main] + + +jobs: + create_nightly: + runs-on: ubuntu-latest + + steps: + - name: 🚛 Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - name: Install Python requirements + run: pip install gitpython semver + + - name: 🔎 Determine next version type + id: version_type + run: | + TYPE=$(python ./tools/ci_tools.py --bump) + + echo ::set-output name=type::$TYPE + + - name: 💉 Inject new version into files + id: version + if: steps.version_type.outputs.type != 'skip' + run: | + RESULT=$(python ./tools/ci_tools.py --nightly) + + echo ::set-output name=next_tag::$RESULT + + - name: "✏️ Generate full changelog" + if: steps.version_type.outputs.type != 'skip' + id: generate-full-changelog + uses: heinrichreimer/github-changelog-generator-action@v2.2 + with: + token: ${{ secrets.GITHUB_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 + pullRequests: true + prWoLabels: false + author: false + unreleased: true + compareLink: true + stripGeneratorNotice: true + verbose: true + unreleasedLabel: ${{ steps.version.outputs.next_tag }} + excludeTagsRegex: "CI/.+" + releaseBranch: "main" + + - name: "🖨️ Print changelog to console" + if: steps.version_type.outputs.type != 'skip' + run: cat CHANGELOG.md + + - name: 💾 Commit and Tag + id: git_commit + if: steps.version_type.outputs.type != 'skip' + run: | + git config user.email ${{ secrets.CI_EMAIL }} + git config user.name ${{ secrets.CI_USER }} + cd repos/avalon-core + git checkout main + git pull + cd ../.. + git add . + git commit -m "[Automated] Bump version" + tag_name="CI/${{ steps.version.outputs.next_tag }}" + git tag -a $tag_name -m "nightly build" + git push + git push origin $tag_name + + - name: 🔨 Merge main back to develop + uses: everlytic/branch-merge@1.1.0 + if: steps.version_type.outputs.type != 'skip' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + source_ref: 'main' + target_branch: 'develop' + commit_message_template: '[Automated] Merged {source_ref} into {target_branch}' \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..782c9c8dda --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,99 @@ +name: Stable Release + +on: + push: + tags: + - '*[0-9].*[0-9].*[0-9]*' + +jobs: + create_release: + runs-on: ubuntu-latest + + steps: + - name: 🚛 Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - 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 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 }} + + - name: "✏️ Generate full changelog" + if: steps.version_type.outputs.type != 'skip' + id: generate-full-changelog + uses: heinrichreimer/github-changelog-generator-action@v2.2 + with: + token: ${{ secrets.GITHUB_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 + pullRequests: true + prWoLabels: false + author: false + unreleased: true + compareLink: true + stripGeneratorNotice: true + verbose: true + futureRelease: ${{ env.RELEASE_VERSION }} + 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' + run: | + git add . + git commit -m "[Automated] Release" + tag_name="${{ env.RELEASE_VERSION }}" + git push + git tag -fa $tag_name -m "stable release" + git push origin $tag_name + + - name: "🚀 Github Release" + uses: docker://antonyurchenko/git-release:latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRAFT_RELEASE: "false" + PRE_RELEASE: "false" + CHANGELOG_FILE: "CHANGELOG.md" + ALLOW_EMPTY_CHANGELOG: "false" + ALLOW_TAG_PREFIX: "true" + + + - name: 🔨 Merge main back to develop + uses: everlytic/branch-merge@1.1.0 + if: steps.version_type.outputs.type != 'skip' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + source_ref: 'main' + target_branch: 'develop' + commit_message_template: '[Automated] Merged release {source_ref} into {target_branch}' \ No newline at end of file diff --git a/.github_changelog_generator b/.github_changelog_generator deleted file mode 100644 index ac934ff84f..0000000000 --- a/.github_changelog_generator +++ /dev/null @@ -1,9 +0,0 @@ -pr-wo-labels=False -exclude-labels=duplicate,question,invalid,wontfix,weekly-digest -author=False -unreleased=True -since-tag=2.13.6 -release-branch=master -enhancement-label=**Enhancements:** -issues=False -pulls=False diff --git a/.gitignore b/.gitignore index 26bf7cf65f..754e3698e2 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ Temporary Items # CX_Freeze ########### /build +/dist/ /vendor/bin/* /.venv diff --git a/.gitmodules b/.gitmodules index 7e6b7cb861..52f2fc0750 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,6 @@ [submodule "repos/avalon-core"] path = repos/avalon-core url = https://github.com/pypeclub/avalon-core.git - branch = develop [submodule "repos/avalon-unreal-integration"] path = repos/avalon-unreal-integration url = https://github.com/pypeclub/avalon-unreal-integration.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 09882896f4..ae4492bd7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,352 @@ # Changelog -## [2.18.0](https://github.com/pypeclub/openpype/tree/2.18.0) (2021-05-18) -[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...2.18.0) +## [3.0.0](https://github.com/pypeclub/openpype/tree/3.0.0) -**Enhancements:** +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.6...3.0.0) -- Use SubsetLoader and multiple contexts for delete_old_versions [\#1484](ttps://github.com/pypeclub/OpenPype/pull/1484)) -- TVPaint: Increment workfile version on successfull publish. [\#1489](https://github.com/pypeclub/OpenPype/pull/1489) -- Maya: Use of multiple deadline servers [\#1483](https://github.com/pypeclub/OpenPype/pull/1483) +### Configuration +- Studio Settings GUI: no more json configuration files. +- OpenPype Modules can be turned on and off. +- Easy to add Application versions. +- Per Project Environment and plugin management. +- Robust profile system for creating reviewables and burnins, with filtering based on Application, Task and data family. +- Configurable publish plugins. +- Options to make any validator or extractor, optional or disabled. +- Color Management is now unified under anatomy settings. +- Subset naming and grouping is fully configurable. +- All project attributes can now be set directly in OpenPype settings. +- Studio Setting can be locked to prevent unwanted artist changes. +- You can now add per project and per task type templates for workfile initialization in most hosts. +- Too many other individual configurable option to list in this changelog :) + +### Local Settings +- Local Settings GUI where users can change certain option on individual basis. + - Application executables. + - Project roots. + - Project site sync settings. + +### Build, Installation and Deployments +- No requirements on artist machine. +- Fully distributed workflow possible. +- Self-contained installation. +- Available on all three major platforms. +- Automatic artist OpenPype updates. +- Studio OpenPype repository for updates distribution. +- Robust Build system. +- Safe studio update versioning with staging and production options. +- MacOS build generates .app and .dmg installer. +- Windows build with installer creation script. + +### Misc +- System and diagnostic info tool in the tray. +- Launching application from Launcher indicates activity. +- All project roots are now named. Single root project are now achieved by having a single named root in the project anatomy. +- Every project root is cast into environment variable as well, so it can be used in DCC instead of absolute path (depends on DCC support for env vars). +- Basic support for task types, on top of task names. +- Timer now change automatically when the context is switched inside running application. +- 'Master" versions have been renamed to "Hero". +- Extract Burnins now supports file sequences and color settings. +- Extract Review support overscan cropping, better letterboxes and background colour fill. +- Delivery tool for copying and renaming any published assets in bulk. +- Harmony, Photoshop and After Effects now connect directly with OpenPype tray instead of spawning their own terminal. + +### Project Manager GUI +- Create Projects. +- Create Shots and Assets. +- Create Tasks and assign task types. +- Fill required asset attributes. +- Validations for duplicated or unsupported names. +- Archive Assets. +- Move Asset within hierarchy. + +### Site Sync (beta) +- Synchronization of published files between workstations and central storage. +- Ability to add arbitrary storage providers to the Site Sync system. +- Default setup includes Disk and Google Drive providers as examples. +- Access to availability information from Loader and Scene Manager. +- Sync queue GUI with filtering, error and status reporting. +- Site sync can be configured on a per-project basis. +- Bulk upload and download from the loader. + +### Ftrack +- Actions have customisable roles. +- Settings on all actions are updated live and don't need openpype restart. +- Ftrack module can now be turned off completely. +- It is enough to specify ftrack server name and the URL will be formed correctly. So instead of mystudio.ftrackapp.com, it's possible to use simply: "mystudio". + +### Editorial +- Fully OTIO based editorial publishing. +- Completely re-done Hiero publishing to be a lot simpler and faster. +- Consistent conforming from Resolve, Hiero and Standalone Publisher. + +### Backend +- OpenPype and Avalon now always share the same database (in 2.x is was possible to split them). +- Major codebase refactoring to allow for better CI, versioning and control of individual integrations. +- OTIO is bundled with build. +- OIIO is bundled with build. +- FFMPEG is bundled with build. +- Rest API and host WebSocket servers have been unified into a single local webserver. +- Maya look assigner has been integrated into the main codebase. +- Publish GUI has been integrated into the main codebase. +- Studio and Project settings overrides are now stored in Mongo. +- Too many other backend fixes and tweaks to list :), you can see full changelog on github for those. +- OpenPype uses Poetry to manage it's virtual environment when running from code. +- all applications can be marked as python 2 or 3 compatible to make the switch a bit easier. + + +### Pull Requests since 3.0.0-rc.6 + + +**Implemented enhancements:** + +- settings: task types enum entity [\#1605](https://github.com/pypeclub/OpenPype/issues/1605) +- Settings: ignore keys in referenced schema [\#1600](https://github.com/pypeclub/OpenPype/issues/1600) +- Maya: support for frame steps and frame lists [\#1585](https://github.com/pypeclub/OpenPype/issues/1585) +- TVPaint: Publish workfile. [\#1548](https://github.com/pypeclub/OpenPype/issues/1548) +- Loader: Current Asset Button [\#1448](https://github.com/pypeclub/OpenPype/issues/1448) +- Hiero: publish with retiming [\#1377](https://github.com/pypeclub/OpenPype/issues/1377) +- Ask user to restart after changing global environments in settings [\#910](https://github.com/pypeclub/OpenPype/issues/910) +- add option to define paht to workfile template [\#895](https://github.com/pypeclub/OpenPype/issues/895) +- Harmony: move server console to system tray [\#676](https://github.com/pypeclub/OpenPype/issues/676) +- Standalone style [\#1630](https://github.com/pypeclub/OpenPype/pull/1630) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Faster hierarchical values push [\#1627](https://github.com/pypeclub/OpenPype/pull/1627) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Launcher tool style [\#1624](https://github.com/pypeclub/OpenPype/pull/1624) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Loader and Library loader enhancements [\#1623](https://github.com/pypeclub/OpenPype/pull/1623) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Tray style [\#1622](https://github.com/pypeclub/OpenPype/pull/1622) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Maya schemas cleanup [\#1610](https://github.com/pypeclub/OpenPype/pull/1610) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Settings: ignore keys in referenced schema [\#1608](https://github.com/pypeclub/OpenPype/pull/1608) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- settings: task types enum entity [\#1606](https://github.com/pypeclub/OpenPype/pull/1606) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Openpype style [\#1604](https://github.com/pypeclub/OpenPype/pull/1604) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- TVPaint: Publish workfile. [\#1597](https://github.com/pypeclub/OpenPype/pull/1597) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Nuke: add option to define path to workfile template [\#1571](https://github.com/pypeclub/OpenPype/pull/1571) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Crop overscan in Extract Review [\#1569](https://github.com/pypeclub/OpenPype/pull/1569) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Unreal and Blender: Material Workflow [\#1562](https://github.com/pypeclub/OpenPype/pull/1562) ([simonebarbieri](https://github.com/simonebarbieri)) +- Harmony: move server console to system tray [\#1560](https://github.com/pypeclub/OpenPype/pull/1560) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Ask user to restart after changing global environments in settings [\#1550](https://github.com/pypeclub/OpenPype/pull/1550) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Hiero: publish with retiming [\#1545](https://github.com/pypeclub/OpenPype/pull/1545) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) **Fixed bugs:** -- Use instance frame start instead of timeline. [\#1486](https://github.com/pypeclub/OpenPype/pull/1486) -- Maya: Redshift - set proper start frame on proxy [\#1480](https://github.com/pypeclub/OpenPype/pull/1480) -- Maya: wrong collection of playblasted frames [\#1517](https://github.com/pypeclub/OpenPype/pull/1517) -- Existing subsets hints in creator [\#1502](https://github.com/pypeclub/OpenPype/pull/1502) +- Library loader load asset documents on OpenPype start [\#1603](https://github.com/pypeclub/OpenPype/issues/1603) +- Resolve: unable to load the same footage twice [\#1317](https://github.com/pypeclub/OpenPype/issues/1317) +- Resolve: unable to load footage [\#1316](https://github.com/pypeclub/OpenPype/issues/1316) +- Add required Python 2 modules [\#1291](https://github.com/pypeclub/OpenPype/issues/1291) +- GUi scaling with hires displays [\#705](https://github.com/pypeclub/OpenPype/issues/705) +- Maya: non unicode string in publish validation [\#673](https://github.com/pypeclub/OpenPype/issues/673) +- Nuke: Rendered Frame validation is triggered by multiple collections [\#156](https://github.com/pypeclub/OpenPype/issues/156) +- avalon-core debugging failing [\#80](https://github.com/pypeclub/OpenPype/issues/80) +- Only check arnold shading group if arnold is used [\#72](https://github.com/pypeclub/OpenPype/issues/72) +- Sync server Qt layout fix [\#1621](https://github.com/pypeclub/OpenPype/pull/1621) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Console Listener on Python 2 fix [\#1620](https://github.com/pypeclub/OpenPype/pull/1620) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Bug: Initialize blessed term only in console mode [\#1619](https://github.com/pypeclub/OpenPype/pull/1619) ([antirotor](https://github.com/antirotor)) +- Settings template skip paths support wrappers [\#1618](https://github.com/pypeclub/OpenPype/pull/1618) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Maya capture 'isolate\_view' fix + minor corrections [\#1617](https://github.com/pypeclub/OpenPype/pull/1617) ([2-REC](https://github.com/2-REC)) +- MacOs Fix launch of standalone publisher [\#1616](https://github.com/pypeclub/OpenPype/pull/1616) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- 'Delivery action' report fix + typos [\#1612](https://github.com/pypeclub/OpenPype/pull/1612) ([2-REC](https://github.com/2-REC)) +- List append fix in mutable dict settings [\#1599](https://github.com/pypeclub/OpenPype/pull/1599) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Documentation: Maya: fix review [\#1598](https://github.com/pypeclub/OpenPype/pull/1598) ([antirotor](https://github.com/antirotor)) +- Bugfix: Set certifi CA bundle for all platforms [\#1596](https://github.com/pypeclub/OpenPype/pull/1596) ([antirotor](https://github.com/antirotor)) + +**Merged pull requests:** + +- Bump dns-packet from 1.3.1 to 1.3.4 in /website [\#1611](https://github.com/pypeclub/OpenPype/pull/1611) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Maya: Render workflow fixes [\#1607](https://github.com/pypeclub/OpenPype/pull/1607) ([antirotor](https://github.com/antirotor)) +- Maya: support for frame steps and frame lists [\#1586](https://github.com/pypeclub/OpenPype/pull/1586) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- 3.0.0 - curated changelog [\#1284](https://github.com/pypeclub/OpenPype/pull/1284) ([mkolar](https://github.com/mkolar)) + +## [2.18.1](https://github.com/pypeclub/openpype/tree/2.18.1) (2021-06-03) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.0...2.18.1) + +**Enhancements:** + +- Faster hierarchical values push [\#1626](https://github.com/pypeclub/OpenPype/pull/1626) +- Feature Delivery in library loader [\#1549](https://github.com/pypeclub/OpenPype/pull/1549) +- Hiero: Initial frame publish support. [\#1172](https://github.com/pypeclub/OpenPype/pull/1172) + +**Fixed bugs:** + +- Maya capture 'isolate\_view' fix + minor corrections [\#1614](https://github.com/pypeclub/OpenPype/pull/1614) +- 'Delivery action' report fix +typos [\#1613](https://github.com/pypeclub/OpenPype/pull/1613) +- Delivery in LibraryLoader - fixed sequence issue [\#1590](https://github.com/pypeclub/OpenPype/pull/1590) +- FFmpeg filters in quote marks [\#1588](https://github.com/pypeclub/OpenPype/pull/1588) +- Ftrack delete action cause circular error [\#1581](https://github.com/pypeclub/OpenPype/pull/1581) +- Fix Maya playblast. [\#1566](https://github.com/pypeclub/OpenPype/pull/1566) +- More failsafes prevent errored runs. [\#1554](https://github.com/pypeclub/OpenPype/pull/1554) +- Celaction publishing [\#1539](https://github.com/pypeclub/OpenPype/pull/1539) +- celaction: app not starting [\#1533](https://github.com/pypeclub/OpenPype/pull/1533) + +**Merged pull requests:** + +- Maya: Render workflow fixes - 2.0 backport [\#1609](https://github.com/pypeclub/OpenPype/pull/1609) +- Maya Hardware support [\#1553](https://github.com/pypeclub/OpenPype/pull/1553) + + +## [CI/3.0.0-rc.6](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.6) (2021-05-27) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.5...CI/3.0.0-rc.6) + +**Implemented enhancements:** + +- Hiero: publish color and transformation soft-effects [\#1376](https://github.com/pypeclub/OpenPype/issues/1376) +- Get rid of `AVALON\_HIERARCHY` and `hiearchy` key on asset [\#432](https://github.com/pypeclub/OpenPype/issues/432) +- Sync to avalon do not store hierarchy key [\#1582](https://github.com/pypeclub/OpenPype/pull/1582) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Tools: launcher scripts for project manager [\#1557](https://github.com/pypeclub/OpenPype/pull/1557) ([antirotor](https://github.com/antirotor)) +- Simple tvpaint publish [\#1555](https://github.com/pypeclub/OpenPype/pull/1555) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Feature Delivery in library loader [\#1546](https://github.com/pypeclub/OpenPype/pull/1546) ([kalisp](https://github.com/kalisp)) +- Documentation: Dev and system build documentation [\#1543](https://github.com/pypeclub/OpenPype/pull/1543) ([antirotor](https://github.com/antirotor)) +- Color entity [\#1542](https://github.com/pypeclub/OpenPype/pull/1542) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Extract review bg color [\#1534](https://github.com/pypeclub/OpenPype/pull/1534) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- TVPaint loader settings [\#1530](https://github.com/pypeclub/OpenPype/pull/1530) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Blender can initialize differente user script paths [\#1528](https://github.com/pypeclub/OpenPype/pull/1528) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Blender and Unreal: Improved Animation Workflow [\#1514](https://github.com/pypeclub/OpenPype/pull/1514) ([simonebarbieri](https://github.com/simonebarbieri)) +- Hiero: publish color and transformation soft-effects [\#1511](https://github.com/pypeclub/OpenPype/pull/1511) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) + +**Fixed bugs:** + +- OpenPype specific version issues [\#1583](https://github.com/pypeclub/OpenPype/issues/1583) +- Ftrack login server can't work without stderr [\#1576](https://github.com/pypeclub/OpenPype/issues/1576) +- Mac application launch [\#1575](https://github.com/pypeclub/OpenPype/issues/1575) +- Settings are not propagated to Nuke write nodes [\#1538](https://github.com/pypeclub/OpenPype/issues/1538) +- Subset names settings not applied for publishing [\#1537](https://github.com/pypeclub/OpenPype/issues/1537) +- Nuke: callback at start not setting colorspace [\#1412](https://github.com/pypeclub/OpenPype/issues/1412) +- Pype 3: Missing icon for Settings [\#1272](https://github.com/pypeclub/OpenPype/issues/1272) +- Blender: cannot initialize Avalon if BLENDER\_USER\_SCRIPTS is already used [\#1050](https://github.com/pypeclub/OpenPype/issues/1050) +- Ftrack delete action cause circular error [\#206](https://github.com/pypeclub/OpenPype/issues/206) +- Build: stop cleaning of pyc files in build directory [\#1592](https://github.com/pypeclub/OpenPype/pull/1592) ([antirotor](https://github.com/antirotor)) +- Ftrack login server can't work without stderr [\#1591](https://github.com/pypeclub/OpenPype/pull/1591) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- FFmpeg filters in quote marks [\#1589](https://github.com/pypeclub/OpenPype/pull/1589) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- OpenPype specific version issues [\#1584](https://github.com/pypeclub/OpenPype/pull/1584) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Mac application launch [\#1580](https://github.com/pypeclub/OpenPype/pull/1580) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Ftrack delete action cause circular error [\#1579](https://github.com/pypeclub/OpenPype/pull/1579) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Hiero: publishing issues [\#1578](https://github.com/pypeclub/OpenPype/pull/1578) ([jezscha](https://github.com/jezscha)) +- Nuke: callback at start not setting colorspace [\#1561](https://github.com/pypeclub/OpenPype/pull/1561) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Bugfix PS subset and quick review [\#1541](https://github.com/pypeclub/OpenPype/pull/1541) ([kalisp](https://github.com/kalisp)) +- Settings are not propagated to Nuke write nodes [\#1540](https://github.com/pypeclub/OpenPype/pull/1540) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- OpenPype: Powershell scripts polishing [\#1536](https://github.com/pypeclub/OpenPype/pull/1536) ([antirotor](https://github.com/antirotor)) +- Host name collecting fix [\#1535](https://github.com/pypeclub/OpenPype/pull/1535) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Handle duplicated task names in project manager [\#1531](https://github.com/pypeclub/OpenPype/pull/1531) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Validate is file attribute in settings schema [\#1529](https://github.com/pypeclub/OpenPype/pull/1529) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) + +**Merged pull requests:** + +- Bump postcss from 8.2.8 to 8.3.0 in /website [\#1593](https://github.com/pypeclub/OpenPype/pull/1593) ([dependabot[bot]](https://github.com/apps/dependabot)) +- User installation documentation [\#1532](https://github.com/pypeclub/OpenPype/pull/1532) ([64qam](https://github.com/64qam)) + +## [CI/3.0.0-rc.5](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.5) (2021-05-19) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.0...CI/3.0.0-rc.5) + +**Implemented enhancements:** + +- OpenPype: Build - Add progress bars [\#1524](https://github.com/pypeclub/OpenPype/pull/1524) ([antirotor](https://github.com/antirotor)) +- Default environments per host imlementation [\#1522](https://github.com/pypeclub/OpenPype/pull/1522) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- OpenPype: use `semver` module for version resolution [\#1513](https://github.com/pypeclub/OpenPype/pull/1513) ([antirotor](https://github.com/antirotor)) +- Feature Aftereffects setting cleanup documentation [\#1510](https://github.com/pypeclub/OpenPype/pull/1510) ([kalisp](https://github.com/kalisp)) +- Feature Sync server settings enhancement [\#1501](https://github.com/pypeclub/OpenPype/pull/1501) ([kalisp](https://github.com/kalisp)) +- Project manager [\#1396](https://github.com/pypeclub/OpenPype/pull/1396) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) + +**Fixed bugs:** + +- Unified schema definition [\#874](https://github.com/pypeclub/OpenPype/issues/874) +- Maya: fix look assignment [\#1526](https://github.com/pypeclub/OpenPype/pull/1526) ([antirotor](https://github.com/antirotor)) +- Bugfix Sync server local site issues [\#1523](https://github.com/pypeclub/OpenPype/pull/1523) ([kalisp](https://github.com/kalisp)) +- Store as list dictionary check initial value with right type [\#1520](https://github.com/pypeclub/OpenPype/pull/1520) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Maya: wrong collection of playblasted frames [\#1515](https://github.com/pypeclub/OpenPype/pull/1515) ([mkolar](https://github.com/mkolar)) +- Convert pyblish logs to string at the moment of logging [\#1512](https://github.com/pypeclub/OpenPype/pull/1512) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- 3.0 | nuke: fixing start\_at with option gui [\#1509](https://github.com/pypeclub/OpenPype/pull/1509) ([jezscha](https://github.com/jezscha)) +- Tests: fix pype -\> openpype to make tests work again [\#1508](https://github.com/pypeclub/OpenPype/pull/1508) ([antirotor](https://github.com/antirotor)) + +**Merged pull requests:** + +- OpenPype: disable submodule update with `--no-submodule-update` [\#1525](https://github.com/pypeclub/OpenPype/pull/1525) ([antirotor](https://github.com/antirotor)) +- Ftrack without autosync in Pype 3 [\#1519](https://github.com/pypeclub/OpenPype/pull/1519) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Feature Harmony setting cleanup documentation [\#1506](https://github.com/pypeclub/OpenPype/pull/1506) ([kalisp](https://github.com/kalisp)) +- Sync Server beginning of documentation [\#1471](https://github.com/pypeclub/OpenPype/pull/1471) ([kalisp](https://github.com/kalisp)) +- Blender: publish layout json [\#1348](https://github.com/pypeclub/OpenPype/pull/1348) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) + +## [2.18.0](https://github.com/pypeclub/openpype/tree/2.18.0) (2021-05-18) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.4...2.18.0) + +**Implemented enhancements:** + +- Default environments per host imlementation [\#1405](https://github.com/pypeclub/OpenPype/issues/1405) +- Blender: publish layout json [\#1346](https://github.com/pypeclub/OpenPype/issues/1346) +- Ftrack without autosync in Pype 3 [\#1128](https://github.com/pypeclub/OpenPype/issues/1128) +- Launcher: started action indicator [\#1102](https://github.com/pypeclub/OpenPype/issues/1102) +- Launch arguments of applications [\#1094](https://github.com/pypeclub/OpenPype/issues/1094) +- Publish: instance info [\#724](https://github.com/pypeclub/OpenPype/issues/724) +- Review: ability to control review length [\#482](https://github.com/pypeclub/OpenPype/issues/482) +- Colorized recognition of creator result [\#394](https://github.com/pypeclub/OpenPype/issues/394) +- event assign user to started task [\#49](https://github.com/pypeclub/OpenPype/issues/49) +- rebuild containers from reference in maya [\#55](https://github.com/pypeclub/OpenPype/issues/55) +- nuke Load metadata [\#66](https://github.com/pypeclub/OpenPype/issues/66) +- Maya: Safer handling of expected render output names [\#1496](https://github.com/pypeclub/OpenPype/pull/1496) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- TVPaint: Increment workfile version on successfull publish. [\#1489](https://github.com/pypeclub/OpenPype/pull/1489) ([tokejepsen](https://github.com/tokejepsen)) +- Use SubsetLoader and multiple contexts for delete\_old\_versions [\#1484](https://github.com/pypeclub/OpenPype/pull/1484) ([tokejepsen](https://github.com/tokejepsen)) +- Maya: Use of multiple deadline servers [\#1483](https://github.com/pypeclub/OpenPype/pull/1483) ([antirotor](https://github.com/antirotor)) + +**Fixed bugs:** + +- Igniter version resolution doesn't consider it's own version [\#1505](https://github.com/pypeclub/OpenPype/issues/1505) +- Maya: Safer handling of expected render output names [\#1159](https://github.com/pypeclub/OpenPype/issues/1159) +- Harmony: Invalid render output from non-conventionally named instance [\#871](https://github.com/pypeclub/OpenPype/issues/871) +- Existing subsets hints in creator [\#1503](https://github.com/pypeclub/OpenPype/pull/1503) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- nuke: space in node name breaking process [\#1494](https://github.com/pypeclub/OpenPype/pull/1494) ([jezscha](https://github.com/jezscha)) +- Maya: wrong collection of playblasted frames [\#1517](https://github.com/pypeclub/OpenPype/pull/1517) ([mkolar](https://github.com/mkolar)) +- Existing subsets hints in creator [\#1502](https://github.com/pypeclub/OpenPype/pull/1502) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Use instance frame start instead of timeline. [\#1486](https://github.com/pypeclub/OpenPype/pull/1486) ([tokejepsen](https://github.com/tokejepsen)) +- Maya: Redshift - set proper start frame on proxy [\#1480](https://github.com/pypeclub/OpenPype/pull/1480) ([antirotor](https://github.com/antirotor)) + +**Closed issues:** + +- Nuke: wrong "star at" value on render load [\#1352](https://github.com/pypeclub/OpenPype/issues/1352) +- DV Resolve - loading/updating - image video [\#915](https://github.com/pypeclub/OpenPype/issues/915) + +**Merged pull requests:** + +- nuke: fixing start\_at with option gui [\#1507](https://github.com/pypeclub/OpenPype/pull/1507) ([jezscha](https://github.com/jezscha)) + +## [CI/3.0.0-rc.4](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.4) (2021-05-12) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...CI/3.0.0-rc.4) + +**Implemented enhancements:** + +- Resolve: documentation [\#1490](https://github.com/pypeclub/OpenPype/issues/1490) +- Hiero: audio to review [\#1378](https://github.com/pypeclub/OpenPype/issues/1378) +- nks color clips after publish [\#44](https://github.com/pypeclub/OpenPype/issues/44) +- Store data from modifiable dict as list [\#1504](https://github.com/pypeclub/OpenPype/pull/1504) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Use SubsetLoader and multiple contexts for delete\_old\_versions [\#1497](https://github.com/pypeclub/OpenPype/pull/1497) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Hiero: publish audio and add to review [\#1493](https://github.com/pypeclub/OpenPype/pull/1493) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Resolve: documentation [\#1491](https://github.com/pypeclub/OpenPype/pull/1491) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Change integratenew template profiles setting [\#1487](https://github.com/pypeclub/OpenPype/pull/1487) ([kalisp](https://github.com/kalisp)) +- Settings tool cleanup [\#1477](https://github.com/pypeclub/OpenPype/pull/1477) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Sorted Applications and Tools in Custom attribute [\#1476](https://github.com/pypeclub/OpenPype/pull/1476) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- PS - group all published instances [\#1416](https://github.com/pypeclub/OpenPype/pull/1416) ([kalisp](https://github.com/kalisp)) +- OpenPype: Support for Docker [\#1289](https://github.com/pypeclub/OpenPype/pull/1289) ([antirotor](https://github.com/antirotor)) + +**Fixed bugs:** + +- Harmony: palettes publishing [\#1439](https://github.com/pypeclub/OpenPype/issues/1439) +- Photoshop: validation for already created images [\#1435](https://github.com/pypeclub/OpenPype/issues/1435) +- Nuke Extracts Thumbnail from frame out of shot range [\#963](https://github.com/pypeclub/OpenPype/issues/963) +- Instance in same Context repairing [\#390](https://github.com/pypeclub/OpenPype/issues/390) +- User Inactivity - Start timers sets wrong time [\#91](https://github.com/pypeclub/OpenPype/issues/91) +- Use instance frame start instead of timeline [\#1499](https://github.com/pypeclub/OpenPype/pull/1499) ([mkolar](https://github.com/mkolar)) +- Various smaller fixes [\#1498](https://github.com/pypeclub/OpenPype/pull/1498) ([mkolar](https://github.com/mkolar)) +- nuke: space in node name breaking process [\#1495](https://github.com/pypeclub/OpenPype/pull/1495) ([jezscha](https://github.com/jezscha)) +- Codec determination in extract burnin [\#1492](https://github.com/pypeclub/OpenPype/pull/1492) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Undefined constant in subprocess module [\#1485](https://github.com/pypeclub/OpenPype/pull/1485) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- List entity catch add/remove item changes properly [\#1482](https://github.com/pypeclub/OpenPype/pull/1482) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Resolve: additional fixes of publishing workflow [\#1481](https://github.com/pypeclub/OpenPype/pull/1481) ([jezscha](https://github.com/jezscha)) +- Photoshop: validation for already created images [\#1436](https://github.com/pypeclub/OpenPype/pull/1436) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) + +**Merged pull requests:** + +- Maya: Support for looks on VRay Proxies [\#1443](https://github.com/pypeclub/OpenPype/pull/1443) ([antirotor](https://github.com/antirotor)) ## [2.17.3](https://github.com/pypeclub/openpype/tree/2.17.3) (2021-05-06) @@ -23,15 +354,102 @@ **Fixed bugs:** -- Nuke: workfile version synced to db version always [\#1479](https://github.com/pypeclub/OpenPype/pull/1479) +- Nuke: workfile version synced to db version always [\#1479](https://github.com/pypeclub/OpenPype/pull/1479) ([jezscha](https://github.com/jezscha)) + +## [CI/3.0.0-rc.3](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.3) (2021-05-05) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.2...CI/3.0.0-rc.3) + +**Implemented enhancements:** + +- Path entity with placeholder [\#1473](https://github.com/pypeclub/OpenPype/pull/1473) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Burnin custom font filepath [\#1472](https://github.com/pypeclub/OpenPype/pull/1472) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Poetry: Move to OpenPype [\#1449](https://github.com/pypeclub/OpenPype/pull/1449) ([antirotor](https://github.com/antirotor)) + +**Fixed bugs:** + +- Mac SSL path needs to be relative to pype\_root [\#1469](https://github.com/pypeclub/OpenPype/issues/1469) +- Resolve: fix loading clips to timeline [\#1421](https://github.com/pypeclub/OpenPype/issues/1421) +- Wrong handling of slashes when loading on mac [\#1411](https://github.com/pypeclub/OpenPype/issues/1411) +- Nuke openpype3 [\#1342](https://github.com/pypeclub/OpenPype/issues/1342) +- Houdini launcher [\#1171](https://github.com/pypeclub/OpenPype/issues/1171) +- Fix SyncServer get\_enabled\_projects should handle global state [\#1475](https://github.com/pypeclub/OpenPype/pull/1475) ([kalisp](https://github.com/kalisp)) +- Igniter buttons enable/disable fix [\#1474](https://github.com/pypeclub/OpenPype/pull/1474) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Mac SSL path needs to be relative to pype\_root [\#1470](https://github.com/pypeclub/OpenPype/pull/1470) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Resolve: 17 compatibility issues and load image sequences [\#1422](https://github.com/pypeclub/OpenPype/pull/1422) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) + +## [CI/3.0.0-rc.2](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.2) (2021-05-04) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.2...CI/3.0.0-rc.2) + +**Implemented enhancements:** + +- Extract burnins with sequences [\#1467](https://github.com/pypeclub/OpenPype/pull/1467) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Extract burnins with color setting [\#1466](https://github.com/pypeclub/OpenPype/pull/1466) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) + +**Fixed bugs:** + +- Fix groups check in Python 2 [\#1468](https://github.com/pypeclub/OpenPype/pull/1468) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) ## [2.17.2](https://github.com/pypeclub/openpype/tree/2.17.2) (2021-05-04) [Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.1...2.17.2) -**Enhancements:** +**Implemented enhancements:** -- Forward/Backward compatible apps and tools with OpenPype 3 [\#1463](https://github.com/pypeclub/OpenPype/pull/1463) +- Forward/Backward compatible apps and tools with OpenPype 3 [\#1463](https://github.com/pypeclub/OpenPype/pull/1463) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) + +## [CI/3.0.0-rc.1](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.1) (2021-05-04) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.1...CI/3.0.0-rc.1) + +**Implemented enhancements:** + +- Only show studio settings to admins [\#1406](https://github.com/pypeclub/OpenPype/issues/1406) +- Ftrack specific settings save warning messages [\#1458](https://github.com/pypeclub/OpenPype/pull/1458) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Faster settings actions [\#1446](https://github.com/pypeclub/OpenPype/pull/1446) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Feature/sync server priority [\#1444](https://github.com/pypeclub/OpenPype/pull/1444) ([kalisp](https://github.com/kalisp)) +- Faster settings UI loading [\#1442](https://github.com/pypeclub/OpenPype/pull/1442) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Igniter re-write [\#1441](https://github.com/pypeclub/OpenPype/pull/1441) ([mkolar](https://github.com/mkolar)) +- Wrap openpype build into installers [\#1419](https://github.com/pypeclub/OpenPype/pull/1419) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Extract review first documentation [\#1404](https://github.com/pypeclub/OpenPype/pull/1404) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Blender PySide2 install guide [\#1403](https://github.com/pypeclub/OpenPype/pull/1403) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Nuke: deadline submission with gpu [\#1394](https://github.com/pypeclub/OpenPype/pull/1394) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Igniter: Reverse item filter for OpenPype version [\#1349](https://github.com/pypeclub/OpenPype/pull/1349) ([antirotor](https://github.com/antirotor)) + +**Fixed bugs:** + +- OpenPype Mongo URL definition [\#1450](https://github.com/pypeclub/OpenPype/issues/1450) +- Various typos and smaller fixes [\#1464](https://github.com/pypeclub/OpenPype/pull/1464) ([mkolar](https://github.com/mkolar)) +- Validation of dynamic items in settings [\#1462](https://github.com/pypeclub/OpenPype/pull/1462) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- List can handle new items correctly [\#1459](https://github.com/pypeclub/OpenPype/pull/1459) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Settings actions process fix [\#1457](https://github.com/pypeclub/OpenPype/pull/1457) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Add to overrides actions fix [\#1456](https://github.com/pypeclub/OpenPype/pull/1456) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- OpenPype Mongo URL definition [\#1455](https://github.com/pypeclub/OpenPype/pull/1455) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Global settings save/load out of system settings [\#1447](https://github.com/pypeclub/OpenPype/pull/1447) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Keep metadata on remove overrides [\#1445](https://github.com/pypeclub/OpenPype/pull/1445) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Nuke: fixing undo for loaded mov and sequence [\#1432](https://github.com/pypeclub/OpenPype/pull/1432) ([jezscha](https://github.com/jezscha)) +- ExtractReview skip empty strings from settings [\#1431](https://github.com/pypeclub/OpenPype/pull/1431) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Bugfix Sync server tweaks [\#1430](https://github.com/pypeclub/OpenPype/pull/1430) ([kalisp](https://github.com/kalisp)) +- Hiero: missing thumbnail in review [\#1429](https://github.com/pypeclub/OpenPype/pull/1429) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Bugfix Maya in deadline for OpenPype [\#1428](https://github.com/pypeclub/OpenPype/pull/1428) ([kalisp](https://github.com/kalisp)) +- AE - validation for duration was 1 frame shorter [\#1427](https://github.com/pypeclub/OpenPype/pull/1427) ([kalisp](https://github.com/kalisp)) +- Houdini menu filename [\#1418](https://github.com/pypeclub/OpenPype/pull/1418) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Fix Avalon plugins attribute overrides [\#1413](https://github.com/pypeclub/OpenPype/pull/1413) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Nuke: submit to Deadline fails [\#1409](https://github.com/pypeclub/OpenPype/pull/1409) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Validate MongoDB Url on start [\#1407](https://github.com/pypeclub/OpenPype/pull/1407) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Nuke: fix set colorspace with new settings [\#1386](https://github.com/pypeclub/OpenPype/pull/1386) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- MacOs build and install issues [\#1380](https://github.com/pypeclub/OpenPype/pull/1380) ([mkolar](https://github.com/mkolar)) + +**Closed issues:** + +- test [\#1452](https://github.com/pypeclub/OpenPype/issues/1452) + +**Merged pull requests:** + +- TVPaint frame range definition [\#1425](https://github.com/pypeclub/OpenPype/pull/1425) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Only show studio settings to admins [\#1420](https://github.com/pypeclub/OpenPype/pull/1420) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- TVPaint documentation [\#1305](https://github.com/pypeclub/OpenPype/pull/1305) ([64qam](https://github.com/64qam)) ## [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) (2021-04-30) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e9473eb4e8..644a74c1f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,7 @@ ## How to contribute to Pype +We are always happy for any contributions for OpenPype improvements. Before making a PR and starting working on an issue, please read these simple guidelines. + #### **Did you find a bug?** 1. Check in the issues and our [bug triage[(https://github.com/pypeclub/pype/projects/2) to make sure it wasn't reported already. @@ -13,11 +15,11 @@ - Open a new GitHub pull request with the patch. - Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. - #### **Do you intend to add a new feature or change an existing one?** - Open a new thread in the [github discussions](https://github.com/pypeclub/pype/discussions/new) -- Do not open issue untill the suggestion is discussed. We will convert accepted suggestions into backlog and point them to the relevant discussion thread to keep the context. +- Do not open issue until the suggestion is discussed. We will convert accepted suggestions into backlog and point them to the relevant discussion thread to keep the context. +- If you are already working on a new feature and you'd like it eventually merged to the main codebase, please consider making a DRAFT PR as soon as possible. This makes it a lot easier to give feedback, discuss the code and functionalit, plus it prevents multiple people tackling the same problem independently. #### **Do you have questions about the source code?** @@ -41,13 +43,11 @@ A few important notes about 2.x and 3.x development: - Please keep the corresponding 2 and 3 PR names the same so they can be easily identified from the PR list page. - Each 2.x PR should be labeled with `2.x-dev` label. -Inside each PR, put a link to the corresponding PR +Inside each PR, put a link to the corresponding PR for the other version Of course if you want to contribute, feel free to make a PR to only 2.x/develop or develop, based on what you are using. While reviewing the PRs, we might convert the code to corresponding PR for the other release ourselves. -We might also change the target of you PR to and intermediate branch, rather than `develop` if we feel it requires some extra work on our end. That way, we preserve all your commits so you don't loos out on the contribution credits. - - +We might also change the target of you PR to and intermediate branch, rather than `develop` if we feel it requires some extra work on our end. That way, we preserve all your commits so you don't loose out on the contribution credits. If a PR is targeted at 2.x release it must be labelled with 2x-dev label in Github. diff --git a/HISTORY.md b/HISTORY.md index 053059a9ea..ae4492bd7a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,514 @@ +# Changelog + + +## [3.0.0](https://github.com/pypeclub/openpype/tree/3.0.0) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.6...3.0.0) + +### Configuration +- Studio Settings GUI: no more json configuration files. +- OpenPype Modules can be turned on and off. +- Easy to add Application versions. +- Per Project Environment and plugin management. +- Robust profile system for creating reviewables and burnins, with filtering based on Application, Task and data family. +- Configurable publish plugins. +- Options to make any validator or extractor, optional or disabled. +- Color Management is now unified under anatomy settings. +- Subset naming and grouping is fully configurable. +- All project attributes can now be set directly in OpenPype settings. +- Studio Setting can be locked to prevent unwanted artist changes. +- You can now add per project and per task type templates for workfile initialization in most hosts. +- Too many other individual configurable option to list in this changelog :) + +### Local Settings +- Local Settings GUI where users can change certain option on individual basis. + - Application executables. + - Project roots. + - Project site sync settings. + +### Build, Installation and Deployments +- No requirements on artist machine. +- Fully distributed workflow possible. +- Self-contained installation. +- Available on all three major platforms. +- Automatic artist OpenPype updates. +- Studio OpenPype repository for updates distribution. +- Robust Build system. +- Safe studio update versioning with staging and production options. +- MacOS build generates .app and .dmg installer. +- Windows build with installer creation script. + +### Misc +- System and diagnostic info tool in the tray. +- Launching application from Launcher indicates activity. +- All project roots are now named. Single root project are now achieved by having a single named root in the project anatomy. +- Every project root is cast into environment variable as well, so it can be used in DCC instead of absolute path (depends on DCC support for env vars). +- Basic support for task types, on top of task names. +- Timer now change automatically when the context is switched inside running application. +- 'Master" versions have been renamed to "Hero". +- Extract Burnins now supports file sequences and color settings. +- Extract Review support overscan cropping, better letterboxes and background colour fill. +- Delivery tool for copying and renaming any published assets in bulk. +- Harmony, Photoshop and After Effects now connect directly with OpenPype tray instead of spawning their own terminal. + +### Project Manager GUI +- Create Projects. +- Create Shots and Assets. +- Create Tasks and assign task types. +- Fill required asset attributes. +- Validations for duplicated or unsupported names. +- Archive Assets. +- Move Asset within hierarchy. + +### Site Sync (beta) +- Synchronization of published files between workstations and central storage. +- Ability to add arbitrary storage providers to the Site Sync system. +- Default setup includes Disk and Google Drive providers as examples. +- Access to availability information from Loader and Scene Manager. +- Sync queue GUI with filtering, error and status reporting. +- Site sync can be configured on a per-project basis. +- Bulk upload and download from the loader. + +### Ftrack +- Actions have customisable roles. +- Settings on all actions are updated live and don't need openpype restart. +- Ftrack module can now be turned off completely. +- It is enough to specify ftrack server name and the URL will be formed correctly. So instead of mystudio.ftrackapp.com, it's possible to use simply: "mystudio". + +### Editorial +- Fully OTIO based editorial publishing. +- Completely re-done Hiero publishing to be a lot simpler and faster. +- Consistent conforming from Resolve, Hiero and Standalone Publisher. + +### Backend +- OpenPype and Avalon now always share the same database (in 2.x is was possible to split them). +- Major codebase refactoring to allow for better CI, versioning and control of individual integrations. +- OTIO is bundled with build. +- OIIO is bundled with build. +- FFMPEG is bundled with build. +- Rest API and host WebSocket servers have been unified into a single local webserver. +- Maya look assigner has been integrated into the main codebase. +- Publish GUI has been integrated into the main codebase. +- Studio and Project settings overrides are now stored in Mongo. +- Too many other backend fixes and tweaks to list :), you can see full changelog on github for those. +- OpenPype uses Poetry to manage it's virtual environment when running from code. +- all applications can be marked as python 2 or 3 compatible to make the switch a bit easier. + + +### Pull Requests since 3.0.0-rc.6 + + +**Implemented enhancements:** + +- settings: task types enum entity [\#1605](https://github.com/pypeclub/OpenPype/issues/1605) +- Settings: ignore keys in referenced schema [\#1600](https://github.com/pypeclub/OpenPype/issues/1600) +- Maya: support for frame steps and frame lists [\#1585](https://github.com/pypeclub/OpenPype/issues/1585) +- TVPaint: Publish workfile. [\#1548](https://github.com/pypeclub/OpenPype/issues/1548) +- Loader: Current Asset Button [\#1448](https://github.com/pypeclub/OpenPype/issues/1448) +- Hiero: publish with retiming [\#1377](https://github.com/pypeclub/OpenPype/issues/1377) +- Ask user to restart after changing global environments in settings [\#910](https://github.com/pypeclub/OpenPype/issues/910) +- add option to define paht to workfile template [\#895](https://github.com/pypeclub/OpenPype/issues/895) +- Harmony: move server console to system tray [\#676](https://github.com/pypeclub/OpenPype/issues/676) +- Standalone style [\#1630](https://github.com/pypeclub/OpenPype/pull/1630) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Faster hierarchical values push [\#1627](https://github.com/pypeclub/OpenPype/pull/1627) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Launcher tool style [\#1624](https://github.com/pypeclub/OpenPype/pull/1624) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Loader and Library loader enhancements [\#1623](https://github.com/pypeclub/OpenPype/pull/1623) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Tray style [\#1622](https://github.com/pypeclub/OpenPype/pull/1622) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Maya schemas cleanup [\#1610](https://github.com/pypeclub/OpenPype/pull/1610) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Settings: ignore keys in referenced schema [\#1608](https://github.com/pypeclub/OpenPype/pull/1608) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- settings: task types enum entity [\#1606](https://github.com/pypeclub/OpenPype/pull/1606) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Openpype style [\#1604](https://github.com/pypeclub/OpenPype/pull/1604) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- TVPaint: Publish workfile. [\#1597](https://github.com/pypeclub/OpenPype/pull/1597) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Nuke: add option to define path to workfile template [\#1571](https://github.com/pypeclub/OpenPype/pull/1571) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Crop overscan in Extract Review [\#1569](https://github.com/pypeclub/OpenPype/pull/1569) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Unreal and Blender: Material Workflow [\#1562](https://github.com/pypeclub/OpenPype/pull/1562) ([simonebarbieri](https://github.com/simonebarbieri)) +- Harmony: move server console to system tray [\#1560](https://github.com/pypeclub/OpenPype/pull/1560) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Ask user to restart after changing global environments in settings [\#1550](https://github.com/pypeclub/OpenPype/pull/1550) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Hiero: publish with retiming [\#1545](https://github.com/pypeclub/OpenPype/pull/1545) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) + +**Fixed bugs:** + +- Library loader load asset documents on OpenPype start [\#1603](https://github.com/pypeclub/OpenPype/issues/1603) +- Resolve: unable to load the same footage twice [\#1317](https://github.com/pypeclub/OpenPype/issues/1317) +- Resolve: unable to load footage [\#1316](https://github.com/pypeclub/OpenPype/issues/1316) +- Add required Python 2 modules [\#1291](https://github.com/pypeclub/OpenPype/issues/1291) +- GUi scaling with hires displays [\#705](https://github.com/pypeclub/OpenPype/issues/705) +- Maya: non unicode string in publish validation [\#673](https://github.com/pypeclub/OpenPype/issues/673) +- Nuke: Rendered Frame validation is triggered by multiple collections [\#156](https://github.com/pypeclub/OpenPype/issues/156) +- avalon-core debugging failing [\#80](https://github.com/pypeclub/OpenPype/issues/80) +- Only check arnold shading group if arnold is used [\#72](https://github.com/pypeclub/OpenPype/issues/72) +- Sync server Qt layout fix [\#1621](https://github.com/pypeclub/OpenPype/pull/1621) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Console Listener on Python 2 fix [\#1620](https://github.com/pypeclub/OpenPype/pull/1620) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Bug: Initialize blessed term only in console mode [\#1619](https://github.com/pypeclub/OpenPype/pull/1619) ([antirotor](https://github.com/antirotor)) +- Settings template skip paths support wrappers [\#1618](https://github.com/pypeclub/OpenPype/pull/1618) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Maya capture 'isolate\_view' fix + minor corrections [\#1617](https://github.com/pypeclub/OpenPype/pull/1617) ([2-REC](https://github.com/2-REC)) +- MacOs Fix launch of standalone publisher [\#1616](https://github.com/pypeclub/OpenPype/pull/1616) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- 'Delivery action' report fix + typos [\#1612](https://github.com/pypeclub/OpenPype/pull/1612) ([2-REC](https://github.com/2-REC)) +- List append fix in mutable dict settings [\#1599](https://github.com/pypeclub/OpenPype/pull/1599) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Documentation: Maya: fix review [\#1598](https://github.com/pypeclub/OpenPype/pull/1598) ([antirotor](https://github.com/antirotor)) +- Bugfix: Set certifi CA bundle for all platforms [\#1596](https://github.com/pypeclub/OpenPype/pull/1596) ([antirotor](https://github.com/antirotor)) + +**Merged pull requests:** + +- Bump dns-packet from 1.3.1 to 1.3.4 in /website [\#1611](https://github.com/pypeclub/OpenPype/pull/1611) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Maya: Render workflow fixes [\#1607](https://github.com/pypeclub/OpenPype/pull/1607) ([antirotor](https://github.com/antirotor)) +- Maya: support for frame steps and frame lists [\#1586](https://github.com/pypeclub/OpenPype/pull/1586) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- 3.0.0 - curated changelog [\#1284](https://github.com/pypeclub/OpenPype/pull/1284) ([mkolar](https://github.com/mkolar)) + +## [2.18.1](https://github.com/pypeclub/openpype/tree/2.18.1) (2021-06-03) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.0...2.18.1) + +**Enhancements:** + +- Faster hierarchical values push [\#1626](https://github.com/pypeclub/OpenPype/pull/1626) +- Feature Delivery in library loader [\#1549](https://github.com/pypeclub/OpenPype/pull/1549) +- Hiero: Initial frame publish support. [\#1172](https://github.com/pypeclub/OpenPype/pull/1172) + +**Fixed bugs:** + +- Maya capture 'isolate\_view' fix + minor corrections [\#1614](https://github.com/pypeclub/OpenPype/pull/1614) +- 'Delivery action' report fix +typos [\#1613](https://github.com/pypeclub/OpenPype/pull/1613) +- Delivery in LibraryLoader - fixed sequence issue [\#1590](https://github.com/pypeclub/OpenPype/pull/1590) +- FFmpeg filters in quote marks [\#1588](https://github.com/pypeclub/OpenPype/pull/1588) +- Ftrack delete action cause circular error [\#1581](https://github.com/pypeclub/OpenPype/pull/1581) +- Fix Maya playblast. [\#1566](https://github.com/pypeclub/OpenPype/pull/1566) +- More failsafes prevent errored runs. [\#1554](https://github.com/pypeclub/OpenPype/pull/1554) +- Celaction publishing [\#1539](https://github.com/pypeclub/OpenPype/pull/1539) +- celaction: app not starting [\#1533](https://github.com/pypeclub/OpenPype/pull/1533) + +**Merged pull requests:** + +- Maya: Render workflow fixes - 2.0 backport [\#1609](https://github.com/pypeclub/OpenPype/pull/1609) +- Maya Hardware support [\#1553](https://github.com/pypeclub/OpenPype/pull/1553) + + +## [CI/3.0.0-rc.6](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.6) (2021-05-27) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.5...CI/3.0.0-rc.6) + +**Implemented enhancements:** + +- Hiero: publish color and transformation soft-effects [\#1376](https://github.com/pypeclub/OpenPype/issues/1376) +- Get rid of `AVALON\_HIERARCHY` and `hiearchy` key on asset [\#432](https://github.com/pypeclub/OpenPype/issues/432) +- Sync to avalon do not store hierarchy key [\#1582](https://github.com/pypeclub/OpenPype/pull/1582) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Tools: launcher scripts for project manager [\#1557](https://github.com/pypeclub/OpenPype/pull/1557) ([antirotor](https://github.com/antirotor)) +- Simple tvpaint publish [\#1555](https://github.com/pypeclub/OpenPype/pull/1555) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Feature Delivery in library loader [\#1546](https://github.com/pypeclub/OpenPype/pull/1546) ([kalisp](https://github.com/kalisp)) +- Documentation: Dev and system build documentation [\#1543](https://github.com/pypeclub/OpenPype/pull/1543) ([antirotor](https://github.com/antirotor)) +- Color entity [\#1542](https://github.com/pypeclub/OpenPype/pull/1542) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Extract review bg color [\#1534](https://github.com/pypeclub/OpenPype/pull/1534) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- TVPaint loader settings [\#1530](https://github.com/pypeclub/OpenPype/pull/1530) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Blender can initialize differente user script paths [\#1528](https://github.com/pypeclub/OpenPype/pull/1528) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Blender and Unreal: Improved Animation Workflow [\#1514](https://github.com/pypeclub/OpenPype/pull/1514) ([simonebarbieri](https://github.com/simonebarbieri)) +- Hiero: publish color and transformation soft-effects [\#1511](https://github.com/pypeclub/OpenPype/pull/1511) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) + +**Fixed bugs:** + +- OpenPype specific version issues [\#1583](https://github.com/pypeclub/OpenPype/issues/1583) +- Ftrack login server can't work without stderr [\#1576](https://github.com/pypeclub/OpenPype/issues/1576) +- Mac application launch [\#1575](https://github.com/pypeclub/OpenPype/issues/1575) +- Settings are not propagated to Nuke write nodes [\#1538](https://github.com/pypeclub/OpenPype/issues/1538) +- Subset names settings not applied for publishing [\#1537](https://github.com/pypeclub/OpenPype/issues/1537) +- Nuke: callback at start not setting colorspace [\#1412](https://github.com/pypeclub/OpenPype/issues/1412) +- Pype 3: Missing icon for Settings [\#1272](https://github.com/pypeclub/OpenPype/issues/1272) +- Blender: cannot initialize Avalon if BLENDER\_USER\_SCRIPTS is already used [\#1050](https://github.com/pypeclub/OpenPype/issues/1050) +- Ftrack delete action cause circular error [\#206](https://github.com/pypeclub/OpenPype/issues/206) +- Build: stop cleaning of pyc files in build directory [\#1592](https://github.com/pypeclub/OpenPype/pull/1592) ([antirotor](https://github.com/antirotor)) +- Ftrack login server can't work without stderr [\#1591](https://github.com/pypeclub/OpenPype/pull/1591) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- FFmpeg filters in quote marks [\#1589](https://github.com/pypeclub/OpenPype/pull/1589) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- OpenPype specific version issues [\#1584](https://github.com/pypeclub/OpenPype/pull/1584) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Mac application launch [\#1580](https://github.com/pypeclub/OpenPype/pull/1580) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Ftrack delete action cause circular error [\#1579](https://github.com/pypeclub/OpenPype/pull/1579) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Hiero: publishing issues [\#1578](https://github.com/pypeclub/OpenPype/pull/1578) ([jezscha](https://github.com/jezscha)) +- Nuke: callback at start not setting colorspace [\#1561](https://github.com/pypeclub/OpenPype/pull/1561) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Bugfix PS subset and quick review [\#1541](https://github.com/pypeclub/OpenPype/pull/1541) ([kalisp](https://github.com/kalisp)) +- Settings are not propagated to Nuke write nodes [\#1540](https://github.com/pypeclub/OpenPype/pull/1540) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- OpenPype: Powershell scripts polishing [\#1536](https://github.com/pypeclub/OpenPype/pull/1536) ([antirotor](https://github.com/antirotor)) +- Host name collecting fix [\#1535](https://github.com/pypeclub/OpenPype/pull/1535) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Handle duplicated task names in project manager [\#1531](https://github.com/pypeclub/OpenPype/pull/1531) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Validate is file attribute in settings schema [\#1529](https://github.com/pypeclub/OpenPype/pull/1529) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) + +**Merged pull requests:** + +- Bump postcss from 8.2.8 to 8.3.0 in /website [\#1593](https://github.com/pypeclub/OpenPype/pull/1593) ([dependabot[bot]](https://github.com/apps/dependabot)) +- User installation documentation [\#1532](https://github.com/pypeclub/OpenPype/pull/1532) ([64qam](https://github.com/64qam)) + +## [CI/3.0.0-rc.5](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.5) (2021-05-19) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.0...CI/3.0.0-rc.5) + +**Implemented enhancements:** + +- OpenPype: Build - Add progress bars [\#1524](https://github.com/pypeclub/OpenPype/pull/1524) ([antirotor](https://github.com/antirotor)) +- Default environments per host imlementation [\#1522](https://github.com/pypeclub/OpenPype/pull/1522) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- OpenPype: use `semver` module for version resolution [\#1513](https://github.com/pypeclub/OpenPype/pull/1513) ([antirotor](https://github.com/antirotor)) +- Feature Aftereffects setting cleanup documentation [\#1510](https://github.com/pypeclub/OpenPype/pull/1510) ([kalisp](https://github.com/kalisp)) +- Feature Sync server settings enhancement [\#1501](https://github.com/pypeclub/OpenPype/pull/1501) ([kalisp](https://github.com/kalisp)) +- Project manager [\#1396](https://github.com/pypeclub/OpenPype/pull/1396) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) + +**Fixed bugs:** + +- Unified schema definition [\#874](https://github.com/pypeclub/OpenPype/issues/874) +- Maya: fix look assignment [\#1526](https://github.com/pypeclub/OpenPype/pull/1526) ([antirotor](https://github.com/antirotor)) +- Bugfix Sync server local site issues [\#1523](https://github.com/pypeclub/OpenPype/pull/1523) ([kalisp](https://github.com/kalisp)) +- Store as list dictionary check initial value with right type [\#1520](https://github.com/pypeclub/OpenPype/pull/1520) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Maya: wrong collection of playblasted frames [\#1515](https://github.com/pypeclub/OpenPype/pull/1515) ([mkolar](https://github.com/mkolar)) +- Convert pyblish logs to string at the moment of logging [\#1512](https://github.com/pypeclub/OpenPype/pull/1512) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- 3.0 | nuke: fixing start\_at with option gui [\#1509](https://github.com/pypeclub/OpenPype/pull/1509) ([jezscha](https://github.com/jezscha)) +- Tests: fix pype -\> openpype to make tests work again [\#1508](https://github.com/pypeclub/OpenPype/pull/1508) ([antirotor](https://github.com/antirotor)) + +**Merged pull requests:** + +- OpenPype: disable submodule update with `--no-submodule-update` [\#1525](https://github.com/pypeclub/OpenPype/pull/1525) ([antirotor](https://github.com/antirotor)) +- Ftrack without autosync in Pype 3 [\#1519](https://github.com/pypeclub/OpenPype/pull/1519) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Feature Harmony setting cleanup documentation [\#1506](https://github.com/pypeclub/OpenPype/pull/1506) ([kalisp](https://github.com/kalisp)) +- Sync Server beginning of documentation [\#1471](https://github.com/pypeclub/OpenPype/pull/1471) ([kalisp](https://github.com/kalisp)) +- Blender: publish layout json [\#1348](https://github.com/pypeclub/OpenPype/pull/1348) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) + +## [2.18.0](https://github.com/pypeclub/openpype/tree/2.18.0) (2021-05-18) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.4...2.18.0) + +**Implemented enhancements:** + +- Default environments per host imlementation [\#1405](https://github.com/pypeclub/OpenPype/issues/1405) +- Blender: publish layout json [\#1346](https://github.com/pypeclub/OpenPype/issues/1346) +- Ftrack without autosync in Pype 3 [\#1128](https://github.com/pypeclub/OpenPype/issues/1128) +- Launcher: started action indicator [\#1102](https://github.com/pypeclub/OpenPype/issues/1102) +- Launch arguments of applications [\#1094](https://github.com/pypeclub/OpenPype/issues/1094) +- Publish: instance info [\#724](https://github.com/pypeclub/OpenPype/issues/724) +- Review: ability to control review length [\#482](https://github.com/pypeclub/OpenPype/issues/482) +- Colorized recognition of creator result [\#394](https://github.com/pypeclub/OpenPype/issues/394) +- event assign user to started task [\#49](https://github.com/pypeclub/OpenPype/issues/49) +- rebuild containers from reference in maya [\#55](https://github.com/pypeclub/OpenPype/issues/55) +- nuke Load metadata [\#66](https://github.com/pypeclub/OpenPype/issues/66) +- Maya: Safer handling of expected render output names [\#1496](https://github.com/pypeclub/OpenPype/pull/1496) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- TVPaint: Increment workfile version on successfull publish. [\#1489](https://github.com/pypeclub/OpenPype/pull/1489) ([tokejepsen](https://github.com/tokejepsen)) +- Use SubsetLoader and multiple contexts for delete\_old\_versions [\#1484](https://github.com/pypeclub/OpenPype/pull/1484) ([tokejepsen](https://github.com/tokejepsen)) +- Maya: Use of multiple deadline servers [\#1483](https://github.com/pypeclub/OpenPype/pull/1483) ([antirotor](https://github.com/antirotor)) + +**Fixed bugs:** + +- Igniter version resolution doesn't consider it's own version [\#1505](https://github.com/pypeclub/OpenPype/issues/1505) +- Maya: Safer handling of expected render output names [\#1159](https://github.com/pypeclub/OpenPype/issues/1159) +- Harmony: Invalid render output from non-conventionally named instance [\#871](https://github.com/pypeclub/OpenPype/issues/871) +- Existing subsets hints in creator [\#1503](https://github.com/pypeclub/OpenPype/pull/1503) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- nuke: space in node name breaking process [\#1494](https://github.com/pypeclub/OpenPype/pull/1494) ([jezscha](https://github.com/jezscha)) +- Maya: wrong collection of playblasted frames [\#1517](https://github.com/pypeclub/OpenPype/pull/1517) ([mkolar](https://github.com/mkolar)) +- Existing subsets hints in creator [\#1502](https://github.com/pypeclub/OpenPype/pull/1502) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Use instance frame start instead of timeline. [\#1486](https://github.com/pypeclub/OpenPype/pull/1486) ([tokejepsen](https://github.com/tokejepsen)) +- Maya: Redshift - set proper start frame on proxy [\#1480](https://github.com/pypeclub/OpenPype/pull/1480) ([antirotor](https://github.com/antirotor)) + +**Closed issues:** + +- Nuke: wrong "star at" value on render load [\#1352](https://github.com/pypeclub/OpenPype/issues/1352) +- DV Resolve - loading/updating - image video [\#915](https://github.com/pypeclub/OpenPype/issues/915) + +**Merged pull requests:** + +- nuke: fixing start\_at with option gui [\#1507](https://github.com/pypeclub/OpenPype/pull/1507) ([jezscha](https://github.com/jezscha)) + +## [CI/3.0.0-rc.4](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.4) (2021-05-12) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...CI/3.0.0-rc.4) + +**Implemented enhancements:** + +- Resolve: documentation [\#1490](https://github.com/pypeclub/OpenPype/issues/1490) +- Hiero: audio to review [\#1378](https://github.com/pypeclub/OpenPype/issues/1378) +- nks color clips after publish [\#44](https://github.com/pypeclub/OpenPype/issues/44) +- Store data from modifiable dict as list [\#1504](https://github.com/pypeclub/OpenPype/pull/1504) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Use SubsetLoader and multiple contexts for delete\_old\_versions [\#1497](https://github.com/pypeclub/OpenPype/pull/1497) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Hiero: publish audio and add to review [\#1493](https://github.com/pypeclub/OpenPype/pull/1493) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Resolve: documentation [\#1491](https://github.com/pypeclub/OpenPype/pull/1491) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Change integratenew template profiles setting [\#1487](https://github.com/pypeclub/OpenPype/pull/1487) ([kalisp](https://github.com/kalisp)) +- Settings tool cleanup [\#1477](https://github.com/pypeclub/OpenPype/pull/1477) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Sorted Applications and Tools in Custom attribute [\#1476](https://github.com/pypeclub/OpenPype/pull/1476) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- PS - group all published instances [\#1416](https://github.com/pypeclub/OpenPype/pull/1416) ([kalisp](https://github.com/kalisp)) +- OpenPype: Support for Docker [\#1289](https://github.com/pypeclub/OpenPype/pull/1289) ([antirotor](https://github.com/antirotor)) + +**Fixed bugs:** + +- Harmony: palettes publishing [\#1439](https://github.com/pypeclub/OpenPype/issues/1439) +- Photoshop: validation for already created images [\#1435](https://github.com/pypeclub/OpenPype/issues/1435) +- Nuke Extracts Thumbnail from frame out of shot range [\#963](https://github.com/pypeclub/OpenPype/issues/963) +- Instance in same Context repairing [\#390](https://github.com/pypeclub/OpenPype/issues/390) +- User Inactivity - Start timers sets wrong time [\#91](https://github.com/pypeclub/OpenPype/issues/91) +- Use instance frame start instead of timeline [\#1499](https://github.com/pypeclub/OpenPype/pull/1499) ([mkolar](https://github.com/mkolar)) +- Various smaller fixes [\#1498](https://github.com/pypeclub/OpenPype/pull/1498) ([mkolar](https://github.com/mkolar)) +- nuke: space in node name breaking process [\#1495](https://github.com/pypeclub/OpenPype/pull/1495) ([jezscha](https://github.com/jezscha)) +- Codec determination in extract burnin [\#1492](https://github.com/pypeclub/OpenPype/pull/1492) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Undefined constant in subprocess module [\#1485](https://github.com/pypeclub/OpenPype/pull/1485) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- List entity catch add/remove item changes properly [\#1482](https://github.com/pypeclub/OpenPype/pull/1482) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Resolve: additional fixes of publishing workflow [\#1481](https://github.com/pypeclub/OpenPype/pull/1481) ([jezscha](https://github.com/jezscha)) +- Photoshop: validation for already created images [\#1436](https://github.com/pypeclub/OpenPype/pull/1436) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) + +**Merged pull requests:** + +- Maya: Support for looks on VRay Proxies [\#1443](https://github.com/pypeclub/OpenPype/pull/1443) ([antirotor](https://github.com/antirotor)) + +## [2.17.3](https://github.com/pypeclub/openpype/tree/2.17.3) (2021-05-06) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.3...2.17.3) + +**Fixed bugs:** + +- Nuke: workfile version synced to db version always [\#1479](https://github.com/pypeclub/OpenPype/pull/1479) ([jezscha](https://github.com/jezscha)) + +## [CI/3.0.0-rc.3](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.3) (2021-05-05) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.2...CI/3.0.0-rc.3) + +**Implemented enhancements:** + +- Path entity with placeholder [\#1473](https://github.com/pypeclub/OpenPype/pull/1473) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Burnin custom font filepath [\#1472](https://github.com/pypeclub/OpenPype/pull/1472) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Poetry: Move to OpenPype [\#1449](https://github.com/pypeclub/OpenPype/pull/1449) ([antirotor](https://github.com/antirotor)) + +**Fixed bugs:** + +- Mac SSL path needs to be relative to pype\_root [\#1469](https://github.com/pypeclub/OpenPype/issues/1469) +- Resolve: fix loading clips to timeline [\#1421](https://github.com/pypeclub/OpenPype/issues/1421) +- Wrong handling of slashes when loading on mac [\#1411](https://github.com/pypeclub/OpenPype/issues/1411) +- Nuke openpype3 [\#1342](https://github.com/pypeclub/OpenPype/issues/1342) +- Houdini launcher [\#1171](https://github.com/pypeclub/OpenPype/issues/1171) +- Fix SyncServer get\_enabled\_projects should handle global state [\#1475](https://github.com/pypeclub/OpenPype/pull/1475) ([kalisp](https://github.com/kalisp)) +- Igniter buttons enable/disable fix [\#1474](https://github.com/pypeclub/OpenPype/pull/1474) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Mac SSL path needs to be relative to pype\_root [\#1470](https://github.com/pypeclub/OpenPype/pull/1470) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Resolve: 17 compatibility issues and load image sequences [\#1422](https://github.com/pypeclub/OpenPype/pull/1422) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) + +## [CI/3.0.0-rc.2](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.2) (2021-05-04) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.2...CI/3.0.0-rc.2) + +**Implemented enhancements:** + +- Extract burnins with sequences [\#1467](https://github.com/pypeclub/OpenPype/pull/1467) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Extract burnins with color setting [\#1466](https://github.com/pypeclub/OpenPype/pull/1466) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) + +**Fixed bugs:** + +- Fix groups check in Python 2 [\#1468](https://github.com/pypeclub/OpenPype/pull/1468) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) + +## [2.17.2](https://github.com/pypeclub/openpype/tree/2.17.2) (2021-05-04) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.1...2.17.2) + +**Implemented enhancements:** + +- Forward/Backward compatible apps and tools with OpenPype 3 [\#1463](https://github.com/pypeclub/OpenPype/pull/1463) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) + +## [CI/3.0.0-rc.1](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.1) (2021-05-04) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.1...CI/3.0.0-rc.1) + +**Implemented enhancements:** + +- Only show studio settings to admins [\#1406](https://github.com/pypeclub/OpenPype/issues/1406) +- Ftrack specific settings save warning messages [\#1458](https://github.com/pypeclub/OpenPype/pull/1458) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Faster settings actions [\#1446](https://github.com/pypeclub/OpenPype/pull/1446) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Feature/sync server priority [\#1444](https://github.com/pypeclub/OpenPype/pull/1444) ([kalisp](https://github.com/kalisp)) +- Faster settings UI loading [\#1442](https://github.com/pypeclub/OpenPype/pull/1442) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Igniter re-write [\#1441](https://github.com/pypeclub/OpenPype/pull/1441) ([mkolar](https://github.com/mkolar)) +- Wrap openpype build into installers [\#1419](https://github.com/pypeclub/OpenPype/pull/1419) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Extract review first documentation [\#1404](https://github.com/pypeclub/OpenPype/pull/1404) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Blender PySide2 install guide [\#1403](https://github.com/pypeclub/OpenPype/pull/1403) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Nuke: deadline submission with gpu [\#1394](https://github.com/pypeclub/OpenPype/pull/1394) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Igniter: Reverse item filter for OpenPype version [\#1349](https://github.com/pypeclub/OpenPype/pull/1349) ([antirotor](https://github.com/antirotor)) + +**Fixed bugs:** + +- OpenPype Mongo URL definition [\#1450](https://github.com/pypeclub/OpenPype/issues/1450) +- Various typos and smaller fixes [\#1464](https://github.com/pypeclub/OpenPype/pull/1464) ([mkolar](https://github.com/mkolar)) +- Validation of dynamic items in settings [\#1462](https://github.com/pypeclub/OpenPype/pull/1462) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- List can handle new items correctly [\#1459](https://github.com/pypeclub/OpenPype/pull/1459) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Settings actions process fix [\#1457](https://github.com/pypeclub/OpenPype/pull/1457) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Add to overrides actions fix [\#1456](https://github.com/pypeclub/OpenPype/pull/1456) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- OpenPype Mongo URL definition [\#1455](https://github.com/pypeclub/OpenPype/pull/1455) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Global settings save/load out of system settings [\#1447](https://github.com/pypeclub/OpenPype/pull/1447) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Keep metadata on remove overrides [\#1445](https://github.com/pypeclub/OpenPype/pull/1445) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Nuke: fixing undo for loaded mov and sequence [\#1432](https://github.com/pypeclub/OpenPype/pull/1432) ([jezscha](https://github.com/jezscha)) +- ExtractReview skip empty strings from settings [\#1431](https://github.com/pypeclub/OpenPype/pull/1431) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Bugfix Sync server tweaks [\#1430](https://github.com/pypeclub/OpenPype/pull/1430) ([kalisp](https://github.com/kalisp)) +- Hiero: missing thumbnail in review [\#1429](https://github.com/pypeclub/OpenPype/pull/1429) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Bugfix Maya in deadline for OpenPype [\#1428](https://github.com/pypeclub/OpenPype/pull/1428) ([kalisp](https://github.com/kalisp)) +- AE - validation for duration was 1 frame shorter [\#1427](https://github.com/pypeclub/OpenPype/pull/1427) ([kalisp](https://github.com/kalisp)) +- Houdini menu filename [\#1418](https://github.com/pypeclub/OpenPype/pull/1418) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Fix Avalon plugins attribute overrides [\#1413](https://github.com/pypeclub/OpenPype/pull/1413) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Nuke: submit to Deadline fails [\#1409](https://github.com/pypeclub/OpenPype/pull/1409) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- Validate MongoDB Url on start [\#1407](https://github.com/pypeclub/OpenPype/pull/1407) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Nuke: fix set colorspace with new settings [\#1386](https://github.com/pypeclub/OpenPype/pull/1386) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- MacOs build and install issues [\#1380](https://github.com/pypeclub/OpenPype/pull/1380) ([mkolar](https://github.com/mkolar)) + +**Closed issues:** + +- test [\#1452](https://github.com/pypeclub/OpenPype/issues/1452) + +**Merged pull requests:** + +- TVPaint frame range definition [\#1425](https://github.com/pypeclub/OpenPype/pull/1425) ([iLLiCiTiT](https://github.com/iLLiCiTiT)) +- Only show studio settings to admins [\#1420](https://github.com/pypeclub/OpenPype/pull/1420) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch)) +- TVPaint documentation [\#1305](https://github.com/pypeclub/OpenPype/pull/1305) ([64qam](https://github.com/64qam)) + +## [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) (2021-04-30) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.0...2.17.1) + +**Enhancements:** + +- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414) +- TVPaint frame range definition [\#1424](https://github.com/pypeclub/OpenPype/pull/1424) +- PS - group all published instances [\#1415](https://github.com/pypeclub/OpenPype/pull/1415) +- Add task name to context pop up. [\#1383](https://github.com/pypeclub/OpenPype/pull/1383) +- Enhance review letterbox feature. [\#1371](https://github.com/pypeclub/OpenPype/pull/1371) + +**Fixed bugs:** + +- Houdini menu filename [\#1417](https://github.com/pypeclub/OpenPype/pull/1417) +- AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426) + +**Merged pull requests:** + +- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399) +- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360) + +## [2.17.0](https://github.com/pypeclub/openpype/tree/2.17.0) (2021-04-20) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-beta.2...2.17.0) + +**Enhancements:** + +- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/OpenPype/pull/1243) +- Settings in mongo as dict [\#1221](https://github.com/pypeclub/OpenPype/pull/1221) +- Maya: Make tx option configurable with presets [\#1328](https://github.com/pypeclub/OpenPype/pull/1328) +- TVPaint asset name validation [\#1302](https://github.com/pypeclub/OpenPype/pull/1302) +- TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/OpenPype/pull/1299) +- TV Paint: Validate mark in and out. [\#1298](https://github.com/pypeclub/OpenPype/pull/1298) +- Validate project settings [\#1297](https://github.com/pypeclub/OpenPype/pull/1297) +- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/OpenPype/pull/1234) +- Show error message in pyblish UI [\#1206](https://github.com/pypeclub/OpenPype/pull/1206) + +**Fixed bugs:** + +- Hiero: fixing source frame from correct object [\#1362](https://github.com/pypeclub/OpenPype/pull/1362) +- Nuke: fix colourspace, prerenders and nuke panes opening [\#1308](https://github.com/pypeclub/OpenPype/pull/1308) +- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/OpenPype/pull/1282) +- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/OpenPype/pull/1194) +- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/OpenPype/pull/1312) +- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/OpenPype/pull/1303) +- After Effects: remove orphaned instances [\#1275](https://github.com/pypeclub/OpenPype/pull/1275) +- Avalon schema names [\#1242](https://github.com/pypeclub/OpenPype/pull/1242) +- Handle duplication of Task name [\#1226](https://github.com/pypeclub/OpenPype/pull/1226) +- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/OpenPype/pull/1217) +- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/OpenPype/pull/1214) +- Bulk mov strict task [\#1204](https://github.com/pypeclub/OpenPype/pull/1204) +- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/OpenPype/pull/1202) +- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/OpenPype/pull/1199) +- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/OpenPype/pull/1178) + + + ## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22) [Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.16.0) @@ -1057,4 +1568,7 @@ A large cleanup release. Most of the change are under the hood. \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* + + \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/igniter/version.py b/igniter/version.py index 4f8f0907e9..3c627aaa1a 100644 --- a/igniter/version.py +++ b/igniter/version.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- """Definition of Igniter version.""" -__version__ = "1.0.0-rc1" +__version__ = "1.0.0" diff --git a/openpype/cli.py b/openpype/cli.py index df38c74a21..9f4561b82e 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -26,7 +26,7 @@ def main(ctx): @main.command() @click.option("-d", "--dev", is_flag=True, help="Settings in Dev mode") -def settings(dev=False): +def settings(dev): """Show Pype Settings UI.""" PypeCommands().launch_settings_gui(dev) @@ -60,13 +60,6 @@ def tray(debug=False): help="Ftrack api user") @click.option("--ftrack-api-key", envvar="FTRACK_API_KEY", help="Ftrack api key") -@click.option("--ftrack-events-path", - envvar="FTRACK_EVENTS_PATH", - help=("path to ftrack event handlers")) -@click.option("--no-stored-credentials", is_flag=True, - help="don't use stored credentials") -@click.option("--store-credentials", is_flag=True, - help="store provided credentials") @click.option("--legacy", is_flag=True, help="run event server without mongo storing") @click.option("--clockify-api-key", envvar="CLOCKIFY_API_KEY", @@ -77,9 +70,6 @@ def eventserver(debug, ftrack_url, ftrack_user, ftrack_api_key, - ftrack_events_path, - no_stored_credentials, - store_credentials, legacy, clockify_api_key, clockify_workspace): @@ -87,10 +77,6 @@ def eventserver(debug, This should be ideally used by system service (such us systemd or upstart on linux and window service). - - You have to set either proper environment variables to provide URL and - credentials or use option to specify them. If you use --store_credentials - provided credentials will be stored for later use. """ if debug: os.environ['OPENPYPE_DEBUG'] = "3" @@ -99,9 +85,6 @@ def eventserver(debug, ftrack_url, ftrack_user, ftrack_api_key, - ftrack_events_path, - no_stored_credentials, - store_credentials, legacy, clockify_api_key, clockify_workspace diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index 377026da4a..5ca2a42510 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -8,8 +8,19 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): This is not possible to do for all applications the same way. """ - order = 0 - app_groups = ["maya", "nuke", "nukex", "hiero", "nukestudio"] + # Execute after workfile template copy + order = 10 + app_groups = [ + "maya", + "nuke", + "nukex", + "hiero", + "nukestudio", + "blender", + "photoshop", + "tvpaint", + "afftereffects" + ] def execute(self): if not self.data.get("start_last_workfile"): diff --git a/openpype/hooks/pre_copy_template_workfile.py b/openpype/hooks/pre_copy_template_workfile.py new file mode 100644 index 0000000000..29a522f933 --- /dev/null +++ b/openpype/hooks/pre_copy_template_workfile.py @@ -0,0 +1,127 @@ +import os +import shutil +from openpype.lib import ( + PreLaunchHook, + get_custom_workfile_template_by_context, + get_custom_workfile_template_by_string_context +) +from openpype.settings import get_project_settings + + +class CopyTemplateWorkfile(PreLaunchHook): + """Copy workfile template. + + This is not possible to do for all applications the same way. + + Prelaunch hook works only if last workfile leads to not existing file. + - That is possible only if it's first version. + """ + + # Before `AddLastWorkfileToLaunchArgs` + order = 0 + app_groups = ["blender", "photoshop", "tvpaint", "afftereffects"] + + def execute(self): + """Check if can copy template for context and do it if possible. + + First check if host for current project should create first workfile. + Second check is if template is reachable and can be copied. + + Args: + last_workfile(str): Path where template will be copied. + + Returns: + None: This is a void method. + """ + + last_workfile = self.data.get("last_workfile_path") + if not last_workfile: + self.log.warning(( + "Last workfile was not collected." + " Can't add it to launch arguments or determine if should" + " copy template." + )) + return + + if os.path.exists(last_workfile): + self.log.debug("Last workfile exits. Skipping {} process.".format( + self.__class__.__name__ + )) + return + + self.log.info("Last workfile does not exits.") + + project_name = self.data["project_name"] + asset_name = self.data["asset_name"] + task_name = self.data["task_name"] + + project_settings = get_project_settings(project_name) + host_settings = project_settings[self.application.host_name] + + workfile_builder_settings = host_settings.get("workfile_builder") + if not workfile_builder_settings: + # TODO remove warning when deprecated + self.log.warning(( + "Seems like old version of settings is used." + " Can't access custom templates in host \"{}\"." + ).format(self.application.full_label)) + return + + if not workfile_builder_settings["create_first_version"]: + self.log.info(( + "Project \"{}\" has turned off to create first workfile for" + " application \"{}\"" + ).format(project_name, self.application.full_label)) + return + + # Backwards compatibility + template_profiles = workfile_builder_settings.get("custom_templates") + if not template_profiles: + self.log.info( + "Custom templates are not filled. Skipping template copy." + ) + return + + project_doc = self.data.get("project_doc") + asset_doc = self.data.get("asset_doc") + anatomy = self.data.get("anatomy") + if project_doc and asset_doc: + self.log.debug("Started filtering of custom template paths.") + template_path = get_custom_workfile_template_by_context( + template_profiles, project_doc, asset_doc, task_name, anatomy + ) + + else: + self.log.warning(( + "Global data collection probably did not execute." + " Using backup solution." + )) + dbcon = self.data.get("dbcon") + template_path = get_custom_workfile_template_by_string_context( + template_profiles, project_name, asset_name, task_name, + dbcon, anatomy + ) + + if not template_path: + self.log.info( + "Registered custom templates didn't match current context." + ) + return + + if not os.path.exists(template_path): + self.log.warning( + "Couldn't find workfile template file \"{}\"".format( + template_path + ) + ) + return + + self.log.info( + f"Creating workfile from template: \"{template_path}\"" + ) + + # Copy template workfile to new destinantion + shutil.copy2( + os.path.normpath(template_path), + os.path.normpath(last_workfile) + ) diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py index c16a72c5e5..393a878f76 100644 --- a/openpype/hooks/pre_non_python_host_launch.py +++ b/openpype/hooks/pre_non_python_host_launch.py @@ -1,4 +1,5 @@ import os +import subprocess from openpype.lib import ( PreLaunchHook, @@ -17,6 +18,8 @@ class NonPythonHostHook(PreLaunchHook): """ app_groups = ["harmony", "photoshop", "aftereffects"] + order = 20 + def execute(self): # Pop executable executable_path = self.launch_context.launch_args.pop(0) @@ -45,3 +48,6 @@ class NonPythonHostHook(PreLaunchHook): if remainders: self.launch_context.launch_args.extend(remainders) + + self.launch_context.kwargs["stdout"] = subprocess.DEVNULL + self.launch_context.kwargs["stderr"] = subprocess.STDOUT diff --git a/openpype/hooks/pre_with_windows_shell.py b/openpype/hooks/pre_with_windows_shell.py index 0c10583b99..441ab1a675 100644 --- a/openpype/hooks/pre_with_windows_shell.py +++ b/openpype/hooks/pre_with_windows_shell.py @@ -11,12 +11,14 @@ class LaunchWithWindowsShell(PreLaunchHook): instead. """ - # Should be as last hook becuase must change launch arguments to string + # Should be as last hook because must change launch arguments to string order = 1000 app_groups = ["nuke", "nukex", "hiero", "nukestudio"] platforms = ["windows"] def execute(self): + launch_args = self.launch_context.clear_launch_args( + self.launch_context.launch_args) new_args = [ # Get comspec which is cmd.exe in most cases. os.environ.get("COMSPEC", "cmd.exe"), @@ -24,7 +26,7 @@ class LaunchWithWindowsShell(PreLaunchHook): "/c", # Convert arguments to command line arguments (as string) "\"{}\"".format( - subprocess.list2cmdline(self.launch_context.launch_args) + subprocess.list2cmdline(launch_args) ) ] # Convert list to string diff --git a/openpype/hosts/blender/plugins/load/load_look.py b/openpype/hosts/blender/plugins/load/load_look.py new file mode 100644 index 0000000000..279af2b626 --- /dev/null +++ b/openpype/hosts/blender/plugins/load/load_look.py @@ -0,0 +1,218 @@ +"""Load a model asset in Blender.""" + +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import os +import json +import bpy + +from avalon import api, blender +import openpype.hosts.blender.api.plugin as plugin + + +class BlendLookLoader(plugin.AssetLoader): + """Load models from a .blend file. + + Because they come from a .blend file we can simply link the collection that + contains the model. There is no further need to 'containerise' it. + """ + + families = ["look"] + representations = ["json"] + + label = "Load Look" + icon = "code-fork" + color = "orange" + + def get_all_children(self, obj): + children = list(obj.children) + + for child in children: + children.extend(child.children) + + return children + + def _process(self, libpath, container_name, objects): + with open(libpath, "r") as fp: + data = json.load(fp) + + path = os.path.dirname(libpath) + materials_path = f"{path}/resources" + + materials = [] + + for entry in data: + file = entry.get('fbx_filename') + if file is None: + continue + + bpy.ops.import_scene.fbx(filepath=f"{materials_path}/{file}") + + mesh = [o for o in bpy.context.scene.objects if o.select_get()][0] + material = mesh.data.materials[0] + material.name = f"{material.name}:{container_name}" + + texture_file = entry.get('tga_filename') + if texture_file: + node_tree = material.node_tree + pbsdf = node_tree.nodes['Principled BSDF'] + base_color = pbsdf.inputs[0] + tex_node = base_color.links[0].from_node + tex_node.image.filepath = f"{materials_path}/{texture_file}" + + materials.append(material) + + for obj in objects: + for child in self.get_all_children(obj): + mesh_name = child.name.split(':')[0] + if mesh_name == material.name.split(':')[0]: + child.data.materials.clear() + child.data.materials.append(material) + break + + bpy.data.objects.remove(mesh) + + return materials, objects + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + + lib_container = plugin.asset_name( + asset, subset + ) + unique_number = plugin.get_unique_number( + asset, subset + ) + namespace = namespace or f"{asset}_{unique_number}" + container_name = plugin.asset_name( + asset, subset, unique_number + ) + + container = bpy.data.collections.new(lib_container) + container.name = container_name + blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + metadata = container.get(blender.pipeline.AVALON_PROPERTY) + + metadata["libpath"] = libpath + metadata["lib_container"] = lib_container + + selected = [o for o in bpy.context.scene.objects if o.select_get()] + + materials, objects = self._process(libpath, container_name, selected) + + # Save the list of imported materials in the metadata container + metadata["objects"] = objects + metadata["materials"] = materials + + metadata["parent"] = str(context["representation"]["parent"]) + metadata["family"] = context["representation"]["context"]["family"] + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + def update(self, container: Dict, representation: Dict): + collection = bpy.data.collections.get(container["objectName"]) + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + self.log.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert not (collection.children), ( + "Nested collections are not supported." + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + collection_metadata = collection.get(blender.pipeline.AVALON_PROPERTY) + collection_libpath = collection_metadata["libpath"] + + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + self.log.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + self.log.info("Library already loaded, not updating...") + return + + for obj in collection_metadata['objects']: + for child in self.get_all_children(obj): + child.data.materials.clear() + + for material in collection_metadata['materials']: + bpy.data.materials.remove(material) + + namespace = collection_metadata['namespace'] + name = collection_metadata['name'] + + container_name = f"{namespace}_{name}" + + materials, objects = self._process( + libpath, container_name, collection_metadata['objects']) + + collection_metadata["objects"] = objects + collection_metadata["materials"] = materials + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + def remove(self, container: Dict) -> bool: + collection = bpy.data.collections.get(container["objectName"]) + if not collection: + return False + + collection_metadata = collection.get(blender.pipeline.AVALON_PROPERTY) + + for obj in collection_metadata['objects']: + for child in self.get_all_children(obj): + child.data.materials.clear() + + for material in collection_metadata['materials']: + bpy.data.materials.remove(material) + + bpy.data.collections.remove(collection) + + return True diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index dc74348949..05149eacc1 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -54,6 +54,14 @@ class ExtractFBX(openpype.api.Extractor): # We set the scale of the scene for the export scene.unit_settings.scale_length = 0.01 + new_materials = [] + + for obj in collections[0].all_objects: + if obj.type == 'MESH': + mat = bpy.data.materials.new(obj.name) + obj.data.materials.append(mat) + new_materials.append(mat) + # We export the fbx bpy.ops.export_scene.fbx( filepath=filepath, @@ -66,6 +74,13 @@ class ExtractFBX(openpype.api.Extractor): scene.unit_settings.scale_length = old_scale + for mat in new_materials: + bpy.data.materials.remove(mat) + + for obj in collections[0].all_objects: + if obj.type == 'MESH': + obj.data.materials.pop() + if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/hosts/hiero/otio/hiero_export.py b/openpype/hosts/hiero/otio/hiero_export.py index 6e751d3aa4..ccc05d5fd7 100644 --- a/openpype/hosts/hiero/otio/hiero_export.py +++ b/openpype/hosts/hiero/otio/hiero_export.py @@ -62,6 +62,76 @@ def _get_metadata(item): return {} +def create_time_effects(otio_clip, track_item): + # get all subtrack items + subTrackItems = flatten(track_item.parent().subTrackItems()) + speed = track_item.playbackSpeed() + + otio_effect = None + # retime on track item + if speed != 1.: + # make effect + otio_effect = otio.schema.LinearTimeWarp() + otio_effect.name = "Speed" + otio_effect.time_scalar = speed + otio_effect.metadata = {} + + # freeze frame effect + if speed == 0.: + otio_effect = otio.schema.FreezeFrame() + otio_effect.name = "FreezeFrame" + otio_effect.metadata = {} + + if otio_effect: + # add otio effect to clip effects + otio_clip.effects.append(otio_effect) + + # loop trought and get all Timewarps + for effect in subTrackItems: + if ((track_item not in effect.linkedItems()) + and (len(effect.linkedItems()) > 0)): + continue + # avoid all effect which are not TimeWarp and disabled + if "TimeWarp" not in effect.name(): + continue + + if not effect.isEnabled(): + continue + + node = effect.node() + name = node["name"].value() + + # solve effect class as effect name + _name = effect.name() + if "_" in _name: + effect_name = re.sub(r"(?:_)[_0-9]+", "", _name) # more numbers + else: + effect_name = re.sub(r"\d+", "", _name) # one number + + metadata = {} + # add knob to metadata + for knob in ["lookup", "length"]: + value = node[knob].value() + animated = node[knob].isAnimated() + if animated: + value = [ + ((node[knob].getValueAt(i)) - i) + for i in range( + track_item.timelineIn(), track_item.timelineOut() + 1) + ] + + metadata[knob] = value + + # make effect + otio_effect = otio.schema.TimeEffect() + otio_effect.name = name + otio_effect.effect_name = effect_name + otio_effect.metadata = metadata + + # add otio effect to clip effects + otio_clip.effects.append(otio_effect) + + def create_otio_reference(clip): metadata = _get_metadata(clip) media_source = clip.mediaSource() @@ -197,8 +267,12 @@ def create_otio_markers(otio_item, item): def create_otio_clip(track_item): clip = track_item.source() - source_in = track_item.sourceIn() - duration = track_item.sourceDuration() + speed = track_item.playbackSpeed() + # flip if speed is in minus + source_in = track_item.sourceIn() if speed > 0 else track_item.sourceOut() + + duration = int(track_item.duration()) + fps = utils.get_rate(track_item) or self.project_fps name = track_item.name() @@ -220,6 +294,11 @@ def create_otio_clip(track_item): create_otio_markers(otio_clip, track_item) create_otio_markers(otio_clip, track_item.source()) + # only if video + if not clip.mediaSource().hasAudio(): + # Add effects to clips + create_time_effects(otio_clip, track_item) + return otio_clip diff --git a/openpype/hosts/hiero/plugins/_publish/collect_calculate_retime.py b/openpype/hosts/hiero/plugins/_publish/collect_calculate_retime.py deleted file mode 100644 index 1b2f047da2..0000000000 --- a/openpype/hosts/hiero/plugins/_publish/collect_calculate_retime.py +++ /dev/null @@ -1,121 +0,0 @@ -from pyblish import api -import hiero -import math - - -class CollectCalculateRetime(api.InstancePlugin): - """Calculate Retiming of selected track items.""" - - order = api.CollectorOrder + 0.02 - label = "Collect Calculate Retiming" - hosts = ["hiero"] - families = ['retime'] - - def process(self, instance): - margin_in = instance.data["retimeMarginIn"] - margin_out = instance.data["retimeMarginOut"] - self.log.debug("margin_in: '{0}', margin_out: '{1}'".format(margin_in, margin_out)) - - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] - - track_item = instance.data["item"] - - # define basic clip frame range variables - timeline_in = int(track_item.timelineIn()) - timeline_out = int(track_item.timelineOut()) - source_in = int(track_item.sourceIn()) - source_out = int(track_item.sourceOut()) - speed = track_item.playbackSpeed() - self.log.debug("_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`,\ - \n source_in: `{2}`,\n source_out: `{3}`,\n speed: `{4}`,\n handle_start: `{5}`,\n handle_end: `{6}`".format( - timeline_in, - timeline_out, - source_in, - source_out, - speed, - handle_start, - handle_end - )) - - # loop withing subtrack items - source_in_change = 0 - source_out_change = 0 - for s_track_item in track_item.linkedItems(): - if isinstance(s_track_item, hiero.core.EffectTrackItem) \ - and "TimeWarp" in s_track_item.node().Class(): - - # adding timewarp attribute to instance - if not instance.data.get("timeWarpNodes", None): - instance.data["timeWarpNodes"] = list() - - # ignore item if not enabled - if s_track_item.isEnabled(): - node = s_track_item.node() - name = node["name"].value() - look_up = node["lookup"].value() - animated = node["lookup"].isAnimated() - if animated: - look_up = [((node["lookup"].getValueAt(i)) - i) - for i in range((timeline_in - handle_start), (timeline_out + handle_end) + 1) - ] - # calculate differnce - diff_in = (node["lookup"].getValueAt( - timeline_in)) - timeline_in - diff_out = (node["lookup"].getValueAt( - timeline_out)) - timeline_out - - # calculate source - source_in_change += diff_in - source_out_change += diff_out - - # calculate speed - speed_in = (node["lookup"].getValueAt(timeline_in) / ( - float(timeline_in) * .01)) * .01 - speed_out = (node["lookup"].getValueAt(timeline_out) / ( - float(timeline_out) * .01)) * .01 - - # calculate handles - handle_start = int( - math.ceil( - (handle_start * speed_in * 1000) / 1000.0) - ) - - handle_end = int( - math.ceil( - (handle_end * speed_out * 1000) / 1000.0) - ) - self.log.debug( - ("diff_in, diff_out", diff_in, diff_out)) - self.log.debug( - ("source_in_change, source_out_change", source_in_change, source_out_change)) - - instance.data["timeWarpNodes"].append({"Class": "TimeWarp", - "name": name, - "lookup": look_up}) - - self.log.debug((source_in_change, source_out_change)) - # recalculate handles by the speed - handle_start *= speed - handle_end *= speed - self.log.debug("speed: handle_start: '{0}', handle_end: '{1}'".format(handle_start, handle_end)) - - source_in += int(source_in_change) - source_out += int(source_out_change * speed) - handle_start += (margin_in) - handle_end += (margin_out) - self.log.debug("margin: handle_start: '{0}', handle_end: '{1}'".format(handle_start, handle_end)) - - # add all data to Instance - instance.data["sourceIn"] = source_in - instance.data["sourceOut"] = source_out - instance.data["sourceInH"] = int(source_in - math.ceil( - (handle_start * 1000) / 1000.0)) - instance.data["sourceOutH"] = int(source_out + math.ceil( - (handle_end * 1000) / 1000.0)) - instance.data["speed"] = speed - - self.log.debug("timeWarpNodes: {}".format(instance.data["timeWarpNodes"])) - self.log.debug("sourceIn: {}".format(instance.data["sourceIn"])) - self.log.debug("sourceOut: {}".format(instance.data["sourceOut"])) - self.log.debug("speed: {}".format(instance.data["speed"])) diff --git a/openpype/hosts/hiero/plugins/_publish/collect_framerate.py b/openpype/hosts/hiero/plugins/_publish/collect_framerate.py deleted file mode 100644 index e11433adb1..0000000000 --- a/openpype/hosts/hiero/plugins/_publish/collect_framerate.py +++ /dev/null @@ -1,23 +0,0 @@ -from pyblish import api - - -class CollectFramerate(api.ContextPlugin): - """Collect framerate from selected sequence.""" - - order = api.CollectorOrder + 0.001 - label = "Collect Framerate" - hosts = ["hiero"] - - def process(self, context): - sequence = context.data["activeSequence"] - context.data["fps"] = self.get_rate(sequence) - self.log.info("Framerate is collected: {}".format(context.data["fps"])) - - def get_rate(self, sequence): - num, den = sequence.framerate().toRational() - rate = float(num) / float(den) - - if rate.is_integer(): - return rate - - return round(rate, 3) diff --git a/openpype/hosts/hiero/plugins/_publish/collect_metadata.py b/openpype/hosts/hiero/plugins/_publish/collect_metadata.py deleted file mode 100644 index c85cb4e898..0000000000 --- a/openpype/hosts/hiero/plugins/_publish/collect_metadata.py +++ /dev/null @@ -1,30 +0,0 @@ -from pyblish import api - - -class CollectClipMetadata(api.InstancePlugin): - """Collect Metadata from selected track items.""" - - order = api.CollectorOrder + 0.01 - label = "Collect Metadata" - hosts = ["hiero"] - - def process(self, instance): - item = instance.data["item"] - ti_metadata = self.metadata_to_string(dict(item.metadata())) - ms_metadata = self.metadata_to_string( - dict(item.source().mediaSource().metadata())) - - instance.data["clipMetadata"] = ti_metadata - instance.data["mediaSourceMetadata"] = ms_metadata - - self.log.info(instance.data["clipMetadata"]) - self.log.info(instance.data["mediaSourceMetadata"]) - return - - def metadata_to_string(self, metadata): - data = dict() - for k, v in metadata.items(): - if v not in ["-", ""]: - data[str(k)] = v - - return data diff --git a/openpype/hosts/hiero/plugins/_publish/collect_timecodes.py b/openpype/hosts/hiero/plugins/_publish/collect_timecodes.py deleted file mode 100644 index e79ee27a15..0000000000 --- a/openpype/hosts/hiero/plugins/_publish/collect_timecodes.py +++ /dev/null @@ -1,90 +0,0 @@ -import pyblish.api -import opentimelineio.opentime as otio_ot - - -class CollectClipTimecodes(pyblish.api.InstancePlugin): - """Collect time with OpenTimelineIO: - source_h(In,Out)[timecode, sec] - timeline(In,Out)[timecode, sec] - """ - - order = pyblish.api.CollectorOrder + 0.101 - label = "Collect Timecodes" - hosts = ["hiero"] - - def process(self, instance): - - data = dict() - self.log.debug("__ instance.data: {}".format(instance.data)) - # Timeline data. - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] - - source_in_h = instance.data("sourceInH", - instance.data("sourceIn") - handle_start) - source_out_h = instance.data("sourceOutH", - instance.data("sourceOut") + handle_end) - - timeline_in = instance.data["clipIn"] - timeline_out = instance.data["clipOut"] - - # set frame start with tag or take it from timeline - frame_start = instance.data.get("startingFrame") - - if not frame_start: - frame_start = timeline_in - - source = instance.data.get("source") - - otio_data = dict() - self.log.debug("__ source: `{}`".format(source)) - - rate_fps = instance.context.data["fps"] - - otio_in_h_ratio = otio_ot.RationalTime( - value=(source.timecodeStart() + ( - source_in_h + (source_out_h - source_in_h))), - rate=rate_fps) - - otio_out_h_ratio = otio_ot.RationalTime( - value=(source.timecodeStart() + source_in_h), - rate=rate_fps) - - otio_timeline_in_ratio = otio_ot.RationalTime( - value=int( - instance.data.get("timelineTimecodeStart", 0)) + timeline_in, - rate=rate_fps) - - otio_timeline_out_ratio = otio_ot.RationalTime( - value=int( - instance.data.get("timelineTimecodeStart", 0)) + timeline_out, - rate=rate_fps) - - otio_data.update({ - - "otioClipInHTimecode": otio_ot.to_timecode(otio_in_h_ratio), - - "otioClipOutHTimecode": otio_ot.to_timecode(otio_out_h_ratio), - - "otioClipInHSec": otio_ot.to_seconds(otio_in_h_ratio), - - "otioClipOutHSec": otio_ot.to_seconds(otio_out_h_ratio), - - "otioTimelineInTimecode": otio_ot.to_timecode( - otio_timeline_in_ratio), - - "otioTimelineOutTimecode": otio_ot.to_timecode( - otio_timeline_out_ratio), - - "otioTimelineInSec": otio_ot.to_seconds(otio_timeline_in_ratio), - - "otioTimelineOutSec": otio_ot.to_seconds(otio_timeline_out_ratio) - }) - - data.update({ - "otioData": otio_data, - "sourceTimecodeIn": otio_ot.to_timecode(otio_in_h_ratio), - "sourceTimecodeOut": otio_ot.to_timecode(otio_out_h_ratio) - }) - instance.data.update(data) - self.log.debug("data: {}".format(instance.data)) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py index 5a9f89651c..b0b171fb61 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py @@ -6,7 +6,7 @@ class PreCollectClipEffects(pyblish.api.InstancePlugin): """Collect soft effects instances.""" order = pyblish.api.CollectorOrder - 0.579 - label = "Pre-collect Clip Effects Instances" + label = "Precollect Clip Effects Instances" families = ["clip"] def process(self, instance): @@ -40,6 +40,12 @@ class PreCollectClipEffects(pyblish.api.InstancePlugin): if review and review_track_index == _track_index: continue for sitem in sub_track_items: + effect = None + # make sure this subtrack item is relative of track item + if ((track_item not in sitem.linkedItems()) + and (len(sitem.linkedItems()) > 0)): + continue + if not (track_index <= _track_index): continue @@ -162,7 +168,7 @@ class PreCollectClipEffects(pyblish.api.InstancePlugin): # grab animation including handles knob_anim = [node[knob].getValueAt(i) for i in range( - self.clip_in_h, self.clip_in_h + 1)] + self.clip_in_h, self.clip_out_h + 1)] node_serialized[knob] = knob_anim else: diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index f7449561ef..92e0a70d15 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -133,6 +133,13 @@ class PrecollectInstances(pyblish.api.ContextPlugin): # create audio subset instance self.create_audio_instance(context, **data) + # add colorspace data + instance.data.update({ + "versionData": { + "colorspace": track_item.sourceMediaColourTransform(), + } + }) + # add audioReview attribute to plate instance data # if reviewTrack is on if tag_data.get("reviewTrack") is not None: @@ -304,9 +311,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin): @staticmethod def create_otio_time_range_from_timeline_item_data(track_item): + speed = track_item.playbackSpeed() timeline = phiero.get_current_sequence() frame_start = int(track_item.timelineIn()) - frame_duration = int(track_item.sourceDuration()) + frame_duration = int(track_item.sourceDuration() / speed) fps = timeline.framerate().toFloat() return hiero_export.create_otio_time_range( @@ -376,6 +384,8 @@ class PrecollectInstances(pyblish.api.ContextPlugin): subtracks = [] subTrackItems = flatten(clip.parent().subTrackItems()) for item in subTrackItems: + if "TimeWarp" in item.name(): + continue # avoid all anotation if isinstance(item, hiero.core.Annotation): continue diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_frame_ranges.py b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_frame_ranges.py deleted file mode 100644 index 21e12e89fa..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_frame_ranges.py +++ /dev/null @@ -1,70 +0,0 @@ -import pyblish.api - - -class CollectFrameRanges(pyblish.api.InstancePlugin): - """ Collect all framranges. - """ - - order = pyblish.api.CollectorOrder - 0.1 - label = "Collect Frame Ranges" - hosts = ["hiero"] - families = ["clip", "effect"] - - def process(self, instance): - - data = dict() - track_item = instance.data["item"] - - # handles - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] - - # source frame ranges - source_in = int(track_item.sourceIn()) - source_out = int(track_item.sourceOut()) - source_in_h = int(source_in - handle_start) - source_out_h = int(source_out + handle_end) - - # timeline frame ranges - clip_in = int(track_item.timelineIn()) - clip_out = int(track_item.timelineOut()) - clip_in_h = clip_in - handle_start - clip_out_h = clip_out + handle_end - - # durations - clip_duration = (clip_out - clip_in) + 1 - clip_duration_h = clip_duration + (handle_start + handle_end) - - # set frame start with tag or take it from timeline `startingFrame` - frame_start = instance.data.get("workfileFrameStart") - - if not frame_start: - frame_start = clip_in - - frame_end = frame_start + (clip_out - clip_in) - - data.update({ - # media source frame range - "sourceIn": source_in, - "sourceOut": source_out, - "sourceInH": source_in_h, - "sourceOutH": source_out_h, - - # timeline frame range - "clipIn": clip_in, - "clipOut": clip_out, - "clipInH": clip_in_h, - "clipOutH": clip_out_h, - - # workfile frame range - "frameStart": frame_start, - "frameEnd": frame_end, - - "clipDuration": clip_duration, - "clipDurationH": clip_duration_h, - - "fps": instance.context.data["fps"] - }) - self.log.info("Frame range data for instance `{}` are: {}".format( - instance, data)) - instance.data.update(data) diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_hierarchy_context.py b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_hierarchy_context.py deleted file mode 100644 index 0696a58e39..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_hierarchy_context.py +++ /dev/null @@ -1,116 +0,0 @@ -import pyblish.api -import avalon.api as avalon - - -class CollectHierarchy(pyblish.api.ContextPlugin): - """Collecting hierarchy from `parents`. - - present in `clip` family instances coming from the request json data file - - It will add `hierarchical_context` into each instance for integrate - plugins to be able to create needed parents for the context if they - don't exist yet - """ - - label = "Collect Hierarchy" - order = pyblish.api.CollectorOrder - families = ["clip"] - - def process(self, context): - temp_context = {} - project_name = avalon.Session["AVALON_PROJECT"] - final_context = {} - final_context[project_name] = {} - final_context[project_name]['entity_type'] = 'Project' - - for instance in context: - self.log.info("Processing instance: `{}` ...".format(instance)) - - # shot data dict - shot_data = {} - families = instance.data.get("families") - - # filter out all unepropriate instances - if not instance.data["publish"]: - continue - if not families: - continue - # exclude other families then self.families with intersection - if not set(self.families).intersection(families): - continue - - # exclude if not heroTrack True - if not instance.data.get("heroTrack"): - continue - - # update families to include `shot` for hierarchy integration - instance.data["families"] = families + ["shot"] - - # get asset build data if any available - shot_data["inputs"] = [ - x["_id"] for x in instance.data.get("assetbuilds", []) - ] - - # suppose that all instances are Shots - shot_data['entity_type'] = 'Shot' - shot_data['tasks'] = instance.data.get("tasks") or [] - shot_data["comments"] = instance.data.get("comments", []) - - shot_data['custom_attributes'] = { - "handleStart": instance.data["handleStart"], - "handleEnd": instance.data["handleEnd"], - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], - "clipIn": instance.data["clipIn"], - "clipOut": instance.data["clipOut"], - 'fps': instance.context.data["fps"], - "resolutionWidth": instance.data["resolutionWidth"], - "resolutionHeight": instance.data["resolutionHeight"], - "pixelAspect": instance.data["pixelAspect"] - } - - actual = {instance.data["asset"]: shot_data} - - for parent in reversed(instance.data["parents"]): - next_dict = {} - parent_name = parent["entity_name"] - next_dict[parent_name] = {} - next_dict[parent_name]["entity_type"] = parent[ - "entity_type"].capitalize() - next_dict[parent_name]["childs"] = actual - actual = next_dict - - temp_context = self._update_dict(temp_context, actual) - - # skip if nothing for hierarchy available - if not temp_context: - return - - final_context[project_name]['childs'] = temp_context - - # adding hierarchy context to context - context.data["hierarchyContext"] = final_context - self.log.debug("context.data[hierarchyContext] is: {}".format( - context.data["hierarchyContext"])) - - def _update_dict(self, parent_dict, child_dict): - """ - Nesting each children into its parent. - - Args: - parent_dict (dict): parent dict wich should be nested with children - child_dict (dict): children dict which should be injested - """ - - for key in parent_dict: - if key in child_dict and isinstance(parent_dict[key], dict): - child_dict[key] = self._update_dict( - parent_dict[key], child_dict[key] - ) - else: - if parent_dict.get(key) and child_dict.get(key): - continue - else: - child_dict[key] = parent_dict[key] - - return child_dict diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_plates.py b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_plates.py deleted file mode 100644 index d4f98f509e..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_plates.py +++ /dev/null @@ -1,169 +0,0 @@ -from pyblish import api -import os -import re -import clique - - -class CollectPlates(api.InstancePlugin): - """Collect plate representations. - """ - - # Run just before CollectSubsets - order = api.CollectorOrder + 0.1020 - label = "Collect Plates" - hosts = ["hiero"] - families = ["plate"] - - def process(self, instance): - # add to representations - if not instance.data.get("representations"): - instance.data["representations"] = list() - - self.main_clip = instance.data["item"] - # get plate source attributes - source_media = instance.data["sourceMedia"] - source_path = instance.data["sourcePath"] - source_first = instance.data["sourceFirst"] - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] - source_in = instance.data["sourceIn"] - source_out = instance.data["sourceOut"] - source_in_h = instance.data["sourceInH"] - source_out_h = instance.data["sourceOutH"] - - # define if review media is sequence - is_sequence = bool(not source_media.singleFile()) - self.log.debug("is_sequence: {}".format(is_sequence)) - - file_dir = os.path.dirname(source_path) - file = os.path.basename(source_path) - ext = os.path.splitext(file)[-1] - - # detect if sequence - if not is_sequence: - # is video file - files = file - else: - files = list() - spliter, padding = self.detect_sequence(file) - self.log.debug("_ spliter, padding: {}, {}".format( - spliter, padding)) - base_name = file.split(spliter)[0] - - # define collection and calculate frame range - collection = clique.Collection( - base_name, - ext, - padding, - set(range( - int(source_first + source_in_h), - int(source_first + source_out_h) + 1 - )) - ) - self.log.debug("_ collection: {}".format(collection)) - - real_files = os.listdir(file_dir) - self.log.debug("_ real_files: {}".format(real_files)) - - # collect frames to repre files list - self.handle_start_exclude = list() - self.handle_end_exclude = list() - for findex, item in enumerate(collection): - if item not in real_files: - self.log.debug("_ item: {}".format(item)) - test_index = findex + int(source_first + source_in_h) - test_start = int(source_first + source_in) - test_end = int(source_first + source_out) - if (test_index < test_start): - self.handle_start_exclude.append(test_index) - elif (test_index > test_end): - self.handle_end_exclude.append(test_index) - continue - files.append(item) - - # change label - instance.data["label"] = "{0} - ({1})".format( - instance.data["label"], ext - ) - - self.log.debug("Instance review: {}".format(instance.data["name"])) - - # adding representation for review mov - representation = { - "files": files, - "stagingDir": file_dir, - "frameStart": frame_start - handle_start, - "frameEnd": frame_end + handle_end, - "name": ext[1:], - "ext": ext[1:] - } - - instance.data["representations"].append(representation) - self.version_data(instance) - - self.log.debug( - "Added representations: {}".format( - instance.data["representations"])) - - self.log.debug( - "instance.data: {}".format(instance.data)) - - def version_data(self, instance): - transfer_data = [ - "handleStart", "handleEnd", "sourceIn", "sourceOut", - "frameStart", "frameEnd", "sourceInH", "sourceOutH", - "clipIn", "clipOut", "clipInH", "clipOutH", "asset", - "track" - ] - - version_data = dict() - # pass data to version - version_data.update({k: instance.data[k] for k in transfer_data}) - - if 'version' in instance.data: - version_data["version"] = instance.data["version"] - - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] - - if self.handle_start_exclude: - handle_start -= len(self.handle_start_exclude) - - if self.handle_end_exclude: - handle_end -= len(self.handle_end_exclude) - - # add to data of representation - version_data.update({ - "colorspace": self.main_clip.sourceMediaColourTransform(), - "families": instance.data["families"], - "subset": instance.data["subset"], - "fps": instance.data["fps"], - "handleStart": handle_start, - "handleEnd": handle_end - }) - instance.data["versionData"] = version_data - - def detect_sequence(self, file): - """ Get identificating pater for image sequence - - Can find file.0001.ext, file.%02d.ext, file.####.ext - - Return: - string: any matching sequence patern - int: padding of sequnce numbering - """ - foundall = re.findall( - r"(#+)|(%\d+d)|(?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$)", file) - if foundall: - found = sorted(list(set(foundall[0])))[-1] - - if "%" in found: - padding = int(re.findall(r"\d+", found)[-1]) - else: - padding = len(found) - - return found, padding - else: - return None, None diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_review.py b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_review.py deleted file mode 100644 index b1d97a71d7..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_review.py +++ /dev/null @@ -1,261 +0,0 @@ -from pyblish import api -import os -import clique -from openpype.hosts.hiero.api import ( - is_overlapping, get_sequence_pattern_and_padding) - - -class CollectReview(api.InstancePlugin): - """Collect review representation. - """ - - # Run just before CollectSubsets - order = api.CollectorOrder + 0.1022 - label = "Collect Review" - hosts = ["hiero"] - families = ["review"] - - def get_review_item(self, instance): - """ - Get review clip track item from review track name - - Args: - instance (obj): publishing instance - - Returns: - hiero.core.TrackItem: corresponding track item - - Raises: - Exception: description - - """ - review_track = instance.data.get("reviewTrack") - video_tracks = instance.context.data["videoTracks"] - for track in video_tracks: - if review_track not in track.name(): - continue - for item in track.items(): - self.log.debug(item) - if is_overlapping(item, self.main_clip): - self.log.debug("Winner is: {}".format(item)) - break - - # validate the clip is fully converted with review clip - assert is_overlapping( - item, self.main_clip, strict=True), ( - "Review clip not cowering fully " - "the clip `{}`").format(self.main_clip.name()) - - return item - - def process(self, instance): - tags = ["review", "ftrackreview"] - - # get reviewable item from `review` instance.data attribute - self.main_clip = instance.data.get("item") - self.rw_clip = self.get_review_item(instance) - - # let user know there is missing review clip and convert instance - # back as not reviewable - assert self.rw_clip, "Missing reviewable clip for '{}'".format( - self.main_clip.name() - ) - - # add to representations - if not instance.data.get("representations"): - instance.data["representations"] = list() - - # get review media main info - rw_source = self.rw_clip.source().mediaSource() - rw_source_duration = int(rw_source.duration()) - self.rw_source_path = rw_source.firstpath() - rw_source_file_info = rw_source.fileinfos().pop() - - # define if review media is sequence - is_sequence = bool(not rw_source.singleFile()) - self.log.debug("is_sequence: {}".format(is_sequence)) - - # get handles - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] - - # review timeline and source frame ranges - rw_clip_in = int(self.rw_clip.timelineIn()) - rw_clip_out = int(self.rw_clip.timelineOut()) - self.rw_clip_source_in = int(self.rw_clip.sourceIn()) - self.rw_clip_source_out = int(self.rw_clip.sourceOut()) - rw_source_first = int(rw_source_file_info.startFrame()) - - # calculate delivery source_in and source_out - # main_clip_timeline_in - review_item_timeline_in + 1 - main_clip_in = self.main_clip.timelineIn() - main_clip_out = self.main_clip.timelineOut() - - source_in_diff = main_clip_in - rw_clip_in - source_out_diff = main_clip_out - rw_clip_out - - if source_in_diff: - self.rw_clip_source_in += source_in_diff - if source_out_diff: - self.rw_clip_source_out += source_out_diff - - # review clip durations - rw_clip_duration = ( - self.rw_clip_source_out - self.rw_clip_source_in) + 1 - rw_clip_duration_h = rw_clip_duration + ( - handle_start + handle_end) - - # add created data to review item data - instance.data["reviewItemData"] = { - "mediaDuration": rw_source_duration - } - - file_dir = os.path.dirname(self.rw_source_path) - file = os.path.basename(self.rw_source_path) - ext = os.path.splitext(file)[-1] - - # detect if sequence - if not is_sequence: - # is video file - files = file - else: - files = list() - spliter, padding = get_sequence_pattern_and_padding(file) - self.log.debug("_ spliter, padding: {}, {}".format( - spliter, padding)) - base_name = file.split(spliter)[0] - - # define collection and calculate frame range - collection = clique.Collection(base_name, ext, padding, set(range( - int(rw_source_first + int( - self.rw_clip_source_in - handle_start)), - int(rw_source_first + int( - self.rw_clip_source_out + handle_end) + 1)))) - self.log.debug("_ collection: {}".format(collection)) - - real_files = os.listdir(file_dir) - self.log.debug("_ real_files: {}".format(real_files)) - - # collect frames to repre files list - for item in collection: - if item not in real_files: - self.log.debug("_ item: {}".format(item)) - continue - files.append(item) - - # add prep tag - tags.extend(["prep", "delete"]) - - # change label - instance.data["label"] = "{0} - ({1})".format( - instance.data["label"], ext - ) - - self.log.debug("Instance review: {}".format(instance.data["name"])) - - # adding representation for review mov - representation = { - "files": files, - "stagingDir": file_dir, - "frameStart": rw_source_first + self.rw_clip_source_in, - "frameEnd": rw_source_first + self.rw_clip_source_out, - "frameStartFtrack": int( - self.rw_clip_source_in - handle_start), - "frameEndFtrack": int(self.rw_clip_source_out + handle_end), - "step": 1, - "fps": instance.data["fps"], - "name": "review", - "tags": tags, - "ext": ext[1:] - } - - if rw_source_duration > rw_clip_duration_h: - self.log.debug("Media duration higher: {}".format( - (rw_source_duration - rw_clip_duration_h))) - representation.update({ - "frameStart": rw_source_first + int( - self.rw_clip_source_in - handle_start), - "frameEnd": rw_source_first + int( - self.rw_clip_source_out + handle_end), - "tags": ["_cut-bigger", "prep", "delete"] - }) - elif rw_source_duration < rw_clip_duration_h: - self.log.debug("Media duration higher: {}".format( - (rw_source_duration - rw_clip_duration_h))) - representation.update({ - "frameStart": rw_source_first + int( - self.rw_clip_source_in - handle_start), - "frameEnd": rw_source_first + int( - self.rw_clip_source_out + handle_end), - "tags": ["prep", "delete"] - }) - - instance.data["representations"].append(representation) - - self.create_thumbnail(instance) - - self.log.debug( - "Added representations: {}".format( - instance.data["representations"])) - - def create_thumbnail(self, instance): - source_file = os.path.basename(self.rw_source_path) - spliter, padding = get_sequence_pattern_and_padding(source_file) - - if spliter: - head, ext = source_file.split(spliter) - else: - head, ext = os.path.splitext(source_file) - - # staging dir creation - staging_dir = os.path.dirname( - self.rw_source_path) - - # get thumbnail frame from the middle - thumb_frame = int(self.rw_clip_source_in + ( - (self.rw_clip_source_out - self.rw_clip_source_in) / 2)) - - thumb_file = "{}thumbnail{}{}".format(head, thumb_frame, ".png") - thumb_path = os.path.join(staging_dir, thumb_file) - - thumbnail = self.rw_clip.thumbnail(thumb_frame).save( - thumb_path, - format='png' - ) - self.log.debug( - "__ thumbnail: `{}`, frame: `{}`".format(thumbnail, thumb_frame)) - - self.log.debug("__ thumbnail: {}".format(thumbnail)) - thumb_representation = { - 'files': thumb_file, - 'stagingDir': staging_dir, - 'name': "thumbnail", - 'thumbnail': True, - 'ext': "png" - } - instance.data["representations"].append( - thumb_representation) - - def version_data(self, instance): - transfer_data = [ - "handleStart", "handleEnd", "sourceIn", "sourceOut", - "frameStart", "frameEnd", "sourceInH", "sourceOutH", - "clipIn", "clipOut", "clipInH", "clipOutH", "asset", - "track" - ] - - version_data = dict() - # pass data to version - version_data.update({k: instance.data[k] for k in transfer_data}) - - if 'version' in instance.data: - version_data["version"] = instance.data["version"] - - # add to data of representation - version_data.update({ - "colorspace": self.rw_clip.sourceMediaColourTransform(), - "families": instance.data["families"], - "subset": instance.data["subset"], - "fps": instance.data["fps"] - }) - instance.data["versionData"] = version_data diff --git a/openpype/hosts/hiero/plugins/_publish/collect_tag_comments.py b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_tag_comments.py similarity index 100% rename from openpype/hosts/hiero/plugins/_publish/collect_tag_comments.py rename to openpype/hosts/hiero/plugins/publish_old_workflow/collect_tag_comments.py diff --git a/openpype/hosts/hiero/plugins/_publish/collect_tag_retime.py b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_tag_retime.py similarity index 100% rename from openpype/hosts/hiero/plugins/_publish/collect_tag_retime.py rename to openpype/hosts/hiero/plugins/publish_old_workflow/collect_tag_retime.py diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/extract_audio.py b/openpype/hosts/hiero/plugins/publish_old_workflow/extract_audio.py deleted file mode 100644 index 6d9abb58e2..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/extract_audio.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -from hiero.exporters.FnExportUtil import writeSequenceAudioWithHandles -import pyblish -import openpype - - -class ExtractAudioFile(openpype.api.Extractor): - """Extracts audio subset file from all active timeline audio tracks""" - - order = pyblish.api.ExtractorOrder - label = "Extract Subset Audio" - hosts = ["hiero"] - families = ["audio"] - match = pyblish.api.Intersection - - def process(self, instance): - # get sequence - sequence = instance.context.data["activeSequence"] - subset = instance.data["subset"] - - # get timeline in / out - clip_in = instance.data["clipIn"] - clip_out = instance.data["clipOut"] - # get handles from context - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] - - staging_dir = self.staging_dir(instance) - self.log.info("Created staging dir: {}...".format(staging_dir)) - - # path to wav file - audio_file = os.path.join( - staging_dir, "{}.wav".format(subset) - ) - - # export audio to disk - writeSequenceAudioWithHandles( - audio_file, - sequence, - clip_in, - clip_out, - handle_start, - handle_end - ) - - # add to representations - if not instance.data.get("representations"): - instance.data["representations"] = list() - - representation = { - 'files': os.path.basename(audio_file), - 'stagingDir': staging_dir, - 'name': "wav", - 'ext': "wav" - } - - instance.data["representations"].append(representation) diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/extract_review_preparation.py b/openpype/hosts/hiero/plugins/publish_old_workflow/extract_review_preparation.py deleted file mode 100644 index aac476e27a..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/extract_review_preparation.py +++ /dev/null @@ -1,334 +0,0 @@ -import os -import sys -import six -import errno -from pyblish import api -import openpype -import clique -from avalon.vendor import filelink - - -class ExtractReviewPreparation(openpype.api.Extractor): - """Cut up clips from long video file""" - - order = api.ExtractorOrder - label = "Extract Review Preparation" - hosts = ["hiero"] - families = ["review"] - - # presets - tags_addition = [] - - def process(self, instance): - inst_data = instance.data - asset = inst_data["asset"] - review_item_data = instance.data.get("reviewItemData") - - # get representation and loop them - representations = inst_data["representations"] - - # get resolution default - resolution_width = inst_data["resolutionWidth"] - resolution_height = inst_data["resolutionHeight"] - - # frame range data - media_duration = review_item_data["mediaDuration"] - - ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") - ffprobe_path = openpype.lib.get_ffmpeg_tool_path("ffprobe") - - # filter out mov and img sequences - representations_new = representations[:] - for repre in representations: - input_args = list() - output_args = list() - - tags = repre.get("tags", []) - - # check if supported tags are in representation for activation - filter_tag = False - for tag in ["_cut-bigger", "prep"]: - if tag in tags: - filter_tag = True - break - if not filter_tag: - continue - - self.log.debug("__ repre: {}".format(repre)) - - files = repre.get("files") - staging_dir = repre.get("stagingDir") - fps = repre.get("fps") - ext = repre.get("ext") - - # make paths - full_output_dir = os.path.join( - staging_dir, "cuts") - - if isinstance(files, list): - new_files = list() - - # frame range delivery included handles - frame_start = ( - inst_data["frameStart"] - inst_data["handleStart"]) - frame_end = ( - inst_data["frameEnd"] + inst_data["handleEnd"]) - self.log.debug("_ frame_start: {}".format(frame_start)) - self.log.debug("_ frame_end: {}".format(frame_end)) - - # make collection from input files list - collections, remainder = clique.assemble(files) - collection = collections.pop() - self.log.debug("_ collection: {}".format(collection)) - - # name components - head = collection.format("{head}") - padding = collection.format("{padding}") - tail = collection.format("{tail}") - self.log.debug("_ head: {}".format(head)) - self.log.debug("_ padding: {}".format(padding)) - self.log.debug("_ tail: {}".format(tail)) - - # make destination file with instance data - # frame start and end range - index = 0 - for image in collection: - dst_file_num = frame_start + index - dst_file_name = head + str(padding % dst_file_num) + tail - src = os.path.join(staging_dir, image) - dst = os.path.join(full_output_dir, dst_file_name) - self.log.info("Creating temp hardlinks: {}".format(dst)) - self.hardlink_file(src, dst) - new_files.append(dst_file_name) - index += 1 - - self.log.debug("_ new_files: {}".format(new_files)) - - else: - # ffmpeg when single file - new_files = "{}_{}".format(asset, files) - - # frame range - frame_start = repre.get("frameStart") - frame_end = repre.get("frameEnd") - - full_input_path = os.path.join( - staging_dir, files) - - os.path.isdir(full_output_dir) or os.makedirs(full_output_dir) - - full_output_path = os.path.join( - full_output_dir, new_files) - - self.log.debug( - "__ full_input_path: {}".format(full_input_path)) - self.log.debug( - "__ full_output_path: {}".format(full_output_path)) - - # check if audio stream is in input video file - ffprob_cmd = ( - "\"{ffprobe_path}\" -i \"{full_input_path}\" -show_streams" - " -select_streams a -loglevel error" - ).format(**locals()) - - self.log.debug("ffprob_cmd: {}".format(ffprob_cmd)) - audio_check_output = openpype.api.run_subprocess(ffprob_cmd) - self.log.debug( - "audio_check_output: {}".format(audio_check_output)) - - # Fix one frame difference - """ TODO: this is just work-around for issue: - https://github.com/pypeclub/pype/issues/659 - """ - frame_duration_extend = 1 - if audio_check_output and ("audio" in inst_data["families"]): - frame_duration_extend = 0 - - # translate frame to sec - start_sec = float(frame_start) / fps - duration_sec = float( - (frame_end - frame_start) + frame_duration_extend) / fps - - empty_add = None - - # check if not missing frames at start - if (start_sec < 0) or (media_duration < frame_end): - # for later swithing off `-c:v copy` output arg - empty_add = True - - # init empty variables - video_empty_start = video_layer_start = "" - audio_empty_start = audio_layer_start = "" - video_empty_end = video_layer_end = "" - audio_empty_end = audio_layer_end = "" - audio_input = audio_output = "" - v_inp_idx = 0 - concat_n = 1 - - # try to get video native resolution data - try: - resolution_output = openpype.api.run_subprocess(( - "\"{ffprobe_path}\" -i \"{full_input_path}\"" - " -v error " - "-select_streams v:0 -show_entries " - "stream=width,height -of csv=s=x:p=0" - ).format(**locals())) - - x, y = resolution_output.split("x") - resolution_width = int(x) - resolution_height = int(y) - except Exception as _ex: - self.log.warning( - "Video native resolution is untracable: {}".format( - _ex)) - - if audio_check_output: - # adding input for empty audio - input_args.append("-f lavfi -i anullsrc") - - # define audio empty concat variables - audio_input = "[1:a]" - audio_output = ":a=1" - v_inp_idx = 1 - - # adding input for video black frame - input_args.append(( - "-f lavfi -i \"color=c=black:" - "s={resolution_width}x{resolution_height}:r={fps}\"" - ).format(**locals())) - - if (start_sec < 0): - # recalculate input video timing - empty_start_dur = abs(start_sec) - start_sec = 0 - duration_sec = float(frame_end - ( - frame_start + (empty_start_dur * fps)) + 1) / fps - - # define starting empty video concat variables - video_empty_start = ( - "[{v_inp_idx}]trim=duration={empty_start_dur}[gv0];" # noqa - ).format(**locals()) - video_layer_start = "[gv0]" - - if audio_check_output: - # define starting empty audio concat variables - audio_empty_start = ( - "[0]atrim=duration={empty_start_dur}[ga0];" - ).format(**locals()) - audio_layer_start = "[ga0]" - - # alter concat number of clips - concat_n += 1 - - # check if not missing frames at the end - if (media_duration < frame_end): - # recalculate timing - empty_end_dur = float( - frame_end - media_duration + 1) / fps - duration_sec = float( - media_duration - frame_start) / fps - - # define ending empty video concat variables - video_empty_end = ( - "[{v_inp_idx}]trim=duration={empty_end_dur}[gv1];" - ).format(**locals()) - video_layer_end = "[gv1]" - - if audio_check_output: - # define ending empty audio concat variables - audio_empty_end = ( - "[0]atrim=duration={empty_end_dur}[ga1];" - ).format(**locals()) - audio_layer_end = "[ga0]" - - # alter concat number of clips - concat_n += 1 - - # concatting black frame togather - output_args.append(( - "-filter_complex \"" - "{audio_empty_start}" - "{video_empty_start}" - "{audio_empty_end}" - "{video_empty_end}" - "{video_layer_start}{audio_layer_start}[1:v]{audio_input}" # noqa - "{video_layer_end}{audio_layer_end}" - "concat=n={concat_n}:v=1{audio_output}\"" - ).format(**locals())) - - # append ffmpeg input video clip - input_args.append("-ss {}".format(start_sec)) - input_args.append("-t {}".format(duration_sec)) - input_args.append("-i \"{}\"".format(full_input_path)) - - # add copy audio video codec if only shortening clip - if ("_cut-bigger" in tags) and (not empty_add): - output_args.append("-c:v copy") - - # make sure it is having no frame to frame comprassion - output_args.append("-intra") - - # output filename - output_args.append("-y \"{}\"".format(full_output_path)) - - mov_args = [ - "\"{}\"".format(ffmpeg_path), - " ".join(input_args), - " ".join(output_args) - ] - subprcs_cmd = " ".join(mov_args) - - # run subprocess - self.log.debug("Executing: {}".format(subprcs_cmd)) - output = openpype.api.run_subprocess(subprcs_cmd) - self.log.debug("Output: {}".format(output)) - - repre_new = { - "files": new_files, - "stagingDir": full_output_dir, - "frameStart": frame_start, - "frameEnd": frame_end, - "frameStartFtrack": frame_start, - "frameEndFtrack": frame_end, - "step": 1, - "fps": fps, - "name": "cut_up_preview", - "tags": [ - "review", "ftrackreview", "delete"] + self.tags_addition, - "ext": ext, - "anatomy_template": "publish" - } - - representations_new.append(repre_new) - - for repre in representations_new: - if ("delete" in repre.get("tags", [])) and ( - "cut_up_preview" not in repre["name"]): - representations_new.remove(repre) - - self.log.debug( - "Representations: {}".format(representations_new)) - instance.data["representations"] = representations_new - - def hardlink_file(self, src, dst): - dirname = os.path.dirname(dst) - - # make sure the destination folder exist - try: - os.makedirs(dirname) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - - # create hardlined file - try: - filelink.create(src, dst, filelink.HARDLINK) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py b/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py new file mode 100644 index 0000000000..de05414c88 --- /dev/null +++ b/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py @@ -0,0 +1,171 @@ +from pyblish import api +import hiero +import math +from openpype.hosts.hiero.otio.hiero_export import create_otio_time_range + +class PrecollectRetime(api.InstancePlugin): + """Calculate Retiming of selected track items.""" + + order = api.CollectorOrder - 0.578 + label = "Precollect Retime" + hosts = ["hiero"] + families = ['retime_'] + + def process(self, instance): + if not instance.data.get("versionData"): + instance.data["versionData"] = {} + + # get basic variables + otio_clip = instance.data["otioClip"] + + source_range = otio_clip.source_range + oc_source_fps = source_range.start_time.rate + oc_source_in = source_range.start_time.value + + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] + frame_start = instance.data["frameStart"] + + track_item = instance.data["item"] + + # define basic clip frame range variables + timeline_in = int(track_item.timelineIn()) + timeline_out = int(track_item.timelineOut()) + source_in = int(track_item.sourceIn()) + source_out = int(track_item.sourceOut()) + speed = track_item.playbackSpeed() + + # calculate available material before retime + available_in = int(track_item.handleInLength() * speed) + available_out = int(track_item.handleOutLength() * speed) + + self.log.debug(( + "_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`, \n " + "source_in: `{2}`,\n source_out: `{3}`,\n speed: `{4}`,\n " + "handle_start: `{5}`,\n handle_end: `{6}`").format( + timeline_in, + timeline_out, + source_in, + source_out, + speed, + handle_start, + handle_end + )) + + # loop withing subtrack items + time_warp_nodes = [] + source_in_change = 0 + source_out_change = 0 + for s_track_item in track_item.linkedItems(): + if isinstance(s_track_item, hiero.core.EffectTrackItem) \ + and "TimeWarp" in s_track_item.node().Class(): + + # adding timewarp attribute to instance + time_warp_nodes = [] + + # ignore item if not enabled + if s_track_item.isEnabled(): + node = s_track_item.node() + name = node["name"].value() + look_up = node["lookup"].value() + animated = node["lookup"].isAnimated() + if animated: + look_up = [ + ((node["lookup"].getValueAt(i)) - i) + for i in range( + (timeline_in - handle_start), + (timeline_out + handle_end) + 1) + ] + # calculate differnce + diff_in = (node["lookup"].getValueAt( + timeline_in)) - timeline_in + diff_out = (node["lookup"].getValueAt( + timeline_out)) - timeline_out + + # calculate source + source_in_change += diff_in + source_out_change += diff_out + + # calculate speed + speed_in = (node["lookup"].getValueAt(timeline_in) / ( + float(timeline_in) * .01)) * .01 + speed_out = (node["lookup"].getValueAt(timeline_out) / ( + float(timeline_out) * .01)) * .01 + + # calculate handles + handle_start = int( + math.ceil( + (handle_start * speed_in * 1000) / 1000.0) + ) + + handle_end = int( + math.ceil( + (handle_end * speed_out * 1000) / 1000.0) + ) + self.log.debug( + ("diff_in, diff_out", diff_in, diff_out)) + self.log.debug( + ("source_in_change, source_out_change", + source_in_change, source_out_change)) + + time_warp_nodes.append({ + "Class": "TimeWarp", + "name": name, + "lookup": look_up + }) + + self.log.debug( + "timewarp source in changes: in {}, out {}".format( + source_in_change, source_out_change)) + + # recalculate handles by the speed + handle_start *= speed + handle_end *= speed + self.log.debug("speed: handle_start: '{0}', handle_end: '{1}'".format( + handle_start, handle_end)) + + # recalculate source with timewarp and by the speed + source_in += int(source_in_change) + source_out += int(source_out_change * speed) + + source_in_h = int(source_in - math.ceil( + (handle_start * 1000) / 1000.0)) + source_out_h = int(source_out + math.ceil( + (handle_end * 1000) / 1000.0)) + + self.log.debug( + "retimed: source_in_h: '{0}', source_out_h: '{1}'".format( + source_in_h, source_out_h)) + + # add all data to Instance + instance.data["handleStart"] = handle_start + instance.data["handleEnd"] = handle_end + instance.data["sourceIn"] = source_in + instance.data["sourceOut"] = source_out + instance.data["sourceInH"] = source_in_h + instance.data["sourceOutH"] = source_out_h + instance.data["speed"] = speed + + source_handle_start = source_in_h - source_in + # frame_start = instance.data["frameStart"] + source_handle_start + duration = source_out_h - source_in_h + frame_end = int(frame_start + duration - (handle_start + handle_end)) + + instance.data["versionData"].update({ + "retime": True, + "speed": speed, + "timewarps": time_warp_nodes, + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": abs(source_handle_start), + "handleEnd": source_out_h - source_out + }) + self.log.debug("versionData: {}".format(instance.data["versionData"])) + self.log.debug("sourceIn: {}".format(instance.data["sourceIn"])) + self.log.debug("sourceOut: {}".format(instance.data["sourceOut"])) + self.log.debug("speed: {}".format(instance.data["speed"])) + + # change otio clip data + instance.data["otioClip"].source_range = create_otio_time_range( + oc_source_in, (source_out - source_in + 1), oc_source_fps) + self.log.debug("otioClip: {}".format(instance.data["otioClip"])) diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/validate_audio.py b/openpype/hosts/hiero/plugins/publish_old_workflow/validate_audio.py deleted file mode 100644 index 0b2a94dc68..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/validate_audio.py +++ /dev/null @@ -1,25 +0,0 @@ -import pyblish -from openpype.hosts.hiero.api import is_overlapping - - -class ValidateAudioFile(pyblish.api.InstancePlugin): - """Validate audio subset has avilable audio track clips""" - - order = pyblish.api.ValidatorOrder - label = "Validate Audio Tracks" - hosts = ["hiero"] - families = ["audio"] - - def process(self, instance): - clip = instance.data["item"] - audio_tracks = instance.context.data["audioTracks"] - audio_clip = None - - for a_track in audio_tracks: - for item in a_track.items(): - if is_overlapping(item, clip): - audio_clip = item - - assert audio_clip, "Missing relative audio clip for clip {}".format( - clip.name() - ) diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/validate_hierarchy.py b/openpype/hosts/hiero/plugins/publish_old_workflow/validate_hierarchy.py deleted file mode 100644 index d43f7fd562..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/validate_hierarchy.py +++ /dev/null @@ -1,22 +0,0 @@ -from pyblish import api - - -class ValidateHierarchy(api.InstancePlugin): - """Validate clip's hierarchy data. - - """ - - order = api.ValidatorOrder - families = ["clip", "shot"] - label = "Validate Hierarchy" - hosts = ["hiero"] - - def process(self, instance): - asset_name = instance.data.get("asset", None) - hierarchy = instance.data.get("hierarchy", None) - parents = instance.data.get("parents", None) - - assert hierarchy, "Hierarchy Tag has to be set \ - and added to clip `{}`".format(asset_name) - assert parents, "Parents build from Hierarchy Tag has \ - to be set and added to clip `{}`".format(asset_name) diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/validate_names.py b/openpype/hosts/hiero/plugins/publish_old_workflow/validate_names.py deleted file mode 100644 index 52e4bf8ecc..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/validate_names.py +++ /dev/null @@ -1,31 +0,0 @@ -from pyblish import api - - -class ValidateNames(api.InstancePlugin): - """Validate sequence, video track and track item names. - - When creating output directories with the name of an item, ending with a - whitespace will fail the extraction. - Exact matching to optimize processing. - """ - - order = api.ValidatorOrder - families = ["clip"] - match = api.Exact - label = "Names" - hosts = ["hiero"] - - def process(self, instance): - - item = instance.data["item"] - - msg = "Track item \"{0}\" ends with a whitespace." - assert not item.name().endswith(" "), msg.format(item.name()) - - msg = "Video track \"{0}\" ends with a whitespace." - msg = msg.format(item.parent().name()) - assert not item.parent().name().endswith(" "), msg - - msg = "Sequence \"{0}\" ends with a whitespace." - msg = msg.format(item.parent().parent().name()) - assert not item.parent().parent().name().endswith(" "), msg diff --git a/openpype/hosts/maya/api/expected_files.py b/openpype/hosts/maya/api/expected_files.py index c6232f6ca4..15e0dc598c 100644 --- a/openpype/hosts/maya/api/expected_files.py +++ b/openpype/hosts/maya/api/expected_files.py @@ -43,6 +43,7 @@ import os from abc import ABCMeta, abstractmethod import six +import attr import openpype.hosts.maya.api.lib as lib @@ -88,6 +89,22 @@ IMAGE_PREFIXES = { } +@attr.s +class LayerMetadata(object): + """Data class for Render Layer metadata.""" + frameStart = attr.ib() + frameEnd = attr.ib() + cameras = attr.ib() + sceneName = attr.ib() + layerName = attr.ib() + renderer = attr.ib() + defaultExt = attr.ib() + filePrefix = attr.ib() + enabledAOVs = attr.ib() + frameStep = attr.ib(default=1) + padding = attr.ib(default=4) + + class ExpectedFiles: """Class grouping functionality for all supported renderers. @@ -95,7 +112,6 @@ class ExpectedFiles: multipart (bool): Flag if multipart exrs are used. """ - multipart = False def __init__(self, render_instance): @@ -142,6 +158,7 @@ class ExpectedFiles: ) def _get_files(self, renderer): + # type: (AExpectedFiles) -> list files = renderer.get_files() self.multipart = renderer.multipart return files @@ -193,7 +210,7 @@ class AExpectedFiles: def get_renderer_prefix(self): """Return prefix for specific renderer. - This is for most renderers the same and can be overriden if needed. + This is for most renderers the same and can be overridden if needed. Returns: str: String with image prefix containing tokens @@ -214,6 +231,7 @@ class AExpectedFiles: return file_prefix def _get_layer_data(self): + # type: () -> LayerMetadata # ______________________________________________ # ____________________/ ____________________________________________/ # 1 - get scene name /__________________/ @@ -230,30 +248,31 @@ class AExpectedFiles: if self.layer.startswith("rs_"): layer_name = self.layer[3:] - return { - "frameStart": int(self.get_render_attribute("startFrame")), - "frameEnd": int(self.get_render_attribute("endFrame")), - "frameStep": int(self.get_render_attribute("byFrameStep")), - "padding": int(self.get_render_attribute("extensionPadding")), + return LayerMetadata( + frameStart=int(self.get_render_attribute("startFrame")), + frameEnd=int(self.get_render_attribute("endFrame")), + frameStep=int(self.get_render_attribute("byFrameStep")), + padding=int(self.get_render_attribute("extensionPadding")), # if we have token in prefix path we'll expect output for # every renderable camera in layer. - "cameras": self.get_renderable_cameras(), - "sceneName": scene_name, - "layerName": layer_name, - "renderer": self.renderer, - "defaultExt": cmds.getAttr("defaultRenderGlobals.imfPluginKey"), - "filePrefix": file_prefix, - "enabledAOVs": self.get_aovs(), - } + cameras=self.get_renderable_cameras(), + sceneName=scene_name, + layerName=layer_name, + renderer=self.renderer, + defaultExt=cmds.getAttr("defaultRenderGlobals.imfPluginKey"), + filePrefix=file_prefix, + enabledAOVs=self.get_aovs() + ) def _generate_single_file_sequence( self, layer_data, force_aov_name=None): + # type: (LayerMetadata, str) -> list expected_files = [] - for cam in layer_data["cameras"]: - file_prefix = layer_data["filePrefix"] + for cam in layer_data.cameras: + file_prefix = layer_data.filePrefix mappings = ( - (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), - (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), + (R_SUBSTITUTE_SCENE_TOKEN, layer_data.sceneName), + (R_SUBSTITUTE_LAYER_TOKEN, layer_data.layerName), (R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)), # this is required to remove unfilled aov token, for example # in Redshift @@ -268,29 +287,30 @@ class AExpectedFiles: file_prefix = re.sub(regex, value, file_prefix) for frame in range( - int(layer_data["frameStart"]), - int(layer_data["frameEnd"]) + 1, - int(layer_data["frameStep"]), + int(layer_data.frameStart), + int(layer_data.frameEnd) + 1, + int(layer_data.frameStep), ): expected_files.append( "{}.{}.{}".format( file_prefix, - str(frame).rjust(layer_data["padding"], "0"), - layer_data["defaultExt"], + str(frame).rjust(layer_data.padding, "0"), + layer_data.defaultExt, ) ) return expected_files def _generate_aov_file_sequences(self, layer_data): + # type: (LayerMetadata) -> list expected_files = [] aov_file_list = {} - for aov in layer_data["enabledAOVs"]: - for cam in layer_data["cameras"]: - file_prefix = layer_data["filePrefix"] + for aov in layer_data.enabledAOVs: + for cam in layer_data.cameras: + file_prefix = layer_data.filePrefix mappings = ( - (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), - (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), + (R_SUBSTITUTE_SCENE_TOKEN, layer_data.sceneName), + (R_SUBSTITUTE_LAYER_TOKEN, layer_data.layerName), (R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)), (R_SUBSTITUTE_AOV_TOKEN, aov[0]), @@ -303,14 +323,14 @@ class AExpectedFiles: aov_files = [] for frame in range( - int(layer_data["frameStart"]), - int(layer_data["frameEnd"]) + 1, - int(layer_data["frameStep"]), + int(layer_data.frameStart), + int(layer_data.frameEnd) + 1, + int(layer_data.frameStep), ): aov_files.append( "{}.{}.{}".format( file_prefix, - str(frame).rjust(layer_data["padding"], "0"), + str(frame).rjust(layer_data.padding, "0"), aov[1], ) ) @@ -318,12 +338,12 @@ class AExpectedFiles: # if we have more then one renderable camera, append # camera name to AOV to allow per camera AOVs. aov_name = aov[0] - if len(layer_data["cameras"]) > 1: + if len(layer_data.cameras) > 1: aov_name = "{}_{}".format(aov[0], self.sanitize_camera_name(cam)) aov_file_list[aov_name] = aov_files - file_prefix = layer_data["filePrefix"] + file_prefix = layer_data.filePrefix expected_files.append(aov_file_list) return expected_files @@ -340,14 +360,13 @@ class AExpectedFiles: layer_data = self._get_layer_data() expected_files = [] - if layer_data.get("enabledAOVs"): - expected_files = self._generate_aov_file_sequences(layer_data) + if layer_data.enabledAOVs: + return self._generate_aov_file_sequences(layer_data) else: - expected_files = self._generate_single_file_sequence(layer_data) - - return expected_files + return self._generate_single_file_sequence(layer_data) def get_renderable_cameras(self): + # type: () -> list """Get all renderable cameras. Returns: @@ -358,12 +377,11 @@ class AExpectedFiles: cmds.listRelatives(x, ap=True)[-1] for x in cmds.ls(cameras=True) ] - renderable_cameras = [] - for cam in cam_parents: - if self.maya_is_true(cmds.getAttr("{}.renderable".format(cam))): - renderable_cameras.append(cam) - - return renderable_cameras + return [ + cam + for cam in cam_parents + if self.maya_is_true(cmds.getAttr("{}.renderable".format(cam))) + ] @staticmethod def maya_is_true(attr_val): @@ -388,18 +406,17 @@ class AExpectedFiles: return bool(attr_val) @staticmethod - def get_layer_overrides(attr): - """Get overrides for attribute on given render layer. + def get_layer_overrides(attribute): + """Get overrides for attribute on current render layer. Args: - attr (str): Maya attribute name. - layer (str): Maya render layer name. + attribute (str): Maya attribute name. Returns: Value of attribute override. """ - connections = cmds.listConnections(attr, plugs=True) + connections = cmds.listConnections(attribute, plugs=True) if connections: for connection in connections: if connection: @@ -410,18 +427,18 @@ class AExpectedFiles: ) yield cmds.getAttr(attr_name) - def get_render_attribute(self, attr): + def get_render_attribute(self, attribute): """Get attribute from render options. Args: - attr (str): name of attribute to be looked up. + attribute (str): name of attribute to be looked up. Returns: Attribute value """ return lib.get_attr_in_layer( - "defaultRenderGlobals.{}".format(attr), layer=self.layer + "defaultRenderGlobals.{}".format(attribute), layer=self.layer ) @@ -543,13 +560,14 @@ class ExpectedFilesVray(AExpectedFiles): return prefix def _get_layer_data(self): + # type: () -> LayerMetadata """Override to get vray specific extension.""" layer_data = super(ExpectedFilesVray, self)._get_layer_data() default_ext = cmds.getAttr("vraySettings.imageFormatStr") if default_ext in ["exr (multichannel)", "exr (deep)"]: default_ext = "exr" - layer_data["defaultExt"] = default_ext - layer_data["padding"] = cmds.getAttr("vraySettings.fileNamePadding") + layer_data.defaultExt = default_ext + layer_data.padding = cmds.getAttr("vraySettings.fileNamePadding") return layer_data def get_files(self): @@ -565,7 +583,7 @@ class ExpectedFilesVray(AExpectedFiles): layer_data = self._get_layer_data() # remove 'beauty' from filenames as vray doesn't output it update = {} - if layer_data.get("enabledAOVs"): + if layer_data.enabledAOVs: for aov, seqs in expected_files[0].items(): if aov.startswith("beauty"): new_list = [] @@ -653,13 +671,14 @@ class ExpectedFilesVray(AExpectedFiles): vray_name = None vray_explicit_name = None vray_file_name = None - for attr in cmds.listAttr(node): - if attr.startswith("vray_filename"): - vray_file_name = cmds.getAttr("{}.{}".format(node, attr)) - elif attr.startswith("vray_name"): - vray_name = cmds.getAttr("{}.{}".format(node, attr)) - elif attr.startswith("vray_explicit_name"): - vray_explicit_name = cmds.getAttr("{}.{}".format(node, attr)) + for node_attr in cmds.listAttr(node): + if node_attr.startswith("vray_filename"): + vray_file_name = cmds.getAttr("{}.{}".format(node, node_attr)) + elif node_attr.startswith("vray_name"): + vray_name = cmds.getAttr("{}.{}".format(node, node_attr)) + elif node_attr.startswith("vray_explicit_name"): + vray_explicit_name = cmds.getAttr( + "{}.{}".format(node, node_attr)) if vray_file_name is not None and vray_file_name != "": final_name = vray_file_name @@ -725,7 +744,7 @@ class ExpectedFilesRedshift(AExpectedFiles): # Redshift doesn't merge Cryptomatte AOV to final exr. We need to check # for such condition and add it to list of expected files. - for aov in layer_data.get("enabledAOVs"): + for aov in layer_data.enabledAOVs: if aov[0].lower() == "cryptomatte": aov_name = aov[0] expected_files.append( diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 909993a173..b87e106865 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2124,7 +2124,7 @@ def bake_to_world_space(nodes, return world_space_nodes -def load_capture_preset(path=None, data=None): +def load_capture_preset(data=None): import capture preset = data @@ -2139,11 +2139,7 @@ def load_capture_preset(path=None, data=None): # GENERIC id = 'Generic' for key in preset[id]: - if key.startswith('isolate'): - pass - # options['isolate'] = preset[id][key] - else: - options[str(key)] = preset[id][key] + options[str(key)] = preset[id][key] # RESOLUTION id = 'Resolution' @@ -2156,6 +2152,10 @@ def load_capture_preset(path=None, data=None): for key in preset['Display Options']: if key.startswith('background'): disp_options[key] = preset['Display Options'][key] + disp_options[key][0] = (float(disp_options[key][0])/255) + disp_options[key][1] = (float(disp_options[key][1])/255) + disp_options[key][2] = (float(disp_options[key][2])/255) + disp_options[key].pop() else: disp_options['displayGradient'] = True @@ -2220,16 +2220,6 @@ def load_capture_preset(path=None, data=None): # use active sound track scene = capture.parse_active_scene() options['sound'] = scene['sound'] - cam_options = dict() - cam_options['overscan'] = 1.0 - cam_options['displayFieldChart'] = False - cam_options['displayFilmGate'] = False - cam_options['displayFilmOrigin'] = False - cam_options['displayFilmPivot'] = False - cam_options['displayGateMask'] = False - cam_options['displayResolution'] = False - cam_options['displaySafeAction'] = False - cam_options['displaySafeTitle'] = False # options['display_options'] = temp_options diff --git a/openpype/hosts/maya/plugins/load/load_ass.py b/openpype/hosts/maya/plugins/load/load_ass.py index b4bbd93f99..b7d44dd431 100644 --- a/openpype/hosts/maya/plugins/load/load_ass.py +++ b/openpype/hosts/maya/plugins/load/load_ass.py @@ -81,7 +81,10 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", - c[0], c[1], c[2]) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) self[:] = nodes diff --git a/openpype/hosts/maya/plugins/load/load_gpucache.py b/openpype/hosts/maya/plugins/load/load_gpucache.py index 5b1b29e184..d0a83b8177 100644 --- a/openpype/hosts/maya/plugins/load/load_gpucache.py +++ b/openpype/hosts/maya/plugins/load/load_gpucache.py @@ -38,7 +38,10 @@ class GpuCacheLoader(api.Loader): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - c[0], c[1], c[2]) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) # Create transform with shape transform_name = label + "_GPU" diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 37a2b145d4..96269f2771 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -85,7 +85,11 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): c = colors.get(family) if c is not None: groupNode.useOutlinerColor.set(1) - groupNode.outlinerColor.set(c[0], c[1], c[2]) + groupNode.outlinerColor.set( + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) self[:] = newNodes diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py b/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py index b705b55f4d..f5662ba462 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py @@ -62,7 +62,10 @@ class LoadVDBtoRedShift(api.Loader): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - c[0], c[1], c[2]) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) # Create VR volume_node = cmds.createNode("RedshiftVolumeShape", diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py index 82ccdb481b..80b453bd13 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py @@ -55,7 +55,10 @@ class LoadVDBtoVRay(api.Loader): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - c[0], c[1], c[2]) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) # Create VR grid_node = cmds.createNode("VRayVolumeGrid", diff --git a/openpype/hosts/maya/plugins/load/load_vrayproxy.py b/openpype/hosts/maya/plugins/load/load_vrayproxy.py index d5d4a941e3..e70f40bf5a 100644 --- a/openpype/hosts/maya/plugins/load/load_vrayproxy.py +++ b/openpype/hosts/maya/plugins/load/load_vrayproxy.py @@ -74,7 +74,10 @@ class VRayProxyLoader(api.Loader): if c is not None: cmds.setAttr("{0}.useOutlinerColor".format(group_node), 1) cmds.setAttr("{0}.outlinerColor".format(group_node), - c[0], c[1], c[2]) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) return containerise( name=name, diff --git a/openpype/hosts/maya/plugins/load/load_vrayscene.py b/openpype/hosts/maya/plugins/load/load_vrayscene.py index b0f0c2a54b..465dab2a76 100644 --- a/openpype/hosts/maya/plugins/load/load_vrayscene.py +++ b/openpype/hosts/maya/plugins/load/load_vrayscene.py @@ -53,7 +53,10 @@ class VRaySceneLoader(api.Loader): if c is not None: cmds.setAttr("{0}.useOutlinerColor".format(group_node), 1) cmds.setAttr("{0}.outlinerColor".format(group_node), - c[0], c[1], c[2]) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) return containerise( name=name, diff --git a/openpype/hosts/maya/plugins/load/load_yeti_cache.py b/openpype/hosts/maya/plugins/load/load_yeti_cache.py index 43c8aa16a0..de0ea6823c 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_cache.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_cache.py @@ -66,7 +66,10 @@ class YetiCacheLoader(api.Loader): if c is not None: cmds.setAttr(group_name + ".useOutlinerColor", 1) cmds.setAttr(group_name + ".outlinerColor", - c[0], c[1], c[2]) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) nodes.append(group_node) diff --git a/openpype/hosts/maya/plugins/load/load_yeti_rig.py b/openpype/hosts/maya/plugins/load/load_yeti_rig.py index a329be4cf5..3f67f98f51 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_rig.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_rig.py @@ -84,7 +84,10 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", - c[0], c[1], c[2]) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) self[:] = nodes return nodes diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 0dc91d67a9..fa1ce7f9a9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -49,9 +49,6 @@ class ExtractPlayblast(openpype.api.Extractor): preset['camera'] = camera - preset['format'] = "image" - preset['quality'] = 95 - preset['compression'] = "png" preset['start_frame'] = start preset['end_frame'] = end camera_option = preset.get("camera_option", {}) @@ -75,7 +72,7 @@ class ExtractPlayblast(openpype.api.Extractor): # Isolate view is requested by having objects in the set besides a # camera. - if instance.data.get("isolate"): + if preset.pop("isolate_view", False) or instance.data.get("isolate"): preset["isolate"] = instance.data["setMembers"] # Show/Hide image planes on request. @@ -93,9 +90,6 @@ class ExtractPlayblast(openpype.api.Extractor): # playblast and viewer preset['viewer'] = False - # Remove panel key since it's internal value to capture_gui - preset.pop("panel", None) - self.log.info('using viewport preset: {}'.format(preset)) path = capture.capture(**preset) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 016efa6499..5a91888781 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -12,10 +12,10 @@ import pymel.core as pm class ExtractThumbnail(openpype.api.Extractor): - """Extract a Camera as Alembic. + """Extract viewport thumbnail. - The cameras gets baked to world space by default. Only when the instance's - `bakeToWorldSpace` is set to False it will include its full hierarchy. + Takes review camera and creates a thumbnail based on viewport + capture. """ @@ -35,17 +35,14 @@ class ExtractThumbnail(openpype.api.Extractor): try: preset = lib.load_capture_preset(data=capture_preset) - except: + except KeyError as ke: + self.log.error('Error loading capture presets: {}'.format(str(ke))) preset = {} - self.log.info('using viewport preset: {}'.format(capture_preset)) + self.log.info('Using viewport preset: {}'.format(preset)) # preset["off_screen"] = False preset['camera'] = camera - preset['format'] = "image" - # preset['compression'] = "qt" - preset['quality'] = 50 - preset['compression'] = "jpg" preset['start_frame'] = instance.data["frameStart"] preset['end_frame'] = instance.data["frameStart"] preset['camera_options'] = { @@ -78,7 +75,7 @@ class ExtractThumbnail(openpype.api.Extractor): # Isolate view is requested by having objects in the set besides a # camera. - if instance.data.get("isolate"): + if preset.pop("isolate_view", False) or instance.data.get("isolate"): preset["isolate"] = instance.data["setMembers"] with maintained_time(): @@ -89,9 +86,6 @@ class ExtractThumbnail(openpype.api.Extractor): # playblast and viewer preset['viewer'] = False - # Remove panel key since it's internal value to capture_gui - preset.pop("panel", None) - path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 9aeaad7ff1..7c795db43d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -243,7 +243,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): "Cannot get value of {}.{}".format( node, attribute_name)) else: - if value != render_value: + if str(value) != str(render_value): invalid = True cls.log.error( ("Invalid value {} set on {}.{}. " diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index bd7a95f916..e6dab5cfc9 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -80,7 +80,7 @@ def install(): # Set context settings. nuke.addOnCreate(workfile_settings.set_context_settings, nodeClass="Root") nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root") - nuke.addOnCreate(lib.open_last_workfile, nodeClass="Root") + nuke.addOnCreate(lib.process_workfile_builder, nodeClass="Root") nuke.addOnCreate(lib.launch_workfiles_app, nodeClass="Root") menu.install() diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 63cac0fd8b..3c41574dbf 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -16,6 +16,7 @@ from avalon.nuke import ( from openpype.api import ( Logger, Anatomy, + BuildWorkfile, get_version_from_path, get_anatomy_settings, get_hierarchy, @@ -1641,23 +1642,69 @@ def launch_workfiles_app(): workfiles.show(os.environ["AVALON_WORKDIR"]) -def open_last_workfile(): - # get state from settings - open_last_version = get_current_project_settings()["nuke"].get( - "general", {}).get("create_initial_workfile") +def process_workfile_builder(): + from openpype.lib import ( + env_value_to_bool, + get_custom_workfile_template + ) + + # get state from settings + workfile_builder = get_current_project_settings()["nuke"].get( + "workfile_builder", {}) + + # get all imortant settings + openlv_on = env_value_to_bool( + env_key="AVALON_OPEN_LAST_WORKFILE", + default=None) + + # get settings + createfv_on = workfile_builder.get("create_first_version") or None + custom_templates = workfile_builder.get("custom_templates") or None + builder_on = workfile_builder.get("builder_on_start") or None - log.info("Opening last workfile...") last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE") - if not os.path.exists(last_workfile_path): - # return if none is defined - if not open_last_version: - return + # generate first version in file not existing and feature is enabled + if createfv_on and not os.path.exists(last_workfile_path): + # get custom template path if any + custom_template_path = get_custom_workfile_template( + custom_templates + ) + # if custom template is defined + if custom_template_path: + log.info("Adding nodes from `{}`...".format( + custom_template_path + )) + try: + # import nodes into current script + nuke.nodePaste(custom_template_path) + except RuntimeError: + raise RuntimeError(( + "Template defined for project: {} is not working. " + "Talk to your manager for an advise").format( + custom_template_path)) + + # if builder at start is defined + if builder_on: + log.info("Building nodes from presets...") + # build nodes by defined presets + BuildWorkfile().process() + + log.info("Saving script as version `{}`...".format( + last_workfile_path + )) + # safe file as version save_file(last_workfile_path) - else: - # to avoid looping of the callback, remove it! - nuke.removeOnCreate(open_last_workfile, nodeClass="Root") + return - # open workfile - open_file(last_workfile_path) + # skip opening of last version if it is not enabled + if not openlv_on or not os.path.exists(last_workfile_path): + return + + # to avoid looping of the callback, remove it! + nuke.removeOnCreate(process_workfile_builder, nodeClass="Root") + + log.info("Opening last workfile...") + # open workfile + open_file(last_workfile_path) diff --git a/openpype/hosts/nuke/plugins/load/load_mov.py b/openpype/hosts/nuke/plugins/load/load_mov.py index e2c9acaa9c..d84c3d4c71 100644 --- a/openpype/hosts/nuke/plugins/load/load_mov.py +++ b/openpype/hosts/nuke/plugins/load/load_mov.py @@ -41,7 +41,7 @@ class LoadMov(api.Loader): icon = "code-fork" color = "orange" - script_start = nuke.root()["first_frame"].value() + first_frame = nuke.root()["first_frame"].value() # options gui defaults = { @@ -71,6 +71,9 @@ class LoadMov(api.Loader): version_data = version.get("data", {}) repr_id = context["representation"]["_id"] + self.handle_start = version_data.get("handleStart", 0) + self.handle_end = version_data.get("handleEnd", 0) + orig_first = version_data.get("frameStart") orig_last = version_data.get("frameEnd") diff = orig_first - 1 @@ -78,9 +81,6 @@ class LoadMov(api.Loader): first = orig_first - diff last = orig_last - diff - handle_start = version_data.get("handleStart", 0) - handle_end = version_data.get("handleEnd", 0) - colorspace = version_data.get("colorspace") repr_cont = context["representation"]["context"] @@ -89,7 +89,7 @@ class LoadMov(api.Loader): context["representation"]["_id"] # create handles offset (only to last, because of mov) - last += handle_start + handle_end + last += self.handle_start + self.handle_end # Fallback to asset name when namespace is None if namespace is None: @@ -133,10 +133,11 @@ class LoadMov(api.Loader): if start_at_workfile: # start at workfile start - read_node['frame'].setValue(str(self.script_start)) + read_node['frame'].setValue(str(self.first_frame)) else: # start at version frame start - read_node['frame'].setValue(str(orig_first - handle_start)) + read_node['frame'].setValue( + str(orig_first - self.handle_start)) if colorspace: read_node["colorspace"].setValue(str(colorspace)) @@ -167,6 +168,11 @@ class LoadMov(api.Loader): read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) + if version_data.get("retime", None): + speed = version_data.get("speed", 1) + time_warp_nodes = version_data.get("timewarps", []) + self.make_retimes(speed, time_warp_nodes) + return containerise( read_node, name=name, @@ -229,9 +235,8 @@ class LoadMov(api.Loader): # set first to 1 first = orig_first - diff last = orig_last - diff - handles = version_data.get("handles", 0) - handle_start = version_data.get("handleStart", 0) - handle_end = version_data.get("handleEnd", 0) + self.handle_start = version_data.get("handleStart", 0) + self.handle_end = version_data.get("handleEnd", 0) colorspace = version_data.get("colorspace") if first is None: @@ -242,13 +247,8 @@ class LoadMov(api.Loader): read_node['name'].value(), representation)) first = 0 - # fix handle start and end if none are available - if not handle_start and not handle_end: - handle_start = handles - handle_end = handles - # create handles offset (only to last, because of mov) - last += handle_start + handle_end + last += self.handle_start + self.handle_end read_node["file"].setValue(file) @@ -259,12 +259,12 @@ class LoadMov(api.Loader): read_node["last"].setValue(last) read_node['frame_mode'].setValue("start at") - if int(self.script_start) == int(read_node['frame'].value()): + if int(self.first_frame) == int(read_node['frame'].value()): # start at workfile start - read_node['frame'].setValue(str(self.script_start)) + read_node['frame'].setValue(str(self.first_frame)) else: # start at version frame start - read_node['frame'].setValue(str(orig_first - handle_start)) + read_node['frame'].setValue(str(orig_first - self.handle_start)) if colorspace: read_node["colorspace"].setValue(str(colorspace)) @@ -282,8 +282,8 @@ class LoadMov(api.Loader): "version": str(version.get("name")), "colorspace": version_data.get("colorspace"), "source": version_data.get("source"), - "handleStart": str(handle_start), - "handleEnd": str(handle_end), + "handleStart": str(self.handle_start), + "handleEnd": str(self.handle_end), "fps": str(version_data.get("fps")), "author": version_data.get("author"), "outputDir": version_data.get("outputDir") @@ -295,6 +295,11 @@ class LoadMov(api.Loader): else: read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) + if version_data.get("retime", None): + speed = version_data.get("speed", 1) + time_warp_nodes = version_data.get("timewarps", []) + self.make_retimes(speed, time_warp_nodes) + # Update the imprinted representation update_container( read_node, updated_dict @@ -310,3 +315,32 @@ class LoadMov(api.Loader): with viewer_update_and_undo_stop(): nuke.delete(read_node) + + def make_retimes(self, speed, time_warp_nodes): + ''' Create all retime and timewarping nodes with coppied animation ''' + if speed != 1: + rtn = nuke.createNode( + "Retime", + "speed {}".format(speed)) + rtn["before"].setValue("continue") + rtn["after"].setValue("continue") + rtn["input.first_lock"].setValue(True) + rtn["input.first"].setValue( + self.first_frame + ) + + if time_warp_nodes != []: + start_anim = self.first_frame + (self.handle_start / speed) + for timewarp in time_warp_nodes: + twn = nuke.createNode(timewarp["Class"], + "name {}".format(timewarp["name"])) + if isinstance(timewarp["lookup"], list): + # if array for animation + twn["lookup"].setAnimated() + for i, value in enumerate(timewarp["lookup"]): + twn["lookup"].setValueAt( + (start_anim + i) + value, + (start_anim + i)) + else: + # if static value `int` + twn["lookup"].setValue(timewarp["lookup"]) diff --git a/openpype/hosts/nuke/plugins/load/load_sequence.py b/openpype/hosts/nuke/plugins/load/load_sequence.py index 9cbd1d4466..5f2128b10f 100644 --- a/openpype/hosts/nuke/plugins/load/load_sequence.py +++ b/openpype/hosts/nuke/plugins/load/load_sequence.py @@ -140,7 +140,7 @@ class LoadSequence(api.Loader): if version_data.get("retime", None): speed = version_data.get("speed", 1) time_warp_nodes = version_data.get("timewarps", []) - self.make_retimes(read_node, speed, time_warp_nodes) + self.make_retimes(speed, time_warp_nodes) return containerise(read_node, name=name, @@ -256,7 +256,7 @@ class LoadSequence(api.Loader): if version_data.get("retime", None): speed = version_data.get("speed", 1) time_warp_nodes = version_data.get("timewarps", []) - self.make_retimes(read_node, speed, time_warp_nodes) + self.make_retimes(speed, time_warp_nodes) # Update the imprinted representation update_container( @@ -285,10 +285,11 @@ class LoadSequence(api.Loader): rtn["after"].setValue("continue") rtn["input.first_lock"].setValue(True) rtn["input.first"].setValue( - self.handle_start + self.first_frame + self.first_frame ) if time_warp_nodes != []: + start_anim = self.first_frame + (self.handle_start / speed) for timewarp in time_warp_nodes: twn = nuke.createNode(timewarp["Class"], "name {}".format(timewarp["name"])) @@ -297,8 +298,8 @@ class LoadSequence(api.Loader): twn["lookup"].setAnimated() for i, value in enumerate(timewarp["lookup"]): twn["lookup"].setValueAt( - (self.first_frame + i) + value, - (self.first_frame + i)) + (start_anim + i) + value, + (start_anim + i)) else: # if static value `int` twn["lookup"].setValue(timewarp["lookup"]) diff --git a/openpype/hosts/tvpaint/hooks/pre_launch_args.py b/openpype/hosts/tvpaint/hooks/pre_launch_args.py index 04cca5d789..b0b13529ca 100644 --- a/openpype/hosts/tvpaint/hooks/pre_launch_args.py +++ b/openpype/hosts/tvpaint/hooks/pre_launch_args.py @@ -34,20 +34,6 @@ class TvpaintPrelaunchHook(PreLaunchHook): "run", self.launch_script_path(), executable_path ) - # Add workfile to launch arguments - workfile_path = self.workfile_path() - if workfile_path: - new_launch_args.append(workfile_path) - - # How to create new command line - # if platform.system().lower() == "windows": - # new_launch_args = [ - # "cmd.exe", - # "/c", - # "Call cmd.exe /k", - # *new_launch_args - # ] - # Append as whole list as these areguments should not be separated self.launch_context.launch_args.append(new_launch_args) @@ -64,38 +50,4 @@ class TvpaintPrelaunchHook(PreLaunchHook): "tvpaint", "launch_script.py" ) - return script_path - - def workfile_path(self): - workfile_path = self.data["last_workfile_path"] - - # copy workfile from template if doesnt exist any on path - if not os.path.exists(workfile_path): - # TODO add ability to set different template workfile path via - # settings - pype_dir = os.path.dirname(os.path.abspath(tvpaint.__file__)) - template_path = os.path.join( - pype_dir, "resources", "template.tvpp" - ) - - if not os.path.exists(template_path): - self.log.warning( - "Couldn't find workfile template file in {}".format( - template_path - ) - ) - return - - self.log.info( - f"Creating workfile from template: \"{template_path}\"" - ) - - # Copy template workfile to new destinantion - shutil.copy2( - os.path.normpath(template_path), - os.path.normpath(workfile_path) - ) - - self.log.info(f"Workfile to open: \"{workfile_path}\"") - - return workfile_path + return script_path \ No newline at end of file diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 61cf7eb780..9b11f9fe80 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -1,3 +1,4 @@ +import os import json import copy import pyblish.api @@ -109,7 +110,7 @@ class CollectInstances(pyblish.api.ContextPlugin): return { "family": "review", - "asset": context.data["workfile_context"]["asset"], + "asset": context.data["asset"], # Dummy subset name "subset": "reviewMain" } diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py new file mode 100644 index 0000000000..b059be90bf --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py @@ -0,0 +1,43 @@ +import os +import json +import pyblish.api +from avalon import io + + +class CollectWorkfile(pyblish.api.ContextPlugin): + label = "Collect Workfile" + order = pyblish.api.CollectorOrder - 1 + hosts = ["tvpaint"] + + def process(self, context): + current_file = context.data["currentFile"] + + self.log.info( + "Workfile path used for workfile family: {}".format(current_file) + ) + + dirpath, filename = os.path.split(current_file) + basename, ext = os.path.splitext(filename) + instance = context.create_instance(name=basename) + + task_name = io.Session["AVALON_TASK"] + subset_name = "workfile" + task_name.capitalize() + + # Create Workfile instance + instance.data.update({ + "subset": subset_name, + "asset": context.data["asset"], + "label": subset_name, + "publish": True, + "family": "workfile", + "families": ["workfile"], + "representations": [{ + "name": ext.lstrip("."), + "ext": ext.lstrip("."), + "files": filename, + "stagingDir": dirpath + }] + }) + self.log.info("Collected workfile instance: {}".format( + json.dumps(instance.data, indent=4) + )) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py b/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py new file mode 100644 index 0000000000..757da3294a --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py @@ -0,0 +1,49 @@ +import pyblish.api +from avalon.tvpaint import save_file + + +class ValidateWorkfileMetadataRepair(pyblish.api.Action): + """Store current context into workfile metadata.""" + + label = "Use current context" + icon = "wrench" + on = "failed" + + def process(self, context, _plugin): + """Save current workfile which should trigger storing of metadata.""" + current_file = context.data["currentFile"] + # Save file should trigger + save_file(current_file) + + +class ValidateWorkfileMetadata(pyblish.api.ContextPlugin): + """Validate if wokrfile contain required metadata for publising.""" + + label = "Validate Workfile Metadata" + order = pyblish.api.ValidatorOrder + + families = ["workfile"] + + actions = [ValidateWorkfileMetadataRepair] + + required_keys = {"project", "asset", "task"} + + def process(self, context): + workfile_context = context.data["workfile_context"] + if not workfile_context: + raise AssertionError( + "Current workfile is missing whole metadata about context." + ) + + missing_keys = [] + for key in self.required_keys: + value = workfile_context.get(key) + if not value: + missing_keys.append(key) + + if missing_keys: + raise AssertionError( + "Current workfile is missing metadata about {}.".format( + ", ".join(missing_keys) + ) + ) diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py new file mode 100644 index 0000000000..7d3913b883 --- /dev/null +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -0,0 +1,66 @@ +import unreal +from openpype.hosts.unreal.api.plugin import Creator +from avalon.unreal import pipeline + + +class CreateLook(Creator): + """Shader connections defining shape look""" + + name = "unrealLook" + label = "Unreal - Look" + family = "look" + icon = "paint-brush" + + root = "/Game/Avalon/Assets" + suffix = "_INS" + + def __init__(self, *args, **kwargs): + super(CreateLook, self).__init__(*args, **kwargs) + + def process(self): + name = self.data["subset"] + + selection = [] + if (self.options or {}).get("useSelection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + # Create the folder + path = f"{self.root}/{self.data['asset']}" + new_name = pipeline.create_folder(path, name) + full_path = f"{path}/{new_name}" + + # Create a new cube static mesh + ar = unreal.AssetRegistryHelpers.get_asset_registry() + cube = ar.get_asset_by_object_path("/Engine/BasicShapes/Cube.Cube") + + # Create the avalon publish instance object + container_name = f"{name}{self.suffix}" + pipeline.create_publish_instance( + instance=container_name, path=full_path) + + # Get the mesh of the selected object + original_mesh = ar.get_asset_by_object_path(selection[0]).get_asset() + materials = original_mesh.get_editor_property('materials') + + self.data["members"] = [] + + # Add the materials to the cube + for material in materials: + name = material.get_editor_property('material_slot_name') + object_path = f"{full_path}/{name}.{name}" + object = unreal.EditorAssetLibrary.duplicate_loaded_asset( + cube.get_asset(), object_path + ) + + # Remove the default material of the cube object + object.get_editor_property('static_materials').pop() + + object.add_material( + material.get_editor_property('material_interface')) + + self.data["members"].append(object_path) + + unreal.EditorAssetLibrary.save_asset(object_path) + + pipeline.imprint(f"{full_path}/{container_name}", self.data) diff --git a/openpype/hosts/unreal/plugins/publish/extract_look.py b/openpype/hosts/unreal/plugins/publish/extract_look.py new file mode 100644 index 0000000000..0f1539a7d5 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/extract_look.py @@ -0,0 +1,120 @@ +import json +import os + +import unreal +from unreal import MaterialEditingLibrary as mat_lib + +import openpype.api + + +class ExtractLook(openpype.api.Extractor): + """Extract look.""" + + label = "Extract Look" + hosts = ["unreal"] + families = ["look"] + optional = True + + def process(self, instance): + # Define extract output file path + stagingdir = self.staging_dir(instance) + resources_dir = instance.data["resourcesDir"] + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + transfers = [] + + json_data = [] + + for member in instance: + asset = ar.get_asset_by_object_path(member) + object = asset.get_asset() + + name = asset.get_editor_property('asset_name') + + json_element = {'material': str(name)} + + material_obj = object.get_editor_property('static_materials')[0] + material = material_obj.material_interface + + base_color = mat_lib.get_material_property_input_node( + material, unreal.MaterialProperty.MP_BASE_COLOR) + + base_color_name = base_color.get_editor_property('parameter_name') + + texture = mat_lib.get_material_default_texture_parameter_value( + material, base_color_name) + + if texture: + # Export Texture + tga_filename = f"{instance.name}_{name}_texture.tga" + + tga_exporter = unreal.TextureExporterTGA() + + tga_export_task = unreal.AssetExportTask() + + tga_export_task.set_editor_property('exporter', tga_exporter) + tga_export_task.set_editor_property('automated', True) + tga_export_task.set_editor_property('object', texture) + tga_export_task.set_editor_property( + 'filename', f"{stagingdir}/{tga_filename}") + tga_export_task.set_editor_property('prompt', False) + tga_export_task.set_editor_property('selected', False) + + unreal.Exporter.run_asset_export_task(tga_export_task) + + json_element['tga_filename'] = tga_filename + + transfers.append(( + f"{stagingdir}/{tga_filename}", + f"{resources_dir}/{tga_filename}")) + + fbx_filename = f"{instance.name}_{name}.fbx" + + fbx_exporter = unreal.StaticMeshExporterFBX() + fbx_exporter.set_editor_property('text', False) + + options = unreal.FbxExportOption() + options.set_editor_property('ascii', False) + options.set_editor_property('collision', False) + + task = unreal.AssetExportTask() + task.set_editor_property('exporter', fbx_exporter) + task.set_editor_property('options', options) + task.set_editor_property('automated', True) + task.set_editor_property('object', object) + task.set_editor_property( + 'filename', f"{stagingdir}/{fbx_filename}") + task.set_editor_property('prompt', False) + task.set_editor_property('selected', False) + + unreal.Exporter.run_asset_export_task(task) + + json_element['fbx_filename'] = fbx_filename + + transfers.append(( + f"{stagingdir}/{fbx_filename}", + f"{resources_dir}/{fbx_filename}")) + + json_data.append(json_element) + + json_filename = f"{instance.name}.json" + json_path = os.path.join(stagingdir, json_filename) + + with open(json_path, "w+") as file: + json.dump(json_data, fp=file, indent=2) + + if "transfers" not in instance.data: + instance.data["transfers"] = [] + if "representations" not in instance.data: + instance.data["representations"] = [] + + json_representation = { + 'name': 'json', + 'ext': 'json', + 'files': json_filename, + "stagingDir": stagingdir, + } + + instance.data["representations"].append(json_representation) + instance.data["transfers"].extend(transfers) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index c97545fdf4..12c04a4236 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -81,7 +81,13 @@ from .avalon_context import ( get_creator_by_name, - change_timer_to_current_context + get_custom_workfile_template, + + change_timer_to_current_context, + + get_custom_workfile_template_by_context, + get_custom_workfile_template_by_string_context, + get_custom_workfile_template ) from .local_settings import ( @@ -91,7 +97,8 @@ from .local_settings import ( OpenPypeSettingsRegistry, get_local_site_id, change_openpype_mongo_url, - get_openpype_username + get_openpype_username, + is_admin_password_required ) from .applications import ( @@ -192,6 +199,10 @@ __all__ = [ "change_timer_to_current_context", + "get_custom_workfile_template_by_context", + "get_custom_workfile_template_by_string_context", + "get_custom_workfile_template", + "IniSettingRegistry", "JSONSettingRegistry", "OpenPypeSecureRegistry", @@ -199,6 +210,7 @@ __all__ = [ "get_local_site_id", "change_openpype_mongo_url", "get_openpype_username", + "is_admin_password_required", "ApplicationLaunchFailed", "ApplictionExecutableNotFound", diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 2a7c58c4ee..c4217cc6d5 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -3,6 +3,7 @@ import os import json import re import copy +import platform import logging import collections import functools @@ -755,18 +756,22 @@ class BuildWorkfile: """ host_name = avalon.api.registered_host().__name__.rsplit(".", 1)[-1] presets = get_project_settings(avalon.io.Session["AVALON_PROJECT"]) + # Get presets for host - build_presets = ( - presets.get(host_name, {}) - .get("workfile_build") - .get("profiles") - ) - if not build_presets: + wb_settings = presets.get(host_name, {}).get("workfile_builder") + + if not wb_settings: + # backward compatibility + wb_settings = presets.get(host_name, {}).get("workfile_build") + + builder_presets = wb_settings.get("profiles") + + if not builder_presets: return task_name_low = task_name.lower() per_task_preset = None - for preset in build_presets: + for preset in builder_presets: preset_tasks = preset.get("tasks") or [] preset_tasks_low = [task.lower() for task in preset_tasks] if task_name_low in preset_tasks_low: @@ -1266,3 +1271,201 @@ def change_timer_to_current_context(): } requests.post(rest_api_url, json=data) + + +def _get_task_context_data_for_anatomy( + project_doc, asset_doc, task_name, anatomy=None +): + """Prepare Task context for anatomy data. + + WARNING: this data structure is currently used only in workfile templates. + Key "task" is currently in rest of pipeline used as string with task + name. + + Args: + project_doc (dict): Project document with available "name" and + "data.code" keys. + asset_doc (dict): Asset document from MongoDB. + task_name (str): Name of context task. + anatomy (Anatomy): Optionally Anatomy for passed project name can be + passed as Anatomy creation may be slow. + + Returns: + dict: With Anatomy context data. + """ + + if anatomy is None: + anatomy = Anatomy(project_doc["name"]) + + asset_name = asset_doc["name"] + project_task_types = anatomy["tasks"] + + # get relevant task type from asset doc + assert task_name in asset_doc["data"]["tasks"], ( + "Task name \"{}\" not found on asset \"{}\"".format( + task_name, asset_name + ) + ) + + task_type = asset_doc["data"]["tasks"][task_name].get("type") + + assert task_type, ( + "Task name \"{}\" on asset \"{}\" does not have specified task type." + ).format(asset_name, task_name) + + # get short name for task type defined in default anatomy settings + project_task_type_data = project_task_types.get(task_type) + assert project_task_type_data, ( + "Something went wrong. Default anatomy tasks are not holding" + "requested task type: `{}`".format(task_type) + ) + + return { + "project": { + "name": project_doc["name"], + "code": project_doc["data"].get("code") + }, + "asset": asset_name, + "task": { + "name": task_name, + "type": task_type, + "short_name": project_task_type_data["short_name"] + } + } + + +def get_custom_workfile_template_by_context( + template_profiles, project_doc, asset_doc, task_name, anatomy=None +): + """Filter and fill workfile template profiles by passed context. + + It is expected that passed argument are already queried documents of + project and asset as parents of processing task name. + + Existence of formatted path is not validated. + + Args: + template_profiles(list): Template profiles from settings. + project_doc(dict): Project document from MongoDB. + asset_doc(dict): Asset document from MongoDB. + task_name(str): Name of task for which templates are filtered. + anatomy(Anatomy): Optionally passed anatomy object for passed project + name. + + Returns: + str: Path to template or None if none of profiles match current + context. (Existence of formatted path is not validated.) + """ + + from openpype.lib import filter_profiles + + if anatomy is None: + anatomy = Anatomy(project_doc["name"]) + + # get project, asset, task anatomy context data + anatomy_context_data = _get_task_context_data_for_anatomy( + project_doc, asset_doc, task_name, anatomy + ) + # add root dict + anatomy_context_data["root"] = anatomy.roots + + # get task type for the task in context + current_task_type = anatomy_context_data["task"]["type"] + + # get path from matching profile + matching_item = filter_profiles( + template_profiles, + {"task_type": current_task_type} + ) + # when path is available try to format it in case + # there are some anatomy template strings + if matching_item: + template = matching_item["path"][platform.system().lower()] + return template.format(**anatomy_context_data) + + return None + + +def get_custom_workfile_template_by_string_context( + template_profiles, project_name, asset_name, task_name, + dbcon=None, anatomy=None +): + """Filter and fill workfile template profiles by passed context. + + Passed context are string representations of project, asset and task. + Function will query documents of project and asset to be able use + `get_custom_workfile_template_by_context` for rest of logic. + + Args: + template_profiles(list): Loaded workfile template profiles. + project_name(str): Project name. + asset_name(str): Asset name. + task_name(str): Task name. + dbcon(AvalonMongoDB): Optional avalon implementation of mongo + connection with context Session. + anatomy(Anatomy): Optionally prepared anatomy object for passed + project. + + Returns: + str: Path to template or None if none of profiles match current + context. (Existence of formatted path is not validated.) + """ + + if dbcon is None: + from avalon.api import AvalonMongoDB + + dbcon = AvalonMongoDB() + + dbcon.install() + + if dbcon.Session["AVALON_PROJECT"] != project_name: + dbcon.Session["AVALON_PROJECT"] = project_name + + project_doc = dbcon.find_one( + {"type": "project"}, + # All we need is "name" and "data.code" keys + { + "name": 1, + "data.code": 1 + } + ) + asset_doc = dbcon.find_one( + { + "type": "asset", + "name": asset_name + }, + # All we need is "name" and "data.tasks" keys + { + "name": 1, + "data.tasks": 1 + } + ) + + return get_custom_workfile_template_by_context( + template_profiles, project_doc, asset_doc, task_name, anatomy + ) + + +def get_custom_workfile_template(template_profiles): + """Filter and fill workfile template profiles by current context. + + Current context is defined by `avalon.api.Session`. That's why this + function should be used only inside host where context is set and stable. + + Args: + template_profiles(list): Template profiles from settings. + + Returns: + str: Path to template or None if none of profiles match current + context. (Existence of formatted path is not validated.) + """ + # Use `avalon.io` as Mongo connection + from avalon import io + + return get_custom_workfile_template_by_string_context( + template_profiles, + io.Session["AVALON_PROJECT"], + io.Session["AVALON_ASSET"], + io.Session["AVALON_TASK"], + io + ) diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index b7f8e0e252..943cd9fcaf 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -43,7 +43,7 @@ def sizeof_fmt(num, suffix='B'): return "%.1f%s%s" % (num, 'Yi', suffix) -def path_from_represenation(representation, anatomy): +def path_from_representation(representation, anatomy): from avalon import pipeline # safer importing try: @@ -126,18 +126,22 @@ def check_destination_path(repre_id, anatomy_filled = anatomy.format_all(anatomy_data) dest_path = anatomy_filled["delivery"][template_name] report_items = collections.defaultdict(list) - sub_msg = None + if not dest_path.solved: msg = ( "Missing keys in Representation's context" " for anatomy template \"{}\"." ).format(template_name) + sub_msg = ( + "Representation: {}
" + ).format(repre_id) + if dest_path.missing_keys: keys = ", ".join(dest_path.missing_keys) - sub_msg = ( - "Representation: {}
- Missing keys: \"{}\"
" - ).format(repre_id, keys) + sub_msg += ( + "- Missing keys: \"{}\"
" + ).format(keys) if dest_path.invalid_types: items = [] @@ -145,10 +149,9 @@ def check_destination_path(repre_id, items.append("\"{}\" {}".format(key, str(value))) keys = ", ".join(items) - sub_msg = ( - "Representation: {}
" + sub_msg += ( "- Invalid value DataType: \"{}\"
" - ).format(repre_id, keys) + ).format(keys) report_items[msg].append(sub_msg) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index bf9a0cb506..158488dd56 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -4,8 +4,10 @@ import clique from .import_utils import discover_host_vendor_module try: + import opentimelineio as otio from opentimelineio import opentime as _ot except ImportError: + otio = discover_host_vendor_module("opentimelineio") _ot = discover_host_vendor_module("opentimelineio.opentime") @@ -166,3 +168,119 @@ def make_sequence_collection(path, otio_range, metadata): head=head, tail=tail, padding=metadata["padding"]) collection.indexes.update([i for i in range(first, (last + 1))]) return dir_path, collection + + +def _sequence_resize(source, length): + step = float(len(source) - 1) / (length - 1) + for i in range(length): + low, ratio = divmod(i * step, 1) + high = low + 1 if ratio > 0 else low + yield (1 - ratio) * source[int(low)] + ratio * source[int(high)] + + +def get_media_range_with_retimes(otio_clip, handle_start, handle_end): + source_range = otio_clip.source_range + available_range = otio_clip.available_range() + media_in = available_range.start_time.value + media_out = available_range.end_time_inclusive().value + + # modifiers + time_scalar = 1. + offset_in = 0 + offset_out = 0 + time_warp_nodes = [] + + # Check for speed effects and adjust playback speed accordingly + for effect in otio_clip.effects: + if isinstance(effect, otio.schema.LinearTimeWarp): + time_scalar = effect.time_scalar + + elif isinstance(effect, otio.schema.FreezeFrame): + # For freeze frame, playback speed must be set after range + time_scalar = 0. + + elif isinstance(effect, otio.schema.TimeEffect): + # For freeze frame, playback speed must be set after range + name = effect.name + effect_name = effect.effect_name + if "TimeWarp" not in effect_name: + continue + metadata = effect.metadata + lookup = metadata.get("lookup") + if not lookup: + continue + + # time warp node + tw_node = { + "Class": "TimeWarp", + "name": name + } + tw_node.update(metadata) + + # get first and last frame offsets + offset_in += lookup[0] + offset_out += lookup[-1] + + # add to timewarp nodes + time_warp_nodes.append(tw_node) + + # multiply by time scalar + offset_in *= time_scalar + offset_out *= time_scalar + + # filip offset if reversed speed + if time_scalar < 0: + _offset_in = offset_out + _offset_out = offset_in + offset_in = _offset_in + offset_out = _offset_out + + # scale handles + handle_start *= abs(time_scalar) + handle_end *= abs(time_scalar) + + # filip handles if reversed speed + if time_scalar < 0: + _handle_start = handle_end + _handle_end = handle_start + handle_start = _handle_start + handle_end = _handle_end + + source_in = source_range.start_time.value + + media_in_trimmed = ( + media_in + source_in + offset_in) + media_out_trimmed = ( + media_in + source_in + ( + ((source_range.duration.value - 1) * abs( + time_scalar)) + offset_out)) + + # calculate available hanles + if (media_in_trimmed - media_in) < handle_start: + handle_start = (media_in_trimmed - media_in) + if (media_out - media_out_trimmed) < handle_end: + handle_end = (media_out - media_out_trimmed) + + # create version data + version_data = { + "versionData": { + "retime": True, + "speed": time_scalar, + "timewarps": time_warp_nodes, + "handleStart": handle_start, + "handleEnd": handle_end + } + } + + returning_dict = { + "mediaIn": media_in_trimmed, + "mediaOut": media_out_trimmed, + "handleStart": handle_start, + "handleEnd": handle_end + } + + # add version data only if retime + if time_warp_nodes or time_scalar != 1.: + returning_dict.update(version_data) + + return returning_dict diff --git a/openpype/lib/local_settings.py b/openpype/lib/local_settings.py index 67845c77cf..66dad279de 100644 --- a/openpype/lib/local_settings.py +++ b/openpype/lib/local_settings.py @@ -29,7 +29,10 @@ except ImportError: import six import appdirs -from openpype.settings import get_local_settings +from openpype.settings import ( + get_local_settings, + get_system_settings +) from .import validate_mongo_connection @@ -562,3 +565,16 @@ def get_openpype_username(): if not username: username = getpass.getuser() return username + + +def is_admin_password_required(): + system_settings = get_system_settings() + password = system_settings["general"].get("admin_password") + if not password: + return False + + local_settings = get_local_settings() + is_admin = local_settings.get("general", {}).get("is_admin", False) + if is_admin: + return False + return True diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index bae48c540b..debeeed6bf 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -36,6 +36,7 @@ from .clockify import ClockifyModule from .log_viewer import LogViewModule from .muster import MusterModule from .deadline import DeadlineModule +from .project_manager_action import ProjectManagerAction from .standalonepublish_action import StandAlonePublishAction from .sync_server import SyncServerModule @@ -73,6 +74,7 @@ __all__ = ( "LogViewModule", "MusterModule", "DeadlineModule", + "ProjectManagerAction", "StandAlonePublishAction", "SyncServerModule" diff --git a/openpype/modules/avalon_apps/avalon_app.py b/openpype/modules/avalon_apps/avalon_app.py index 243c4f928a..4e95f6e72b 100644 --- a/openpype/modules/avalon_apps/avalon_app.py +++ b/openpype/modules/avalon_apps/avalon_app.py @@ -86,7 +86,7 @@ class AvalonModule(PypeModule, ITrayModule, IWebServerRoutes): from Qt import QtWidgets # Actions action_library_loader = QtWidgets.QAction( - "Library loader", tray_menu + "Loader", tray_menu ) action_library_loader.triggered.connect(self.show_library_loader) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index b8d76aa028..c7efbd5ab3 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -139,6 +139,25 @@ class ITrayModule: """ pass + def execute_in_main_thread(self, callback): + """ Pushes callback to the queue or process 'callback' on a main thread + + Some callbacks need to be processed on main thread (menu actions + must be added on main thread or they won't get triggered etc.) + """ + # called without initialized tray, still main thread needed + if not self.tray_initialized: + try: + callback = self._main_thread_callbacks.popleft() + callback() + except: + self.log.warning( + "Failed to execute {} in main thread".format(callback), + exc_info=True) + + return + self.manager.tray_manager.execute_in_main_thread(callback) + def show_tray_message(self, title, message, icon=None, msecs=None): """Show tray message. @@ -153,6 +172,10 @@ class ITrayModule: if self._tray_manager: self._tray_manager.show_tray_message(title, message, icon, msecs) + def add_doubleclick_callback(self, callback): + if hasattr(self.manager, "add_doubleclick_callback"): + self.manager.add_doubleclick_callback(self, callback) + class ITrayAction(ITrayModule): """Implementation of Tray action. @@ -165,6 +188,9 @@ class ITrayAction(ITrayModule): necessary. """ + admin_action = False + _admin_submenu = None + @property @abstractmethod def label(self): @@ -178,9 +204,19 @@ class ITrayAction(ITrayModule): def tray_menu(self, tray_menu): from Qt import QtWidgets - action = QtWidgets.QAction(self.label, tray_menu) + + if self.admin_action: + menu = self.admin_submenu(tray_menu) + action = QtWidgets.QAction(self.label, menu) + menu.addAction(action) + if not menu.menuAction().isVisible(): + menu.menuAction().setVisible(True) + + else: + action = QtWidgets.QAction(self.label, tray_menu) + tray_menu.addAction(action) + action.triggered.connect(self.on_action_trigger) - tray_menu.addAction(action) def tray_start(self): return @@ -188,6 +224,16 @@ class ITrayAction(ITrayModule): def tray_exit(self): return + @staticmethod + def admin_submenu(tray_menu): + if ITrayAction._admin_submenu is None: + from Qt import QtWidgets + + admin_submenu = QtWidgets.QMenu("Admin", tray_menu) + admin_submenu.menuAction().setVisible(False) + ITrayAction._admin_submenu = admin_submenu + return ITrayAction._admin_submenu + class ITrayService(ITrayModule): # Module's property @@ -214,6 +260,7 @@ class ITrayService(ITrayModule): def services_submenu(tray_menu): if ITrayService._services_submenu is None: from Qt import QtWidgets + services_submenu = QtWidgets.QMenu("Services", tray_menu) services_submenu.menuAction().setVisible(False) ITrayService._services_submenu = services_submenu @@ -658,7 +705,7 @@ class TrayModulesManager(ModulesManager): ) def __init__(self): - self.log = PypeLogger().get_logger(self.__class__.__name__) + self.log = PypeLogger.get_logger(self.__class__.__name__) self.modules = [] self.modules_by_id = {} @@ -666,6 +713,28 @@ class TrayModulesManager(ModulesManager): self._report = {} self.tray_manager = None + self.doubleclick_callbacks = {} + self.doubleclick_callback = None + + def add_doubleclick_callback(self, module, callback): + """Register doubleclick callbacks on tray icon. + + Currently there is no way how to determine which is launched. Name of + callback can be defined with `doubleclick_callback` attribute. + + Missing feature how to define default callback. + """ + callback_name = "_".join([module.name, callback.__name__]) + if callback_name not in self.doubleclick_callbacks: + self.doubleclick_callbacks[callback_name] = callback + if self.doubleclick_callback is None: + self.doubleclick_callback = callback_name + return + + self.log.warning(( + "Callback with name \"{}\" is already registered." + ).format(callback_name)) + def initialize(self, tray_manager, tray_menu): self.tray_manager = tray_manager self.initialize_modules() @@ -680,6 +749,10 @@ class TrayModulesManager(ModulesManager): output.append(module) return output + def restart_tray(self): + if self.tray_manager: + self.tray_manager.restart() + def tray_init(self): report = {} time_start = time.time() diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index ea953441a2..52ed9bba76 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -105,7 +105,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): families = ["render.farm", "prerender.farm", "renderlayer", "imagesequence", "vrayscene"] - aov_filter = {"maya": [r".+(?:\.|_)([Bb]eauty)(?:\.|_).*"], + aov_filter = {"maya": [r".*(?:\.|_)*([Bb]eauty)(?:\.|_)*.*"], "aftereffects": [r".*"], # for everything from AE "harmony": [r".*"], # for everything from AE "celaction": [r".*"]} @@ -435,9 +435,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): preview = False if app in self.aov_filter.keys(): for aov_pattern in self.aov_filter[app]: - if re.match(aov_pattern, - aov - ): + if re.match(aov_pattern, aov): preview = True break diff --git a/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py index bc6a58624a..214f1ecf18 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py @@ -41,12 +41,9 @@ class PushHierValuesToNonHier(ServerAction): label = "OpenPype Admin" variant = "- Push Hierarchical values To Non-Hierarchical" - hierarchy_entities_query = ( - "select id, parent_id from TypedContext where project_id is \"{}\"" - ) - entities_query = ( - "select id, name, parent_id, link from TypedContext" - " where project_id is \"{}\" and object_type_id in ({})" + entities_query_by_project = ( + "select id, parent_id, object_type_id from TypedContext" + " where project_id is \"{}\"" ) cust_attrs_query = ( "select id, key, object_type_id, is_hierarchical, default" @@ -187,18 +184,18 @@ class PushHierValuesToNonHier(ServerAction): "message": "Nothing has changed." } - entities = session.query(self.entities_query.format( - project_entity["id"], - self.join_query_keys(destination_object_type_ids) - )).all() + ( + parent_id_by_entity_id, + filtered_entities + ) = self.all_hierarchy_entities( + session, + selected_ids, + project_entity, + destination_object_type_ids + ) self.log.debug("Preparing whole project hierarchy by ids.") - parent_id_by_entity_id = self.all_hierarchy_ids( - session, project_entity - ) - filtered_entities = self.filter_entities_by_selection( - entities, selected_ids, parent_id_by_entity_id - ) + entities_by_obj_id = { obj_id: [] for obj_id in destination_object_type_ids @@ -252,39 +249,77 @@ class PushHierValuesToNonHier(ServerAction): return True - def all_hierarchy_ids(self, session, project_entity): - parent_id_by_entity_id = {} - - hierarchy_entities = session.query( - self.hierarchy_entities_query.format(project_entity["id"]) - ) - for hierarchy_entity in hierarchy_entities: - entity_id = hierarchy_entity["id"] - parent_id = hierarchy_entity["parent_id"] - parent_id_by_entity_id[entity_id] = parent_id - return parent_id_by_entity_id - - def filter_entities_by_selection( - self, entities, selected_ids, parent_id_by_entity_id + def all_hierarchy_entities( + self, + session, + selected_ids, + project_entity, + destination_object_type_ids ): + selected_ids = set(selected_ids) + filtered_entities = [] - for entity in entities: - entity_id = entity["id"] - if entity_id in selected_ids: - filtered_entities.append(entity) - continue + parent_id_by_entity_id = {} + # Query is simple if project is in selection + if project_entity["id"] in selected_ids: + entities = session.query( + self.entities_query_by_project.format(project_entity["id"]) + ).all() - parent_id = entity["parent_id"] - while True: - if parent_id in selected_ids: + for entity in entities: + if entity["object_type_id"] in destination_object_type_ids: filtered_entities.append(entity) - break + entity_id = entity["id"] + parent_id_by_entity_id[entity_id] = entity["parent_id"] + return parent_id_by_entity_id, filtered_entities - parent_id = parent_id_by_entity_id.get(parent_id) - if parent_id is None: - break + # Query selection and get it's link to be able calculate parentings + entities_with_link = session.query(( + "select id, parent_id, link, object_type_id" + " from TypedContext where id in ({})" + ).format(self.join_query_keys(selected_ids))).all() - return filtered_entities + # Process and store queried entities and store all lower entities to + # `bottom_ids` + # - bottom_ids should not contain 2 ids where one is parent of second + bottom_ids = set(selected_ids) + for entity in entities_with_link: + if entity["object_type_id"] in destination_object_type_ids: + filtered_entities.append(entity) + children_id = None + for idx, item in enumerate(reversed(entity["link"])): + item_id = item["id"] + if idx > 0 and item_id in bottom_ids: + bottom_ids.remove(item_id) + + if children_id is not None: + parent_id_by_entity_id[children_id] = item_id + + children_id = item_id + + # Query all children of selection per one hierarchy level and process + # their data the same way as selection but parents are already known + chunk_size = 100 + while bottom_ids: + child_entities = [] + # Query entities in chunks + entity_ids = list(bottom_ids) + for idx in range(0, len(entity_ids), chunk_size): + _entity_ids = entity_ids[idx:idx + chunk_size] + child_entities.extend(session.query(( + "select id, parent_id, object_type_id from" + " TypedContext where parent_id in ({})" + ).format(self.join_query_keys(_entity_ids))).all()) + + bottom_ids = set() + for entity in child_entities: + entity_id = entity["id"] + parent_id_by_entity_id[entity_id] = entity["parent_id"] + bottom_ids.add(entity_id) + if entity["object_type_id"] in destination_object_type_ids: + filtered_entities.append(entity) + + return parent_id_by_entity_id, filtered_entities def get_hier_values( self, @@ -387,10 +422,10 @@ class PushHierValuesToNonHier(ServerAction): for key, value in parent_values.items(): hier_values_by_entity_id[task_id][key] = value configuration_id = hier_attr_id_by_key[key] - _entity_key = collections.OrderedDict({ - "configuration_id": configuration_id, - "entity_id": task_id - }) + _entity_key = collections.OrderedDict([ + ("configuration_id", configuration_id), + ("entity_id", task_id) + ]) session.recorded_operations.push( ftrack_api.operation.UpdateEntityOperation( @@ -401,6 +436,9 @@ class PushHierValuesToNonHier(ServerAction): value ) ) + if len(session.recorded_operations) > 100: + session.commit() + session.commit() def push_values_to_entities( @@ -425,10 +463,10 @@ class PushHierValuesToNonHier(ServerAction): if value is None: continue - _entity_key = collections.OrderedDict({ - "configuration_id": attr["id"], - "entity_id": entity_id - }) + _entity_key = collections.OrderedDict([ + ("configuration_id", attr["id"]), + ("entity_id", entity_id) + ]) session.recorded_operations.push( ftrack_api.operation.UpdateEntityOperation( @@ -439,6 +477,9 @@ class PushHierValuesToNonHier(ServerAction): value ) ) + if len(session.recorded_operations) > 100: + session.commit() + session.commit() diff --git a/openpype/modules/ftrack/event_handlers_user/action_delivery.py b/openpype/modules/ftrack/event_handlers_user/action_delivery.py index f8553b2eac..2e7599647a 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delivery.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delivery.py @@ -9,7 +9,7 @@ from openpype.api import Anatomy, config from openpype.modules.ftrack.lib import BaseAction, statics_icon from openpype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY from openpype.lib.delivery import ( - path_from_represenation, + path_from_representation, get_format_dict, check_destination_path, process_single_file, @@ -74,7 +74,7 @@ class Delivery(BaseAction): "value": project_name }) - # Prpeare anatomy data + # Prepare anatomy data anatomy = Anatomy(project_name) new_anatomies = [] first = None @@ -368,12 +368,18 @@ class Delivery(BaseAction): def launch(self, session, entities, event): if "values" not in event["data"]: - return + return { + "success": True, + "message": "Nothing to do" + } values = event["data"]["values"] skipped = values.pop("__skipped__") if skipped: - return None + return { + "success": False, + "message": "Action skipped" + } user_id = event["source"]["user"]["id"] user_entity = session.query( @@ -391,27 +397,45 @@ class Delivery(BaseAction): try: self.db_con.install() - self.real_launch(session, entities, event) - job["status"] = "done" + report = self.real_launch(session, entities, event) - except Exception: + except Exception as exc: + report = { + "success": False, + "title": "Delivery failed", + "items": [{ + "type": "label", + "value": ( + "Error during delivery action process:
{}" + "

Check logs for more information." + ).format(str(exc)) + }] + } self.log.warning( "Failed during processing delivery action.", exc_info=True ) finally: - if job["status"] != "done": + if report["success"]: + job["status"] = "done" + else: job["status"] = "failed" session.commit() self.db_con.uninstall() - if job["status"] == "failed": + if not report["success"]: + self.show_interface( + items=report["items"], + title=report["title"], + event=event + ) return { "success": False, - "message": "Delivery failed. Check logs for more information." + "message": "Errors during delivery process. See report." } - return True + + return report def real_launch(self, session, entities, event): self.log.info("Delivery action just started.") @@ -431,7 +455,7 @@ class Delivery(BaseAction): if not repre_names: return { "success": True, - "message": "Not selected components to deliver." + "message": "No selected components to deliver." } location_path = location_path.strip() @@ -479,7 +503,7 @@ class Delivery(BaseAction): if frame: repre["context"]["frame"] = len(str(frame)) * "#" - repre_path = path_from_represenation(repre, anatomy) + repre_path = path_from_representation(repre, anatomy) # TODO add backup solution where root of path from component # is replaced with root args = ( @@ -502,7 +526,7 @@ class Delivery(BaseAction): def report(self, report_items): """Returns dict with final status of delivery (succes, fail etc.).""" items = [] - title = "Delivery report" + for msg, _items in report_items.items(): if not _items: continue @@ -533,9 +557,8 @@ class Delivery(BaseAction): return { "items": items, - "title": title, - "success": False, - "message": "Delivery Finished" + "title": "Delivery report", + "success": False } diff --git a/openpype/modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/ftrack/ftrack_server/event_server_cli.py index b5cc1bef3e..8bba22b475 100644 --- a/openpype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/ftrack/ftrack_server/event_server_cli.py @@ -422,17 +422,18 @@ def run_event_server( ftrack_url, ftrack_user, ftrack_api_key, - ftrack_events_path, - no_stored_credentials, - store_credentials, legacy, clockify_api_key, clockify_workspace ): - if not no_stored_credentials: + if not ftrack_user or not ftrack_api_key: + print(( + "Ftrack user/api key were not passed." + " Trying to use credentials from user keyring." + )) cred = credentials.get_credentials(ftrack_url) - username = cred.get('username') - api_key = cred.get('api_key') + ftrack_user = cred.get("username") + ftrack_api_key = cred.get("api_key") if clockify_workspace and clockify_api_key: os.environ["CLOCKIFY_WORKSPACE"] = clockify_workspace @@ -445,209 +446,16 @@ def run_event_server( return 1 # Validate entered credentials - if not validate_credentials(ftrack_url, username, api_key): + if not validate_credentials(ftrack_url, ftrack_user, ftrack_api_key): print('Exiting! < Please enter valid credentials >') return 1 - if store_credentials: - credentials.save_credentials(username, api_key, ftrack_url) - # Set Ftrack environments os.environ["FTRACK_SERVER"] = ftrack_url - os.environ["FTRACK_API_USER"] = username - os.environ["FTRACK_API_KEY"] = api_key - # TODO This won't work probably - if ftrack_events_path: - if isinstance(ftrack_events_path, (list, tuple)): - ftrack_events_path = os.pathsep.join(ftrack_events_path) - os.environ["FTRACK_EVENTS_PATH"] = ftrack_events_path + os.environ["FTRACK_API_USER"] = ftrack_user + os.environ["FTRACK_API_KEY"] = ftrack_api_key if legacy: return legacy_server(ftrack_url) return main_loop(ftrack_url) - - -def main(argv): - ''' - There are 4 values neccessary for event server: - 1.) Ftrack url - "studio.ftrackapp.com" - 2.) Username - "my.username" - 3.) API key - "apikey-long11223344-6665588-5565" - 4.) Path/s to events - "X:/path/to/folder/with/events" - - All these values can be entered with arguments or environment variables. - - arguments: - "-ftrackurl {url}" - "-ftrackuser {username}" - "-ftrackapikey {api key}" - "-ftrackeventpaths {path to events}" - - environment variables: - FTRACK_SERVER - FTRACK_API_USER - FTRACK_API_KEY - FTRACK_EVENTS_PATH - - Credentials (Username & API key): - - Credentials can be stored for auto load on next start - - To *Store/Update* these values add argument "-storecred" - - They will be stored to appsdir file when login is successful - - To *Update/Override* values with enviromnet variables is also needed to: - - *don't enter argument for that value* - - add argument "-noloadcred" (currently stored credentials won't be loaded) - - Order of getting values: - 1.) Arguments are always used when entered. - - entered values through args have most priority! (in each case) - 2.) Credentials are tried to load from appsdir file. - - skipped when credentials were entered through args or credentials - are not stored yet - - can be skipped with "-noloadcred" argument - 3.) Environment variables are last source of values. - - will try to get not yet set values from environments - - Best practice: - - set environment variables FTRACK_SERVER & FTRACK_EVENTS_PATH - - launch event_server_cli with args: - ~/event_server_cli.py -ftrackuser "{username}" -ftrackapikey "{API key}" -storecred - - next time launch event_server_cli.py only with set environment variables - FTRACK_SERVER & FTRACK_EVENTS_PATH - ''' - parser = argparse.ArgumentParser(description='Ftrack event server') - parser.add_argument( - "-ftrackurl", type=str, metavar='FTRACKURL', - help=( - "URL to ftrack server where events should handle" - " (default from environment: $FTRACK_SERVER)" - ) - ) - parser.add_argument( - "-ftrackuser", type=str, - help=( - "Username should be the username of the user in ftrack" - " to record operations against." - " (default from environment: $FTRACK_API_USER)" - ) - ) - parser.add_argument( - "-ftrackapikey", type=str, - help=( - "Should be the API key to use for authentication" - " (default from environment: $FTRACK_API_KEY)" - ) - ) - parser.add_argument( - "-ftrackeventpaths", nargs='+', - help=( - "List of paths where events are stored." - " (default from environment: $FTRACK_EVENTS_PATH)" - ) - ) - parser.add_argument( - '-storecred', - help=( - "Entered credentials will be also stored" - " to apps dir for future usage" - ), - action="store_true" - ) - parser.add_argument( - '-noloadcred', - help="Load creadentials from apps dir", - action="store_true" - ) - parser.add_argument( - '-legacy', - help="Load creadentials from apps dir", - action="store_true" - ) - parser.add_argument( - "-clockifyapikey", type=str, - help=( - "Enter API key for Clockify actions." - " (default from environment: $CLOCKIFY_API_KEY)" - ) - ) - parser.add_argument( - "-clockifyworkspace", type=str, - help=( - "Enter workspace for Clockify." - " (default from module presets or " - "environment: $CLOCKIFY_WORKSPACE)" - ) - ) - ftrack_url = os.environ.get("FTRACK_SERVER") - username = os.environ.get("FTRACK_API_USER") - api_key = os.environ.get("FTRACK_API_KEY") - - kwargs, args = parser.parse_known_args(argv) - - if kwargs.ftrackurl: - ftrack_url = kwargs.ftrackurl - - # Load Ftrack url from settings if not set - if not ftrack_url: - ftrack_url = get_ftrack_url_from_settings() - - event_paths = None - if kwargs.ftrackeventpaths: - event_paths = kwargs.ftrackeventpaths - - if not kwargs.noloadcred: - cred = credentials.get_credentials(ftrack_url) - username = cred.get('username') - api_key = cred.get('api_key') - - if kwargs.ftrackuser: - username = kwargs.ftrackuser - - if kwargs.ftrackapikey: - api_key = kwargs.ftrackapikey - - if kwargs.clockifyworkspace: - os.environ["CLOCKIFY_WORKSPACE"] = kwargs.clockifyworkspace - - if kwargs.clockifyapikey: - os.environ["CLOCKIFY_API_KEY"] = kwargs.clockifyapikey - - legacy = kwargs.legacy - - # Check url regex and accessibility - ftrack_url = check_ftrack_url(ftrack_url) - if not ftrack_url: - print('Exiting! < Please enter Ftrack server url >') - return 1 - - # Validate entered credentials - if not validate_credentials(ftrack_url, username, api_key): - print('Exiting! < Please enter valid credentials >') - return 1 - - if kwargs.storecred: - credentials.save_credentials(username, api_key, ftrack_url) - - # Set Ftrack environments - os.environ["FTRACK_SERVER"] = ftrack_url - os.environ["FTRACK_API_USER"] = username - os.environ["FTRACK_API_KEY"] = api_key - if event_paths: - if isinstance(event_paths, (list, tuple)): - event_paths = os.pathsep.join(event_paths) - os.environ["FTRACK_EVENTS_PATH"] = event_paths - - if legacy: - return legacy_server(ftrack_url) - - return main_loop(ftrack_url) - - -if __name__ == "__main__": - # Register interupt signal - def signal_handler(sig, frame): - print("You pressed Ctrl+C. Process ended.") - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - sys.exit(main(sys.argv)) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py index 28815ca010..a348617cfc 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py @@ -20,7 +20,7 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): # NOTE Import python module here to know if import was successful import ftrack_api - session = ftrack_api.Session(auto_connect_event_hub=True) + session = ftrack_api.Session(auto_connect_event_hub=False) self.log.debug("Ftrack user: \"{0}\"".format(session.api_user)) # Collect task diff --git a/openpype/modules/launcher_action.py b/openpype/modules/launcher_action.py index 5ed8585b6a..0059ff021b 100644 --- a/openpype/modules/launcher_action.py +++ b/openpype/modules/launcher_action.py @@ -15,6 +15,8 @@ class LauncherAction(PypeModule, ITrayAction): def tray_init(self): self.create_window() + self.add_doubleclick_callback(self.show_launcher) + def tray_start(self): return diff --git a/openpype/modules/project_manager_action.py b/openpype/modules/project_manager_action.py new file mode 100644 index 0000000000..1387aa258c --- /dev/null +++ b/openpype/modules/project_manager_action.py @@ -0,0 +1,59 @@ +from . import PypeModule, ITrayAction + + +class ProjectManagerAction(PypeModule, ITrayAction): + label = "Project Manager (beta)" + name = "project_manager" + admin_action = True + + def initialize(self, modules_settings): + enabled = False + module_settings = modules_settings.get(self.name) + if module_settings: + enabled = module_settings.get("enabled", enabled) + self.enabled = enabled + + # Tray attributes + self.project_manager_window = None + + def connect_with_modules(self, *_a, **_kw): + return + + def tray_init(self): + """Initialization in tray implementation of ITrayAction.""" + self.create_project_manager_window() + + def on_action_trigger(self): + """Implementation for action trigger of ITrayAction.""" + self.show_project_manager_window() + + def create_project_manager_window(self): + """Initializa Settings Qt window.""" + if self.project_manager_window: + return + from openpype.tools.project_manager import ProjectManagerWindow + + self.project_manager_window = ProjectManagerWindow() + + def show_project_manager_window(self): + """Show project manager tool window. + + Raises: + AssertionError: Window must be already created. Call + `create_project_manager_window` before calling this method. + """ + if not self.project_manager_window: + raise AssertionError("Window is not initialized.") + + # Store if was visible + was_minimized = self.project_manager_window.isMinimized() + + # Show settings gui + self.project_manager_window.show() + + if was_minimized: + self.project_manager_window.showNormal() + + # Pull window to the front. + self.project_manager_window.raise_() + self.project_manager_window.activateWindow() diff --git a/openpype/modules/settings_action.py b/openpype/modules/settings_action.py index 5651868f68..1035dc0dcd 100644 --- a/openpype/modules/settings_action.py +++ b/openpype/modules/settings_action.py @@ -37,7 +37,8 @@ class ISettingsChangeListener: class SettingsAction(PypeModule, ITrayAction): """Action to show Setttings tool.""" name = "settings" - label = "Settings" + label = "Studio Settings" + admin_action = True def initialize(self, _modules_settings): # This action is always enabled @@ -45,7 +46,7 @@ class SettingsAction(PypeModule, ITrayAction): # User role # TODO should be changeable - self.user_role = "developer" + self.user_role = "manager" # Tray attributes self.settings_window = None @@ -66,14 +67,19 @@ class SettingsAction(PypeModule, ITrayAction): if self.settings_window: return from openpype.tools.settings import MainWidget - self.settings_window = MainWidget(self.user_role) + + self.settings_window = MainWidget(self.user_role, reset_on_show=False) + self.settings_window.trigger_restart.connect(self._on_trigger_restart) + + def _on_trigger_restart(self): + self.manager.restart_tray() def show_settings_window(self): """Show settings tool window. Raises: AssertionError: Window must be already created. Call - `create_settings_window` before callint this method. + `create_settings_window` before calling this method. """ if not self.settings_window: raise AssertionError("Window is not initialized.") @@ -100,7 +106,7 @@ class SettingsAction(PypeModule, ITrayAction): class LocalSettingsAction(PypeModule, ITrayAction): """Action to show Setttings tool.""" name = "local_settings" - label = "Local Settings" + label = "Settings" def initialize(self, _modules_settings): # This action is always enabled diff --git a/openpype/modules/standalonepublish_action.py b/openpype/modules/standalonepublish_action.py index 87f7446341..78d87cb6c7 100644 --- a/openpype/modules/standalonepublish_action.py +++ b/openpype/modules/standalonepublish_action.py @@ -1,5 +1,5 @@ import os -import sys +import platform import subprocess from openpype.lib import get_pype_execute_args from . import PypeModule, ITrayAction @@ -35,4 +35,14 @@ class StandAlonePublishAction(PypeModule, ITrayAction): def run_standalone_publisher(self): args = get_pype_execute_args("standalonepublisher") - subprocess.Popen(args, creationflags=subprocess.DETACHED_PROCESS) + kwargs = {} + if platform.system().lower() == "darwin": + new_args = ["open", "-a", args.pop(0), "--args"] + new_args.extend(args) + args = new_args + + detached_process = getattr(subprocess, "DETACHED_PROCESS", None) + if detached_process is not None: + kwargs["creationflags"] = detached_process + + subprocess.Popen(args, **kwargs) diff --git a/openpype/modules/sync_server/tray/app.py b/openpype/modules/sync_server/tray/app.py index b3b6f0a6c3..dd2b4be749 100644 --- a/openpype/modules/sync_server/tray/app.py +++ b/openpype/modules/sync_server/tray/app.py @@ -45,7 +45,6 @@ class SyncServerWindow(QtWidgets.QDialog): self.pause_btn = QtWidgets.QPushButton("Pause server") left_column_layout.addWidget(self.pause_btn) - left_column.setLayout(left_column_layout) repres = SyncRepresentationSummaryWidget( sync_server, @@ -60,8 +59,6 @@ class SyncServerWindow(QtWidgets.QDialog): split.setSizes([180, 950, 200]) container_layout.addWidget(split) - container.setLayout(container_layout) - body_layout = QtWidgets.QHBoxLayout(body) body_layout.addWidget(container) body_layout.setContentsMargins(0, 0, 0, 0) @@ -77,7 +74,6 @@ class SyncServerWindow(QtWidgets.QDialog): layout.addWidget(body) layout.addWidget(footer) - self.setLayout(body_layout) self.setWindowTitle("Sync Queue") self.projects.project_changed.connect( diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index eae912206e..d38416fbce 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -602,7 +602,6 @@ class SyncServerDetailWindow(QtWidgets.QDialog): layout.addWidget(body) layout.addWidget(footer) - self.setLayout(body_layout) self.setWindowTitle("Sync Representation Detail") diff --git a/openpype/modules/webserver/host_console_listener.py b/openpype/modules/webserver/host_console_listener.py new file mode 100644 index 0000000000..01a8af643e --- /dev/null +++ b/openpype/modules/webserver/host_console_listener.py @@ -0,0 +1,152 @@ +import aiohttp +from aiohttp import web +import json +import logging +from concurrent.futures import CancelledError +from Qt import QtWidgets + +from openpype.modules import ITrayService + +log = logging.getLogger(__name__) + + +class IconType: + IDLE = "idle" + RUNNING = "running" + FAILED = "failed" + + +class MsgAction: + CONNECTING = "connecting" + INITIALIZED = "initialized" + ADD = "add" + CLOSE = "close" + + +class HostListener: + def __init__(self, webserver, module): + self._window_per_id = {} + self.module = module + self.webserver = webserver + self._window_per_id = {} # dialogs per host name + self._action_per_id = {} # QAction per host name + + webserver.add_route('*', "/ws/host_listener", self.websocket_handler) + + def _host_is_connecting(self, host_name, label): + from openpype.tools.tray_app.app import ConsoleDialog + + """ Initialize dialog, adds to submenu. """ + services_submenu = self.module._services_submenu + action = QtWidgets.QAction(label, services_submenu) + action.triggered.connect(lambda: self.show_widget(host_name)) + + services_submenu.addAction(action) + self._action_per_id[host_name] = action + self._set_host_icon(host_name, IconType.IDLE) + widget = ConsoleDialog("") + self._window_per_id[host_name] = widget + + def _set_host_icon(self, host_name, icon_type): + """Assigns icon to action for 'host_name' with 'icon_type'. + + Action must exist in self._action_per_id + + Args: + host_name (str) + icon_type (IconType) + """ + action = self._action_per_id.get(host_name) + if not action: + raise ValueError("Unknown host {}".format(host_name)) + + icon = None + if icon_type == IconType.IDLE: + icon = ITrayService.get_icon_idle() + elif icon_type == IconType.RUNNING: + icon = ITrayService.get_icon_running() + elif icon_type == IconType.FAILED: + icon = ITrayService.get_icon_failed() + else: + log.info("Unknown icon type {} for {}".format(icon_type, + host_name)) + action.setIcon(icon) + + def show_widget(self, host_name): + """Shows prepared widget for 'host_name'. + + Dialog get initialized when 'host_name' is connecting. + """ + self.module.execute_in_main_thread( + lambda: self._show_widget(host_name)) + + def _show_widget(self, host_name): + widget = self._window_per_id[host_name] + widget.show() + widget.raise_() + widget.activateWindow() + + async def websocket_handler(self, request): + ws = web.WebSocketResponse() + await ws.prepare(request) + + widget = None + try: + async for msg in ws: + if msg.type == aiohttp.WSMsgType.TEXT: + host_name, action, text = self._parse_message(msg) + + if action == MsgAction.CONNECTING: + self._action_per_id[host_name] = None + # must be sent to main thread, or action wont trigger + self.module.execute_in_main_thread( + lambda: self._host_is_connecting(host_name, text)) + elif action == MsgAction.CLOSE: + # clean close + self._close(host_name) + await ws.close() + elif action == MsgAction.INITIALIZED: + self.module.execute_in_main_thread( + # must be queued as _host_is_connecting might not + # be triggered/finished yet + lambda: self._set_host_icon(host_name, + IconType.RUNNING)) + elif action == MsgAction.ADD: + self.module.execute_in_main_thread( + lambda: self._add_text(host_name, text)) + elif msg.type == aiohttp.WSMsgType.ERROR: + print('ws connection closed with exception %s' % + ws.exception()) + host_name, _, _ = self._parse_message(msg) + self._set_host_icon(host_name, IconType.FAILED) + except CancelledError: # recoverable + pass + except Exception as exc: + log.warning("Exception during communication", exc_info=True) + if widget: + error_msg = str(exc) + widget.append_text(error_msg) + + return ws + + def _add_text(self, host_name, text): + widget = self._window_per_id[host_name] + widget.append_text(text) + + def _close(self, host_name): + """ Clean close - remove from menu, delete widget.""" + services_submenu = self.module._services_submenu + action = self._action_per_id.pop(host_name) + services_submenu.removeAction(action) + widget = self._window_per_id.pop(host_name) + if widget.isVisible(): + widget.hide() + widget.deleteLater() + + def _parse_message(self, msg): + data = json.loads(msg.data) + action = data.get("action") + host_name = data["host"] + value = data.get("text") + + return host_name, action, value diff --git a/openpype/modules/webserver/webserver_module.py b/openpype/modules/webserver/webserver_module.py index 59a0a08427..b61619acde 100644 --- a/openpype/modules/webserver/webserver_module.py +++ b/openpype/modules/webserver/webserver_module.py @@ -23,6 +23,7 @@ class WebServerModule(PypeModule, ITrayService): def initialize(self, _module_settings): self.enabled = True self.server_manager = None + self._host_listener = None self.port = self.find_free_port() @@ -37,6 +38,7 @@ class WebServerModule(PypeModule, ITrayService): def tray_init(self): self.create_server_manager() self._add_resources_statics() + self._add_listeners() def tray_start(self): self.start_server() @@ -54,6 +56,13 @@ class WebServerModule(PypeModule, ITrayService): webserver_url, static_prefix ) + def _add_listeners(self): + from openpype.modules.webserver import host_console_listener + + self._host_listener = host_console_listener.HostListener( + self.server_manager, self + ) + def start_server(self): if self.server_manager: self.server_manager.start_server() diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 68b1f9a52a..3753f1bfc9 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -11,7 +11,7 @@ from openpype import resources from openpype.lib.delivery import ( sizeof_fmt, - path_from_represenation, + path_from_representation, get_format_dict, check_destination_path, process_single_file, @@ -170,7 +170,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): if repre["name"] not in selected_repres: continue - repre_path = path_from_represenation(repre, self.anatomy) + repre_path = path_from_representation(repre, self.anatomy) anatomy_data = copy.deepcopy(repre["context"]) new_report_items = check_destination_path(str(repre["_id"]), diff --git a/openpype/plugins/publish/collect_otio_review.py b/openpype/plugins/publish/collect_otio_review.py index 4243926ba3..e78ccc032c 100644 --- a/openpype/plugins/publish/collect_otio_review.py +++ b/openpype/plugins/publish/collect_otio_review.py @@ -29,7 +29,7 @@ class CollectOcioReview(pyblish.api.InstancePlugin): otio_review_clips = [] otio_timeline = instance.context.data["otioTimeline"] otio_clip = instance.data["otioClip"] - + self.log.debug("__ otioClip: {}".format(otio_clip)) # optionally get `reviewTrack` review_track_name = instance.data.get("reviewTrack") @@ -37,7 +37,7 @@ class CollectOcioReview(pyblish.api.InstancePlugin): otio_tl_range = otio_clip.range_in_parent() # calculate real timeline end needed for the clip - clip_end_frame = int( + clip_frame_end = int( otio_tl_range.start_time.value + otio_tl_range.duration.value) # skip if no review track available @@ -57,13 +57,12 @@ class CollectOcioReview(pyblish.api.InstancePlugin): track_rip = track.range_in_parent() # calculate real track end frame - track_end_frame = int( - track_rip.start_time.value + track_rip.duration.value) + track_frame_end = int(track_rip.end_time_exclusive().value) # check if the end of track is not lower then clip requirement - if clip_end_frame > track_end_frame: + if clip_frame_end > track_frame_end: # calculate diference duration - gap_duration = clip_end_frame - track_end_frame + gap_duration = clip_frame_end - track_frame_end # create rational time range for gap otio_gap_range = otio.opentime.TimeRange( start_time=otio.opentime.RationalTime( diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index cebfc90630..fa376eeac7 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -11,6 +11,7 @@ import clique import opentimelineio as otio import pyblish.api import openpype +from openpype.lib import editorial class CollectOcioSubsetResources(pyblish.api.InstancePlugin): @@ -27,59 +28,80 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): return if not instance.data.get("representations"): - instance.data["representations"] = list() - version_data = dict() + instance.data["representations"] = [] + + if not instance.data.get("versionData"): + instance.data["versionData"] = {} + + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] # get basic variables otio_clip = instance.data["otioClip"] - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - - # generate range in parent - otio_src_range = otio_clip.source_range otio_avalable_range = otio_clip.available_range() - trimmed_media_range = openpype.lib.trim_media_range( - otio_avalable_range, otio_src_range) + media_fps = otio_avalable_range.start_time.rate - # calculate wth handles - otio_src_range_handles = openpype.lib.otio_range_with_handles( - otio_src_range, instance) - trimmed_media_range_h = openpype.lib.trim_media_range( - otio_avalable_range, otio_src_range_handles) + # get available range trimmed with processed retimes + retimed_attributes = editorial.get_media_range_with_retimes( + otio_clip, handle_start, handle_end) + self.log.debug( + ">> retimed_attributes: {}".format(retimed_attributes)) - # frame start and end from media - s_frame_start, s_frame_end = openpype.lib.otio_range_to_frame_range( - trimmed_media_range) - a_frame_start, a_frame_end = openpype.lib.otio_range_to_frame_range( - otio_avalable_range) - a_frame_start_h, a_frame_end_h = openpype.lib.\ - otio_range_to_frame_range(trimmed_media_range_h) + # break down into variables + media_in = int(retimed_attributes["mediaIn"]) + media_out = int(retimed_attributes["mediaOut"]) + handle_start = int(retimed_attributes["handleStart"]) + handle_end = int(retimed_attributes["handleEnd"]) - # fix frame_start and frame_end frame to be in range of media - if a_frame_start_h < a_frame_start: - a_frame_start_h = a_frame_start + # set versiondata if any retime + version_data = retimed_attributes.get("versionData") - if a_frame_end_h > a_frame_end: - a_frame_end_h = a_frame_end + if version_data: + instance.data["versionData"].update(version_data) - # count the difference for frame_start and frame_end - diff_start = s_frame_start - a_frame_start_h - diff_end = a_frame_end_h - s_frame_end + # convert to available frame range with handles + a_frame_start_h = media_in - handle_start + a_frame_end_h = media_out + handle_end + + # create trimmed ocio time range + trimmed_media_range_h = editorial.range_from_frames( + a_frame_start_h, (a_frame_end_h - a_frame_start_h + 1), + media_fps + ) + self.log.debug("trimmed_media_range_h: {}".format( + trimmed_media_range_h)) + self.log.debug("a_frame_start_h: {}".format( + a_frame_start_h)) + self.log.debug("a_frame_end_h: {}".format( + a_frame_end_h)) + + # create frame start and end + frame_start = instance.data["frameStart"] + frame_end = frame_start + (media_out - media_in) # add to version data start and end range data # for loader plugins to be correctly displayed and loaded - version_data.update({ - "frameStart": frame_start, - "frameEnd": frame_end, - "handleStart": diff_start, - "handleEnd": diff_end, - "fps": otio_avalable_range.start_time.rate + instance.data["versionData"].update({ + "fps": media_fps }) + if not instance.data["versionData"].get("retime"): + instance.data["versionData"].update({ + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": handle_start, + "handleEnd": handle_end, + }) + else: + instance.data["versionData"].update({ + "frameStart": frame_start, + "frameEnd": frame_end + }) + # change frame_start and frame_end values # for representation to be correctly renumbered in integrate_new - frame_start -= diff_start - frame_end += diff_end + frame_start -= handle_start + frame_end += handle_end media_ref = otio_clip.media_reference metadata = media_ref.metadata @@ -136,12 +158,13 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): frame_start, frame_end, file=filename) if repre: - instance.data["versionData"] = version_data - self.log.debug(">>>>>>>> version data {}".format(version_data)) # add representation to instance data instance.data["representations"].append(repre) self.log.debug(">>>>>>>> {}".format(repre)) + import pprint + self.log.debug(pprint.pformat(instance.data)) + def _create_representation(self, start, end, **kwargs): """ Creating representation data. diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 76f6ffc608..ef52d51325 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -44,6 +44,7 @@ class ExtractBurnin(openpype.api.Extractor): "harmony", "fusion", "aftereffects", + "tvpaint" # "resolve" ] optional = True diff --git a/openpype/plugins/publish/extract_otio_review.py b/openpype/plugins/publish/extract_otio_review.py index 2f46bcb375..818903b54b 100644 --- a/openpype/plugins/publish/extract_otio_review.py +++ b/openpype/plugins/publish/extract_otio_review.py @@ -51,7 +51,6 @@ class ExtractOTIOReview(openpype.api.Extractor): def process(self, instance): # TODO: convert resulting image sequence to mp4 - # TODO: add oudio ouput to the mp4 if audio in review is on. # get otio clip and other time info from instance clip # TODO: what if handles are different in `versionData`? diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 892b8c86bf..e1e24af3ea 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -3,6 +3,9 @@ import re import copy import json +from abc import ABCMeta, abstractmethod +import six + import clique import pyblish.api @@ -14,6 +17,7 @@ from openpype.lib import ( get_decompress_dir, decompress ) +import speedcopy class ExtractReview(pyblish.api.InstancePlugin): @@ -187,7 +191,16 @@ class ExtractReview(pyblish.api.InstancePlugin): "New representation tags: `{}`".format(new_repre["tags"]) ) - temp_data = self.prepare_temp_data(instance, repre, output_def) + temp_data = self.prepare_temp_data( + instance, repre, output_def) + files_to_clean = [] + if temp_data["input_is_sequence"]: + self.log.info("Filling gaps in sequence.") + files_to_clean = self.fill_sequence_gaps( + temp_data["origin_repre"]["files"], + new_repre["stagingDir"], + temp_data["frame_start"], + temp_data["frame_end"]) try: # temporary until oiiotool is supported cross platform ffmpeg_args = self._ffmpeg_arguments( @@ -198,7 +211,7 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("Unsupported compression on input " + "files. Skipping!!!") return - raise + raise NotImplementedError subprcs_cmd = " ".join(ffmpeg_args) @@ -209,6 +222,11 @@ class ExtractReview(pyblish.api.InstancePlugin): subprcs_cmd, shell=True, logger=self.log ) + # delete files added to fill gaps + if files_to_clean: + for f in files_to_clean: + os.unlink(f) + output_name = output_def["filename_suffix"] if temp_data["without_handles"]: output_name += "_noHandles" @@ -601,6 +619,89 @@ class ExtractReview(pyblish.api.InstancePlugin): return all_args + def fill_sequence_gaps(self, files, staging_dir, start_frame, end_frame): + # type: (list, str, int, int) -> list + """Fill missing files in sequence by duplicating existing ones. + + This will take nearest frame file and copy it with so as to fill + gaps in sequence. Last existing file there is is used to for the + hole ahead. + + Args: + files (list): List of representation files. + staging_dir (str): Path to staging directory. + start_frame (int): Sequence start (no matter what files are there) + end_frame (int): Sequence end (no matter what files are there) + + Returns: + list of added files. Those should be cleaned after work + is done. + + Raises: + AssertionError: if more then one collection is obtained. + + """ + collections = clique.assemble(files)[0] + assert len(collections) == 1, "Multiple collections found." + col = collections[0] + # do nothing if sequence is complete + if list(col.indexes)[0] == start_frame and \ + list(col.indexes)[-1] == end_frame and \ + col.is_contiguous(): + return [] + + holes = col.holes() + + # generate ideal sequence + complete_col = clique.assemble( + [("{}{:0" + str(col.padding) + "d}{}").format( + col.head, f, col.tail + ) for f in range(start_frame, end_frame)] + )[0][0] # type: clique.Collection + + new_files = {} + last_existing_file = None + + for idx in holes.indexes: + # get previous existing file + test_file = os.path.normpath(os.path.join( + staging_dir, + ("{}{:0" + str(complete_col.padding) + "d}{}").format( + complete_col.head, idx - 1, complete_col.tail))) + if os.path.isfile(test_file): + new_files[idx] = test_file + last_existing_file = test_file + else: + if not last_existing_file: + # previous file is not found (sequence has a hole + # at the beginning. Use first available frame + # there is. + try: + last_existing_file = list(col)[0] + except IndexError: + # empty collection? + raise AssertionError( + "Invalid sequence collected") + new_files[idx] = os.path.normpath( + os.path.join(staging_dir, last_existing_file)) + + files_to_clean = [] + if new_files: + # so now new files are dict with missing frame as a key and + # existing file as a value. + for frame, file in new_files.items(): + self.log.info( + "Filling gap {} with {}".format(frame, file)) + + hole = os.path.join( + staging_dir, + ("{}{:0" + str(col.padding) + "d}{}").format( + col.head, frame, col.tail)) + speedcopy.copyfile(file, hole) + files_to_clean.append(hole) + + return files_to_clean + def input_output_paths(self, new_repre, output_def, temp_data): """Deduce input nad output file paths based on entered data. @@ -619,7 +720,6 @@ class ExtractReview(pyblish.api.InstancePlugin): if temp_data["input_is_sequence"]: collections = clique.assemble(repre["files"])[0] - full_input_path = os.path.join( staging_dir, collections[0].format("{head}{padding}{tail}") @@ -873,12 +973,6 @@ class ExtractReview(pyblish.api.InstancePlugin): """ filters = [] - letter_box_def = output_def["letter_box"] - letter_box_enabled = letter_box_def["enabled"] - - # Get instance data - pixel_aspect = temp_data["pixel_aspect"] - # NOTE Skipped using instance's resolution full_input_path_single_file = temp_data["full_input_path_single_file"] input_data = ffprobe_streams( @@ -887,6 +981,33 @@ class ExtractReview(pyblish.api.InstancePlugin): input_width = int(input_data["width"]) input_height = int(input_data["height"]) + # 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 + + # Convert overscan value video filters + overscan_crop = output_def.get("overscan_crop") + overscan = OverscanCrop(input_width, input_height, overscan_crop) + overscan_crop_filters = overscan.video_filters() + # Add overscan filters to filters if are any and modify input + # resolution by it's values + if overscan_crop_filters: + filters.extend(overscan_crop_filters) + input_width = overscan.width() + input_height = overscan.height() + # Use output resolution as inputs after cropping to skip usage of + # instance data resolution + if output_width is None or output_height is None: + output_width = input_width + output_height = input_height + + letter_box_def = output_def["letter_box"] + letter_box_enabled = letter_box_def["enabled"] + + # Get instance data + pixel_aspect = temp_data["pixel_aspect"] + # Make sure input width and height is not an odd number input_width_is_odd = bool(input_width % 2 != 0) input_height_is_odd = bool(input_height % 2 != 0) @@ -911,10 +1032,6 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("input_width: `{}`".format(input_width)) self.log.debug("input_height: `{}`".format(input_height)) - # 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 # Use instance resolution if output definition has not set it. if output_width is None or output_height is None: output_width = temp_data["resolution_width"] @@ -1438,3 +1555,291 @@ class ExtractReview(pyblish.api.InstancePlugin): vf_back = "-vf " + ",".join(vf_fixed) return vf_back + + +@six.add_metaclass(ABCMeta) +class _OverscanValue: + def __repr__(self): + return "<{}> {}".format(self.__class__.__name__, str(self)) + + @abstractmethod + def copy(self): + """Create a copy of object.""" + pass + + @abstractmethod + def size_for(self, value): + """Calculate new value for passed value.""" + pass + + +class PixValueExplicit(_OverscanValue): + def __init__(self, value): + self._value = int(value) + + def __str__(self): + return "{}px".format(self._value) + + def copy(self): + return PixValueExplicit(self._value) + + def size_for(self, value): + if self._value == 0: + return value + return self._value + + +class PercentValueExplicit(_OverscanValue): + def __init__(self, value): + self._value = float(value) + + def __str__(self): + return "{}%".format(abs(self._value)) + + def copy(self): + return PercentValueExplicit(self._value) + + def size_for(self, value): + if self._value == 0: + return value + return int((value / 100) * self._value) + + +class PixValueRelative(_OverscanValue): + def __init__(self, value): + self._value = int(value) + + def __str__(self): + sign = "-" if self._value < 0 else "+" + return "{}{}px".format(sign, abs(self._value)) + + def copy(self): + return PixValueRelative(self._value) + + def size_for(self, value): + return value + self._value + + +class PercentValueRelative(_OverscanValue): + def __init__(self, value): + self._value = float(value) + + def __str__(self): + return "{}%".format(self._value) + + def copy(self): + return PercentValueRelative(self._value) + + def size_for(self, value): + if self._value == 0: + return value + + offset = int((value / 100) * self._value) + + return value + offset + + +class PercentValueRelativeSource(_OverscanValue): + def __init__(self, value, source_sign): + self._value = float(value) + if source_sign not in ("-", "+"): + raise ValueError( + "Invalid sign value \"{}\" expected \"-\" or \"+\"".format( + source_sign + ) + ) + self._source_sign = source_sign + + def __str__(self): + return "{}%{}".format(self._value, self._source_sign) + + def copy(self): + return PercentValueRelativeSource(self._value, self._source_sign) + + def size_for(self, value): + if self._value == 0: + return value + return int((value * 100) / (100 - self._value)) + + +class OverscanCrop: + """Helper class to read overscan string and calculate output resolution. + + It is possible to enter single value for both width and heigh or two values + for width and height. Overscan string may have a few variants. Each variant + define output size for input size. + + ### Example + For input size: 2200px + + | String | Output | Description | + |----------|--------|-------------------------------------------------| + | "" | 2200px | Empty string does nothing. | + | "10%" | 220px | Explicit percent size. | + | "-10%" | 1980px | Relative percent size (decrease). | + | "+10%" | 2420px | Relative percent size (increase). | + | "-10%+" | 2000px | Relative percent size to output size. | + | "300px" | 300px | Explicit output size cropped or expanded. | + | "-300px" | 1900px | Relative pixel size (decrease). | + | "+300px" | 2500px | Relative pixel size (increase). | + | "300" | 300px | Value without "%" and "px" is used as has "px". | + + Value without sign (+/-) in is always explicit and value with sign is + relative. Output size for "200px" and "+200px" are not the same. + Values "0", "0px" or "0%" are ignored. + + All values that cause output resolution smaller than 1 pixel are invalid. + + Value "-10%+" is a special case which says that input's resolution is + bigger by 10% than expected output. + + It is possible to combine these variants to define different output for + width and height. + + Resolution: 2000px 1000px + + | String | Output | + |---------------|---------------| + | "100px 120px" | 2100px 1120px | + | "-10% -200px" | 1800px 800px | + """ + + item_regex = re.compile(r"([\+\-])?([0-9]+)(.+)?") + relative_source_regex = re.compile(r"%([\+\-])") + + def __init__(self, input_width, input_height, string_value): + # Make sure that is not None + string_value = string_value or "" + + self.input_width = input_width + self.input_height = input_height + + width, height = self._convert_string_to_values(string_value) + self._width_value = width + self._height_value = height + + self._string_value = string_value + + def __str__(self): + return "{}".format(self._string_value) + + def __repr__(self): + return "<{}>".format(self.__class__.__name__) + + def width(self): + """Calculated width.""" + return self._width_value.size_for(self.input_width) + + def height(self): + """Calculated height.""" + return self._height_value.size_for(self.input_height) + + def video_filters(self): + """FFmpeg video filters to achieve expected result. + + Filter may be empty, use "crop" filter, "pad" filter or combination of + "crop" and "pad". + + Returns: + list: FFmpeg video filters. + """ + # crop=width:height:x:y - explicit start x, y position + # crop=width:height - x, y are related to center by width/height + # pad=width:heigth:x:y - explicit start x, y position + # pad=width:heigth - x, y are set to 0 by default + + width = self.width() + height = self.height() + + output = [] + if self.input_width == width and self.input_height == height: + return output + + # Make sure resolution has odd numbers + if width % 2 == 1: + width -= 1 + + if height % 2 == 1: + height -= 1 + + if width <= self.input_width and height <= self.input_height: + output.append("crop={}:{}".format(width, height)) + + elif width >= self.input_width and height >= self.input_height: + output.append( + "pad={}:{}:(iw-ow)/2:(ih-oh)/2".format(width, height) + ) + + 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)) + + 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)) + + return output + + def _convert_string_to_values(self, orig_string_value): + string_value = orig_string_value.strip().lower() + if not string_value: + return [PixValueRelative(0), PixValueRelative(0)] + + # Replace "px" (and spaces before) with single space + string_value = re.sub(r"([ ]+)?px", " ", string_value) + string_value = re.sub(r"([ ]+)%", "%", string_value) + # Make sure +/- sign at the beggining of string is next to number + string_value = re.sub(r"^([\+\-])[ ]+", "\g<1>", string_value) + # Make sure +/- sign in the middle has zero spaces before number under + # which belongs + string_value = re.sub( + r"[ ]([\+\-])[ ]+([0-9])", + r" \g<1>\g<2>", + string_value + ) + string_parts = [ + part + for part in string_value.split(" ") + if part + ] + + error_msg = "Invalid string for rescaling \"{}\"".format( + orig_string_value + ) + if 1 > len(string_parts) > 2: + raise ValueError(error_msg) + + output = [] + for item in string_parts: + groups = self.item_regex.findall(item) + if not groups: + raise ValueError(error_msg) + + relative_sign, value, ending = groups[0] + if not relative_sign: + if not ending: + output.append(PixValueExplicit(value)) + else: + output.append(PercentValueExplicit(value)) + else: + source_sign_group = self.relative_source_regex.findall(ending) + if not ending: + output.append(PixValueRelative(int(relative_sign + value))) + + elif source_sign_group: + source_sign = source_sign_group[0] + output.append(PercentValueRelativeSource( + float(relative_sign + value), source_sign + )) + else: + output.append( + PercentValueRelative(float(relative_sign + value)) + ) + + if len(output) == 1: + width = output.pop(0) + height = width.copy() + else: + width, height = output + + return width, height diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 3a926789fb..c5ce6d23aa 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -417,21 +417,22 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst_padding_exp = src_padding_exp dst_start_frame = None + collection_start = list(src_collection.indexes)[0] for i in src_collection.indexes: # TODO 1.) do not count padding in each index iteration # 2.) do not count dst_padding from src_padding before # index_frame_start check + frame_number = i - collection_start src_padding = src_padding_exp % i src_file_name = "{0}{1}{2}".format( src_head, src_padding, src_tail) - dst_padding = src_padding_exp % i + dst_padding = src_padding_exp % frame_number if index_frame_start is not None: dst_padding_exp = "%0{}d".format(frame_start_padding) - dst_padding = dst_padding_exp % index_frame_start - index_frame_start += 1 + dst_padding = dst_padding_exp % (index_frame_start + frame_number) # noqa: E501 dst = "{0}{1}{2}".format( dst_head, diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 326ca8349a..f86ece6799 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -27,7 +27,10 @@ class PypeCommands: from openpype.tools import settings # TODO change argument options to allow enum of user roles - user_role = "developer" + if dev: + user_role = "developer" + else: + user_role = "manager" settings.main(user_role) @staticmethod diff --git a/openpype/tools/settings/resources/images/eye.png b/openpype/resources/icons/eye.png similarity index 100% rename from openpype/tools/settings/resources/images/eye.png rename to openpype/resources/icons/eye.png diff --git a/openpype/scripts/non_python_host_launch.py b/openpype/scripts/non_python_host_launch.py index 506105d2ce..32c4b23f4f 100644 --- a/openpype/scripts/non_python_host_launch.py +++ b/openpype/scripts/non_python_host_launch.py @@ -81,11 +81,11 @@ def main(argv): host_name = os.environ["AVALON_APP"].lower() if host_name == "photoshop": - from avalon.photoshop.lib import launch + from avalon.photoshop.lib import main elif host_name == "aftereffects": - from avalon.aftereffects.lib import launch + from avalon.aftereffects.lib import main elif host_name == "harmony": - from avalon.harmony.lib import launch + from avalon.harmony.lib import main else: title = "Unknown host name" message = ( @@ -97,7 +97,7 @@ def main(argv): if launch_args: # Launch host implementation - launch(*launch_args) + main(*launch_args) else: # Show message box on_invalid_args(after_script_idx is None) diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index f54dbb9612..698b3b35a9 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -4,8 +4,12 @@ "enabled": true, "optional": true, "active": true, - "skip_resolution_check": [".*"], - "skip_timelines_check": [".*"] + "skip_resolution_check": [ + ".*" + ], + "skip_timelines_check": [ + ".*" + ] }, "AfterEffectsSubmitDeadline": { "use_published": true, @@ -14,5 +18,9 @@ "secondary_pool": "", "chunk_size": 1000000 } + }, + "workfile_builder": { + "create_first_version": false, + "custom_templates": [] } } \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json new file mode 100644 index 0000000000..a7262dcb5d --- /dev/null +++ b/openpype/settings/defaults/project_settings/blender.json @@ -0,0 +1,6 @@ +{ + "workfile_builder": { + "create_first_version": false, + "custom_templates": [] + } +} \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index d3b8c55d07..5f779fccfa 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -55,6 +55,7 @@ "ftrack" ] }, + "overscan_crop": "", "width": 0, "height": 0, "bg_color": [ @@ -272,8 +273,7 @@ "active_site": "studio", "remote_site": "studio" }, - "sites": { - } + "sites": {} }, "project_plugins": { "windows": [], diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 779b8bb3f3..ba685ae502 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -293,19 +293,22 @@ }, "Display Options": { "background": [ - 0.7, - 0.7, - 0.7 + 125, + 125, + 125, + 255 ], "backgroundBottom": [ - 0.7, - 0.7, - 0.7 + 125, + 125, + 125, + 255 ], "backgroundTop": [ - 0.7, - 0.7, - 0.7 + 125, + 125, + 125, + 255 ], "override_display": true }, @@ -393,74 +396,88 @@ "load": { "colors": { "model": [ - 0.821, - 0.518, - 0.117 + 209, + 132, + 30, + 255 ], "rig": [ - 0.144, - 0.443, - 0.463 + 59, + 226, + 235, + 255 ], "pointcache": [ - 0.368, - 0.821, - 0.117 + 94, + 209, + 30, + 255 ], "animation": [ - 0.368, - 0.821, - 0.117 + 94, + 209, + 30, + 255 ], "ass": [ - 1.0, - 0.332, - 0.312 + 249, + 135, + 53, + 255 ], "camera": [ - 0.447, - 0.312, - 1.0 + 136, + 114, + 244, + 255 ], "fbx": [ - 1.0, - 0.931, - 0.312 + 215, + 166, + 255, + 255 ], "mayaAscii": [ - 0.312, - 1.0, - 0.747 + 67, + 174, + 255, + 255 ], "setdress": [ - 0.312, - 1.0, - 0.747 + 255, + 250, + 90, + 255 ], "layout": [ - 0.312, - 1.0, - 0.747 + 255, + 250, + 90, + 255 ], "vdbcache": [ - 0.312, - 1.0, - 0.428 + 249, + 54, + 0, + 255 ], "vrayproxy": [ - 0.258, - 0.95, - 0.541 + 255, + 150, + 12, + 255 ], "yeticache": [ - 0.2, - 0.8, - 0.3 + 99, + 206, + 220, + 255 ], "yetiRig": [ - 0.0, - 0.8, - 0.5 + 0, + 205, + 125, + 255 ] } }, diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index bb5232cea7..13e1924b36 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -6,9 +6,7 @@ "load": "ctrl+alt+l", "manage": "ctrl+alt+m", "build_workfile": "ctrl+alt+b" - }, - "open_workfile_at_start": false, - "create_initial_workfile": true + } }, "create": { "CreateWriteRender": { @@ -147,12 +145,13 @@ "node_name_template": "{class_name}_{ext}" } }, - "workfile_build": { + "workfile_builder": { + "create_first_version": false, + "custom_templates": [], + "builder_on_start": false, "profiles": [ { - "tasks": [ - "compositing" - ], + "tasks": [], "current_context": [ { "subset_name_filters": [], @@ -162,10 +161,12 @@ ], "repre_names": [ "exr", - "dpx" + "dpx", + "mov" ], "loaders": [ - "LoadSequence" + "LoadSequence", + "LoadMov" ] } ], diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index 0db6e8248d..b306a757a6 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -13,5 +13,9 @@ "jpg" ] } + }, + "workfile_builder": { + "create_first_version": false, + "custom_templates": [] } } \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 9d5b922b8e..b4f3b315ec 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -32,5 +32,9 @@ } } }, + "workfile_builder": { + "create_first_version": false, + "custom_templates": [] + }, "filters": {} } \ No newline at end of file diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index 5c4aa6c485..31da9e9e7b 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -164,5 +164,8 @@ }, "standalonepublish_tool": { "enabled": true + }, + "project_manager": { + "enabled": true } } \ No newline at end of file diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 33881a6097..f64ca1e98d 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -103,6 +103,7 @@ from .enum_entity import ( EnumEntity, AppsEnumEntity, ToolsEnumEntity, + TaskTypeEnumEntity, ProvidersEnum ) @@ -154,6 +155,7 @@ __all__ = ( "EnumEntity", "AppsEnumEntity", "ToolsEnumEntity", + "TaskTypeEnumEntity", "ProvidersEnum", "ListEntity", diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 90efb73fbc..c6bff1ff47 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -111,6 +111,8 @@ class BaseItemEntity(BaseEntity): self.file_item = None # Reference to `RootEntity` self.root_item = None + # Change of value requires restart of OpenPype + self._require_restart_on_change = False # Entity is in hierarchy of dynamically created entity self.is_in_dynamic_item = False @@ -171,6 +173,14 @@ class BaseItemEntity(BaseEntity): roles = [roles] self.roles = roles + @property + def require_restart_on_change(self): + return self._require_restart_on_change + + @property + def require_restart(self): + return False + @property def has_studio_override(self): """Says if entity or it's children has studio overrides.""" @@ -261,6 +271,14 @@ class BaseItemEntity(BaseEntity): self, "Dynamic entity has set `is_group` to true." ) + if ( + self.require_restart_on_change + and (self.is_dynamic_item or self.is_in_dynamic_item) + ): + raise EntitySchemaError( + self, "Dynamic entity can't require restart." + ) + @abstractmethod def set_override_state(self, state): """Set override state and trigger it on children. @@ -788,6 +806,15 @@ class ItemEntity(BaseItemEntity): # Root item reference self.root_item = self.parent.root_item + # Item require restart on value change + require_restart_on_change = self.schema_data.get("require_restart") + if ( + require_restart_on_change is None + and not (self.is_dynamic_item or self.is_in_dynamic_item) + ): + require_restart_on_change = self.parent.require_restart_on_change + self._require_restart_on_change = require_restart_on_change + # File item reference if self.parent.is_file: self.file_item = self.parent diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index 907bf98784..4b221720c3 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -439,10 +439,10 @@ class DictMutableKeysEntity(EndpointEntity): new_initial_value = [] for key, value in _settings_value: if key in initial_value: - new_initial_value.append(key, initial_value.pop(key)) + new_initial_value.append([key, initial_value.pop(key)]) for key, value in initial_value.items(): - new_initial_value.append(key, value) + new_initial_value.append([key, value]) initial_value = new_initial_value else: initial_value = _settings_value diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index c6021b68de..5df365508c 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -219,6 +219,41 @@ class ToolsEnumEntity(BaseEnumEntity): self._current_value = new_value +class TaskTypeEnumEntity(BaseEnumEntity): + schema_types = ["task-types-enum"] + + def _item_initalization(self): + self.multiselection = True + self.value_on_not_set = [] + self.enum_items = [] + self.valid_keys = set() + self.valid_value_types = (list, ) + self.placeholder = None + + def _get_enum_values(self): + anatomy_entity = self.get_entity_from_path( + "project_settings/project_anatomy" + ) + + valid_keys = set() + enum_items = [] + for task_type in anatomy_entity["tasks"].keys(): + enum_items.append({task_type: task_type}) + valid_keys.add(task_type) + + return enum_items, valid_keys + + def set_override_state(self, *args, **kwargs): + super(TaskTypeEnumEntity, self).set_override_state(*args, **kwargs) + + self.enum_items, self.valid_keys = self._get_enum_values() + new_value = [] + for key in self._current_value: + if key in self.valid_keys: + new_value.append(key) + self._current_value = new_value + + class ProvidersEnum(BaseEnumEntity): schema_types = ["providers-enum"] diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 409e6a66b4..295333eb60 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -68,8 +68,18 @@ class EndpointEntity(ItemEntity): def on_change(self): for callback in self.on_change_callbacks: callback() + + if self.require_restart_on_change: + if self.require_restart: + self.root_item.add_item_require_restart(self) + else: + self.root_item.remove_item_require_restart(self) self.parent.on_child_change(self) + @property + def require_restart(self): + return self.has_unsaved_changes + def update_default_value(self, value): value = self._check_update_value(value, "default") self._default_value = value @@ -115,6 +125,10 @@ class InputEntity(EndpointEntity): """Entity's value without metadata.""" return self._current_value + @property + def require_restart(self): + return self._value_is_modified + def _settings_value(self): return copy.deepcopy(self.value) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index ed3d7aed84..05f4ea64f8 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -17,26 +17,60 @@ WRAPPER_TYPES = ["form", "collapsible-wrap"] NOT_SET = type("NOT_SET", (), {"__bool__": lambda obj: False})() OVERRIDE_VERSION = 1 +DEFAULT_VALUES_KEY = "__default_values__" +TEMPLATE_METADATA_KEYS = ( + DEFAULT_VALUES_KEY, +) + 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, required_keys=None, missing_keys=None + 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() - _template = [] - default_values = {} - for item in template: - if isinstance(item, dict) and "__default_values__" in item: - default_values = item["__default_values__"] - else: - _template.append(item) - template = _template + # 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: @@ -46,21 +80,55 @@ def _fill_schema_template_data( 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: - output.append(_fill_schema_template_data( - item, template_data, required_keys, missing_keys - )) + # 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, required_keys, missing_keys + 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) @@ -76,7 +144,8 @@ def _fill_schema_template_data( else: # Only replace the key in string template = template.replace(replacement_string, value) - output = template + + output = template.replace("__dbcb__", "{").replace("__decb__", "}") else: output = template @@ -105,11 +174,15 @@ def _fill_schema_template(child_data, schema_collection, schema_templates): 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 + template, single_template_data, skip_paths ) except SchemaTemplateMissingKeys as exc: @@ -166,7 +239,7 @@ def _fill_inner_schemas(schema_data, schema_collection, schema_templates): schema_templates ) - elif child_type == "schema_template": + elif child_type in ("template", "schema_template"): for filled_child in _fill_schema_template( child, schema_collection, schema_templates ): diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index b89473d9fb..401d3980c9 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -55,6 +55,8 @@ class RootEntity(BaseItemEntity): def __init__(self, schema_data, reset): super(RootEntity, self).__init__(schema_data) + self._require_restart_callbacks = [] + self._item_ids_require_restart = set() self._item_initalization() if reset: self.reset() @@ -64,6 +66,31 @@ class RootEntity(BaseItemEntity): """Current OverrideState.""" return self._override_state + @property + def require_restart(self): + return bool(self._item_ids_require_restart) + + def add_require_restart_change_callback(self, callback): + self._require_restart_callbacks.append(callback) + + def _on_require_restart_change(self): + for callback in self._require_restart_callbacks: + callback() + + def add_item_require_restart(self, item): + was_empty = len(self._item_ids_require_restart) == 0 + self._item_ids_require_restart.add(item.id) + if was_empty: + self._on_require_restart_change() + + def remove_item_require_restart(self, item): + if item.id not in self._item_ids_require_restart: + return + + self._item_ids_require_restart.remove(item.id) + if not self._item_ids_require_restart: + self._on_require_restart_change() + @abstractmethod def reset(self): """Reset values and entities to initial state. diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index e77f13d351..64c5a7f366 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -78,6 +78,10 @@ "type": "schema", "name": "schema_project_hiero" }, + { + "type": "schema", + "name": "schema_project_blender" + }, { "type": "schema", "name": "schema_project_aftereffects" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 63bf9274a3..8024de6d45 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -85,6 +85,14 @@ ] } ] + }, + { + "type": "schema_template", + "name": "template_workfile_options", + "skip_paths": [ + "workfile_builder/builder_on_start", + "workfile_builder/profiles" + ] } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json new file mode 100644 index 0000000000..af09329a03 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -0,0 +1,17 @@ +{ + "type": "dict", + "collapsible": true, + "key": "blender", + "label": "Blender", + "is_file": true, + "children": [ + { + "type": "schema_template", + "name": "template_workfile_options", + "skip_paths": [ + "workfile_builder/builder_on_start", + "workfile_builder/profiles" + ] + } + ] +} diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 5022b75719..f709e84651 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -43,16 +43,6 @@ "label": "Build Workfile" } ] - }, - { - "type": "boolean", - "key": "open_workfile_at_start", - "label": "Open Workfile window at start of a Nuke session" - }, - { - "type": "boolean", - "key": "create_initial_workfile", - "label": "Create initial workfile version if none available" } ] }, @@ -103,8 +93,8 @@ "template_data": [] }, { - "type": "schema", - "name": "schema_workfile_build" + "type": "schema_template", + "name": "template_workfile_options" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 3a20b4e79c..4eb6c26dbb 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -52,6 +52,14 @@ ] } ] + }, + { + "type": "schema_template", + "name": "template_workfile_options", + "skip_paths": [ + "workfile_builder/builder_on_start", + "workfile_builder/profiles" + ] } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 00080f8247..6f90bb4263 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -112,6 +112,14 @@ } ] }, + { + "type": "schema_template", + "name": "template_workfile_options", + "skip_paths": [ + "workfile_builder/builder_on_start", + "workfile_builder/profiles" + ] + }, { "type": "schema", "name": "schema_publish_gui_filter" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 11b95862fa..0c89575d74 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -173,6 +173,15 @@ { "type": "separator" }, + { + "type": "label", + "label": "Crop input overscan. See the documentation for more information." + }, + { + "type": "text", + "key": "overscan_crop", + "label": "Overscan crop" + }, { "type": "label", "label": "Width and Height must be both set to higher value than 0 else source resolution is used." diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json new file mode 100644 index 0000000000..d6b81c8687 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -0,0 +1,471 @@ +{ + "type": "dict", + "collapsible": true, + "key": "ExtractPlayblast", + "label": "Extract Playblast settings", + "children": [ + { + "type": "dict", + "key": "capture_preset", + "children": [ + { + "type": "dict", + "key": "Codec", + "children": [ + { + "type": "label", + "label": "Codec" + }, + { + "type": "text", + "key": "compression", + "label": "Compression type" + }, + { + "type": "text", + "key": "format", + "label": "Data format" + }, + { + "type": "number", + "key": "quality", + "label": "Quality", + "decimal": 0, + "minimum": 0, + "maximum": 100 + }, + + { + "type": "splitter" + } + ] + }, + { + "type": "dict", + "key": "Display Options", + "children": [ + { + "type": "label", + "label": "Display Options" + }, + + { + "type": "color", + "key": "background", + "label": "Background Color: " + }, + { + "type": "color", + "key": "backgroundBottom", + "label": "Background Bottom: " + }, + { + "type": "color", + "key": "backgroundTop", + "label": "Background Top: " + }, + { + "type": "boolean", + "key": "override_display", + "label": "Override display options" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Generic", + "children": [ + { + "type": "label", + "label": "Generic" + }, + { + "type": "boolean", + "key": "isolate_view", + "label": " Isolate view" + }, + { + "type": "boolean", + "key": "off_screen", + "label": " Off Screen" + } + ] + }, + + { + "type": "dict", + "key": "PanZoom", + "children": [ + { + "type": "boolean", + "key": "pan_zoom", + "label": " Pan Zoom" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Renderer", + "children": [ + { + "type": "label", + "label": "Renderer" + }, + { + "type": "enum", + "key": "rendererName", + "label": "Renderer name", + "enum_items": [ + { "vp2Renderer": "Viewport 2.0" } + ] + } + ] + }, + { + "type": "dict", + "key": "Resolution", + "children": [ + { + "type": "splitter" + }, + { + "type": "label", + "label": "Resolution" + }, + { + "type": "number", + "key": "width", + "label": " Width", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + }, + { + "type": "number", + "key": "height", + "label": "Height", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + }, + { + "type": "number", + "key": "percent", + "label": "percent", + "decimal": 1, + "minimum": 0, + "maximum": 200 + }, + { + "type": "text", + "key": "mode", + "label": "Mode" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "collapsible": true, + "key": "Viewport Options", + "label": "Viewport Options", + "children": [ + { + "type": "boolean", + "key": "override_viewport_options", + "label": "override_viewport_options" + }, + { + "type": "enum", + "key": "displayLights", + "label": "Display Lights", + "enum_items": [ + { "default": "Default Lighting"}, + { "all": "All Lights"}, + { "selected": "Selected Lights"}, + { "flat": "Flat Lighting"}, + { "nolights": "No Lights"} + ] + }, + { + "type": "number", + "key": "textureMaxResolution", + "label": "Texture Clamp Resolution", + "decimal": 0 + }, + { + "type": "number", + "key": "multiSample", + "label": "Anti Aliasing Samples", + "decimal": 0, + "minimum": 0, + "maximum": 32 + }, + { + "type": "boolean", + "key": "shadows", + "label": "Display Shadows" + }, + { + "type": "boolean", + "key": "textures", + "label": "Display Textures" + }, + { + "type": "boolean", + "key": "twoSidedLighting", + "label": "Two Sided Lighting" + }, + { + "type": "boolean", + "key": "ssaoEnable", + "label": "Screen Space Ambient Occlusion" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "cameras", + "label": "cameras" + }, + { + "type": "boolean", + "key": "clipGhosts", + "label": "clipGhosts" + }, + { + "type": "boolean", + "key": "controlVertices", + "label": "controlVertices" + }, + { + "type": "boolean", + "key": "deformers", + "label": "deformers" + }, + { + "type": "boolean", + "key": "dimensions", + "label": "dimensions" + }, + { + "type": "boolean", + "key": "dynamicConstraints", + "label": "dynamicConstraints" + }, + { + "type": "boolean", + "key": "dynamics", + "label": "dynamics" + }, + { + "type": "boolean", + "key": "fluids", + "label": "fluids" + }, + { + "type": "boolean", + "key": "follicles", + "label": "follicles" + }, + { + "type": "boolean", + "key": "gpuCacheDisplayFilter", + "label": "gpuCacheDisplayFilter" + }, + { + "type": "boolean", + "key": "greasePencils", + "label": "greasePencils" + }, + { + "type": "boolean", + "key": "grid", + "label": "grid" + }, + { + "type": "boolean", + "key": "hairSystems", + "label": "hairSystems" + }, + { + "type": "boolean", + "key": "handles", + "label": "handles" + }, + { + "type": "boolean", + "key": "hud", + "label": "hud" + }, + { + "type": "boolean", + "key": "hulls", + "label": "hulls" + }, + { + "type": "boolean", + "key": "ikHandles", + "label": "ikHandles" + }, + { + "type": "boolean", + "key": "imagePlane", + "label": "imagePlane" + }, + { + "type": "boolean", + "key": "joints", + "label": "joints" + }, + { + "type": "boolean", + "key": "lights", + "label": "lights" + }, + { + "type": "boolean", + "key": "locators", + "label": "locators" + }, + { + "type": "boolean", + "key": "manipulators", + "label": "manipulators" + }, + { + "type": "boolean", + "key": "motionTrails", + "label": "motionTrails" + }, + { + "type": "boolean", + "key": "nCloths", + "label": "nCloths" + }, + { + "type": "boolean", + "key": "nParticles", + "label": "nParticles" + }, + { + "type": "boolean", + "key": "nRigids", + "label": "nRigids" + }, + { + "type": "boolean", + "key": "nurbsCurves", + "label": "nurbsCurves" + }, + { + "type": "boolean", + "key": "nurbsSurfaces", + "label": "nurbsSurfaces" + }, + { + "type": "boolean", + "key": "particleInstancers", + "label": "particleInstancers" + }, + { + "type": "boolean", + "key": "pivots", + "label": "pivots" + }, + { + "type": "boolean", + "key": "planes", + "label": "planes" + }, + { + "type": "boolean", + "key": "pluginShapes", + "label": "pluginShapes" + }, + { + "type": "boolean", + "key": "polymeshes", + "label": "polymeshes" + }, + { + "type": "boolean", + "key": "strokes", + "label": "strokes" + }, + { + "type": "boolean", + "key": "subdivSurfaces", + "label": "subdivSurfaces" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "Camera Options", + "label": "Camera Options", + "children": [ + { + "type": "boolean", + "key": "displayGateMask", + "label": "displayGateMask" + }, + { + "type": "boolean", + "key": "displayResolution", + "label": "displayResolution" + }, + { + "type": "boolean", + "key": "displayFilmGate", + "label": "displayFilmGate" + }, + { + "type": "boolean", + "key": "displayFieldChart", + "label": "displayFieldChart" + }, + { + "type": "boolean", + "key": "displaySafeAction", + "label": "displaySafeAction" + }, + { + "type": "boolean", + "key": "displaySafeTitle", + "label": "displaySafeTitle" + }, + { + "type": "boolean", + "key": "displayFilmPivot", + "label": "displayFilmPivot" + }, + { + "type": "boolean", + "key": "displayFilmOrigin", + "label": "displayFilmOrigin" + }, + { + "type": "number", + "key": "overscan", + "label": "overscan", + "decimal": 1, + "minimum": 0, + "maximum": 10 + } + ] + } + ] + } + ] +} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json index dd9d0508b4..0b09d08700 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json @@ -11,144 +11,74 @@ "label": "Loaded Subsets Outliner Colors", "children": [ { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "Model", - "name": "model" - } - ] + "type": "color", + "label": "Model:", + "key": "model" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "Rig", - "name": "rig" - } - ] + "type": "color", + "label": "Rig:", + "key": "rig" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "Pointcache", - "name": "pointcache" - } - ] + "type": "color", + "label": "Pointcache:", + "key": "pointcache" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "Animation", - "name": "animation" - } - ] + "type": "color", + "label": "Animation:", + "key": "animation" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "Arnold Standin", - "name": "ass" - } - ] + "type": "color", + "label": "Arnold Standin:", + "key": "ass" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "Camera", - "name": "camera" - } - ] + "type": "color", + "label": "Camera:", + "key": "camera" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "FBX", - "name": "fbx" - } - ] + "type": "color", + "label": "FBX:", + "key": "fbx" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "Maya Scene", - "name": "mayaAscii" - } - ] + "type": "color", + "label": "Maya Scene:", + "key": "mayaAscii" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "Set Dress", - "name": "setdress" - } - ] + "type": "color", + "label": "Set Dress:", + "key": "setdress" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "Layout", - "name": "layout" - } - ] + "type": "color", + "label": "Layout:", + "key": "layout" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "VDB Cache", - "name": "vdbcache" - } - ] + "type": "color", + "label": "VDB Cache:", + "key": "vdbcache" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "Vray Proxy", - "name": "vrayproxy" - } - ] + "type": "color", + "label": "Vray Proxy:", + "key": "vrayproxy" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "Yeti Cache", - "name": "yeticache" - } - ] + "type": "color", + "label": "Yeti Cache:", + "key": "yeticache" }, { - "type": "schema_template", - "name": "template_color", - "template_data": [ - { - "label": "Yeti Rig", - "name": "yetiRig" - } - ] + "type": "color", + "label": "Yeti Rig:", + "key": "yetiRig" } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 4cabf5bb74..0abcdd2965 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -297,8 +297,8 @@ "label": "Extractors" }, { - "type": "schema_template", - "name": "template_maya_capture" + "type": "schema", + "name": "schema_maya_capture" }, { "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json index 0cb36c2f92..078bb81bba 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json @@ -94,4 +94,4 @@ } } ] -} +} \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_color.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_color.json index 04ce055525..af8fd9dae4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_color.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_color.json @@ -2,7 +2,7 @@ { "type": "list-strict", "key": "{name}", - "label": "{label}:", + "label": "{label}", "object_types": [ { "label": "Red", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json deleted file mode 100644 index e4e0b034dd..0000000000 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json +++ /dev/null @@ -1,541 +0,0 @@ -[ - { - "type": "dict", - "collapsible": true, - "key": "ExtractPlayblast", - "label": "Extract Playblast settings", - "children": [ - { - "type": "dict", - "key": "capture_preset", - "children": [ - { - "type": "dict", - "key": "Codec", - "children": [ - { - "type": "label", - "label": "Codec" - }, - { - "type": "text", - "key": "compression", - "label": "Compression type" - }, - { - "type": "text", - "key": "format", - "label": "Data format" - }, - { - "type": "number", - "key": "quality", - "label": "Quality", - "decimal": 0, - "minimum": 0, - "maximum": 100 - }, - - { - "type": "splitter" - } - ] - }, - { - "type": "dict", - "key": "Display Options", - "children": [ - { - "type": "label", - "label": "Display Options" - }, - { - "type": "list-strict", - "key": "background", - "label": "Background Color: ", - "object_types": [ - { - "label": "Red", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Green", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Blue", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - } - ] - }, - { - "type": "list-strict", - "key": "backgroundBottom", - "label": "Background Bottom: ", - "object_types": [ - { - "label": "Red", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Green", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Blue", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - } - ] - }, - { - "type": "list-strict", - "key": "backgroundTop", - "label": "Background Top: ", - "object_types": [ - { - "label": "Red", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Green", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Blue", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - } - ] - }, - { - "type": "boolean", - "key": "override_display", - "label": "Override display options" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "key": "Generic", - "children": [ - { - "type": "label", - "label": "Generic" - }, - { - "type": "boolean", - "key": "isolate_view", - "label": " Isolate view" - }, - { - "type": "boolean", - "key": "off_screen", - "label": " Off Screen" - } - ] - }, - - { - "type": "dict", - "key": "PanZoom", - "children": [ - { - "type": "boolean", - "key": "pan_zoom", - "label": " Pan Zoom" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "key": "Renderer", - "children": [ - { - "type": "label", - "label": "Renderer" - }, - { - "type": "enum", - "key": "rendererName", - "label": "Renderer name", - "enum_items": [ - { "vp2Renderer": "Viewport 2.0" } - ] - } - ] - }, - { - "type": "dict", - "key": "Resolution", - "children": [ - { - "type": "splitter" - }, - { - "type": "label", - "label": "Resolution" - }, - { - "type": "number", - "key": "width", - "label": " Width", - "decimal": 0, - "minimum": 0, - "maximum": 99999 - }, - { - "type": "number", - "key": "height", - "label": "Height", - "decimal": 0, - "minimum": 0, - "maximum": 99999 - }, - { - "type": "number", - "key": "percent", - "label": "percent", - "decimal": 1, - "minimum": 0, - "maximum": 200 - }, - { - "type": "text", - "key": "mode", - "label": "Mode" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "collapsible": true, - "key": "Viewport Options", - "label": "Viewport Options", - "children": [ - { - "type": "boolean", - "key": "override_viewport_options", - "label": "override_viewport_options" - }, - { - "type": "enum", - "key": "displayLights", - "label": "Display Lights", - "enum_items": [ - { "default": "Default Lighting"}, - { "all": "All Lights"}, - { "selected": "Selected Lights"}, - { "flat": "Flat Lighting"}, - { "nolights": "No Lights"} - ] - }, - { - "type": "number", - "key": "textureMaxResolution", - "label": "Texture Clamp Resolution", - "decimal": 0 - }, - { - "type": "number", - "key": "multiSample", - "label": "Anti Aliasing Samples", - "decimal": 0, - "minimum": 0, - "maximum": 32 - }, - { - "type": "boolean", - "key": "shadows", - "label": "Display Shadows" - }, - { - "type": "boolean", - "key": "textures", - "label": "Display Textures" - }, - { - "type": "boolean", - "key": "twoSidedLighting", - "label": "Two Sided Lighting" - }, - { - "type": "boolean", - "key": "ssaoEnable", - "label": "Screen Space Ambient Occlusion" - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "cameras", - "label": "cameras" - }, - { - "type": "boolean", - "key": "clipGhosts", - "label": "clipGhosts" - }, - { - "type": "boolean", - "key": "controlVertices", - "label": "controlVertices" - }, - { - "type": "boolean", - "key": "deformers", - "label": "deformers" - }, - { - "type": "boolean", - "key": "dimensions", - "label": "dimensions" - }, - { - "type": "boolean", - "key": "dynamicConstraints", - "label": "dynamicConstraints" - }, - { - "type": "boolean", - "key": "dynamics", - "label": "dynamics" - }, - { - "type": "boolean", - "key": "fluids", - "label": "fluids" - }, - { - "type": "boolean", - "key": "follicles", - "label": "follicles" - }, - { - "type": "boolean", - "key": "gpuCacheDisplayFilter", - "label": "gpuCacheDisplayFilter" - }, - { - "type": "boolean", - "key": "greasePencils", - "label": "greasePencils" - }, - { - "type": "boolean", - "key": "grid", - "label": "grid" - }, - { - "type": "boolean", - "key": "hairSystems", - "label": "hairSystems" - }, - { - "type": "boolean", - "key": "handles", - "label": "handles" - }, - { - "type": "boolean", - "key": "hud", - "label": "hud" - }, - { - "type": "boolean", - "key": "hulls", - "label": "hulls" - }, - { - "type": "boolean", - "key": "ikHandles", - "label": "ikHandles" - }, - { - "type": "boolean", - "key": "imagePlane", - "label": "imagePlane" - }, - { - "type": "boolean", - "key": "joints", - "label": "joints" - }, - { - "type": "boolean", - "key": "lights", - "label": "lights" - }, - { - "type": "boolean", - "key": "locators", - "label": "locators" - }, - { - "type": "boolean", - "key": "manipulators", - "label": "manipulators" - }, - { - "type": "boolean", - "key": "motionTrails", - "label": "motionTrails" - }, - { - "type": "boolean", - "key": "nCloths", - "label": "nCloths" - }, - { - "type": "boolean", - "key": "nParticles", - "label": "nParticles" - }, - { - "type": "boolean", - "key": "nRigids", - "label": "nRigids" - }, - { - "type": "boolean", - "key": "nurbsCurves", - "label": "nurbsCurves" - }, - { - "type": "boolean", - "key": "nurbsSurfaces", - "label": "nurbsSurfaces" - }, - { - "type": "boolean", - "key": "particleInstancers", - "label": "particleInstancers" - }, - { - "type": "boolean", - "key": "pivots", - "label": "pivots" - }, - { - "type": "boolean", - "key": "planes", - "label": "planes" - }, - { - "type": "boolean", - "key": "pluginShapes", - "label": "pluginShapes" - }, - { - "type": "boolean", - "key": "polymeshes", - "label": "polymeshes" - }, - { - "type": "boolean", - "key": "strokes", - "label": "strokes" - }, - { - "type": "boolean", - "key": "subdivSurfaces", - "label": "subdivSurfaces" - } - ] - }, - { - "type": "dict", - "collapsible": true, - "key": "Camera Options", - "label": "Camera Options", - "children": [ - { - "type": "boolean", - "key": "displayGateMask", - "label": "displayGateMask" - }, - { - "type": "boolean", - "key": "displayResolution", - "label": "displayResolution" - }, - { - "type": "boolean", - "key": "displayFilmGate", - "label": "displayFilmGate" - }, - { - "type": "boolean", - "key": "displayFieldChart", - "label": "displayFieldChart" - }, - { - "type": "boolean", - "key": "displaySafeAction", - "label": "displaySafeAction" - }, - { - "type": "boolean", - "key": "displaySafeTitle", - "label": "displaySafeTitle" - }, - { - "type": "boolean", - "key": "displayFilmPivot", - "label": "displayFilmPivot" - }, - { - "type": "boolean", - "key": "displayFilmOrigin", - "label": "displayFilmOrigin" - }, - { - "type": "number", - "key": "overscan", - "label": "overscan", - "decimal": 1, - "minimum": 0, - "maximum": 10 - } - ] - } - ] - } - ] - } -] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_builder_simple.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_builder_simple.json new file mode 100644 index 0000000000..49cf9ca94a --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_builder_simple.json @@ -0,0 +1,23 @@ +[ + { + "type": "dict", + "collapsible": true, + "key": "workfile_builder", + "label": "Workfile Builder", + "children": [ + { + "type": "boolean", + "key": "create_first_version", + "label": "Create first workfile", + "default": false + }, + { + "type": "path", + "key": "template_path", + "label": "First workfile template", + "multiplatform": true, + "multipath": false + } + ] + } +] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_options.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_options.json new file mode 100644 index 0000000000..815df85879 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_options.json @@ -0,0 +1,145 @@ +[{ + "type": "dict", + "collapsible": true, + "key": "workfile_builder", + "label": "Workfile Builder", + "children": [ + { + "type": "boolean", + "key": "create_first_version", + "label": "Create first workfile", + "default": false + }, + { + "type": "list", + "key": "custom_templates", + "label": "Custom templates", + "is_group": true, + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "task-types-enum", + "key": "task_types", + "label": "Task types" + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Absolute path to workfile template or OpenPype Anatomy text is accepted." + }, + { + "type": "path", + "key": "path", + "label": "Path", + "multiplatform": true, + "multipath": false + } + ] + } + }, + { + "type": "boolean", + "key": "builder_on_start", + "label": "Run Builder Profiles on first launch", + "default": false + }, + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "tasks", + "label": "Tasks", + "type": "list", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "key": "current_context", + "label": "Current Context", + "type": "list", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "subset_name_filters", + "label": "Subset name Filters", + "type": "list", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "repre_names", + "label": "Repre Names", + "type": "list", + "object_type": "text" + }, + { + "key": "loaders", + "label": "Loaders", + "type": "list", + "object_type": "text" + } + ] + } + }, + { + "type": "separator" + }, + { + "key": "linked_assets", + "label": "Linked Assets/Shots", + "type": "list", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "subset_name_filters", + "label": "Subset name Filters", + "type": "list", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "repre_names", + "label": "Repre Names", + "type": "list", + "object_type": "text" + }, + { + "key": "loaders", + "label": "Loaders", + "type": "list", + "object_type": "text" + } + ] + } + } + ] + } + } + ] +} +] diff --git a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json index 50ec330a11..5f659522c3 100644 --- a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json +++ b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json @@ -3,6 +3,7 @@ "key": "ftrack", "label": "Ftrack", "collapsible": true, + "require_restart": true, "checkbox_key": "enabled", "children": [ { diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index 568ccad5b9..fe5a8d8203 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -34,7 +34,8 @@ "key": "environment", "label": "Environment", "type": "raw-json", - "env_group_key": "global" + "env_group_key": "global", + "require_restart": true }, { "type": "splitter" @@ -44,7 +45,8 @@ "key": "openpype_path", "label": "Versions Repository", "multiplatform": true, - "multipath": true + "multipath": true, + "require_restart": true } ] } diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index b643293c87..d6527f368d 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -10,6 +10,7 @@ "key": "avalon", "label": "Avalon", "collapsible": true, + "require_restart": true, "children": [ { "type": "number", @@ -35,6 +36,7 @@ "key": "timers_manager", "label": "Timers Manager", "collapsible": true, + "require_restart": true, "checkbox_key": "enabled", "children": [ { @@ -66,6 +68,7 @@ "key": "clockify", "label": "Clockify", "collapsible": true, + "require_restart": true, "checkbox_key": "enabled", "children": [ { @@ -84,6 +87,7 @@ "key": "sync_server", "label": "Site Sync", "collapsible": true, + "require_restart": true, "checkbox_key": "enabled", "children": [ { @@ -114,6 +118,7 @@ "type": "dict", "key": "deadline", "label": "Deadline", + "require_restart": true, "collapsible": true, "checkbox_key": "enabled", "children": [ @@ -133,6 +138,7 @@ "type": "dict", "key": "muster", "label": "Muster", + "require_restart": true, "collapsible": true, "checkbox_key": "enabled", "children": [ @@ -186,6 +192,20 @@ "label": "Enabled" } ] + }, + { + "type": "dict", + "key": "project_manager", + "label": "Project Manager (beta)", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] } ] } diff --git a/openpype/style/__init__.py b/openpype/style/__init__.py new file mode 100644 index 0000000000..89a210bee9 --- /dev/null +++ b/openpype/style/__init__.py @@ -0,0 +1,93 @@ +import os +import json +import collections +from openpype import resources + + +_STYLESHEET_CACHE = None +_FONT_IDS = None + +current_dir = os.path.dirname(os.path.abspath(__file__)) + + +def _load_stylesheet(): + from . import qrc_resources + + qrc_resources.qInitResources() + + style_path = os.path.join(current_dir, "style.css") + with open(style_path, "r") as style_file: + stylesheet = style_file.read() + + data_path = os.path.join(current_dir, "data.json") + with open(data_path, "r") as data_stream: + data = json.load(data_stream) + + data_deque = collections.deque() + for item in data.items(): + data_deque.append(item) + + fill_data = {} + while data_deque: + key, value = data_deque.popleft() + if isinstance(value, dict): + for sub_key, sub_value in value.items(): + new_key = "{}:{}".format(key, sub_key) + data_deque.append((new_key, sub_value)) + continue + fill_data[key] = value + + for key, value in fill_data.items(): + replacement_key = "{" + key + "}" + stylesheet = stylesheet.replace(replacement_key, value) + return stylesheet + + +def _load_font(): + from Qt import QtGui + + global _FONT_IDS + + # Check if font ids are still loaded + if _FONT_IDS is not None: + for font_id in tuple(_FONT_IDS): + font_families = QtGui.QFontDatabase.applicationFontFamilies( + font_id + ) + # Reset font if font id is not available + if not font_families: + _FONT_IDS = None + break + + if _FONT_IDS is None: + _FONT_IDS = [] + fonts_dirpath = os.path.join(current_dir, "fonts") + font_dirs = [] + font_dirs.append(os.path.join(fonts_dirpath, "Montserrat")) + font_dirs.append(os.path.join(fonts_dirpath, "Spartan")) + + loaded_fonts = [] + for font_dir in font_dirs: + for filename in os.listdir(font_dir): + if os.path.splitext(filename)[1] not in [".ttf"]: + continue + full_path = os.path.join(font_dir, filename) + font_id = QtGui.QFontDatabase.addApplicationFont(full_path) + _FONT_IDS.append(font_id) + font_families = QtGui.QFontDatabase.applicationFontFamilies( + font_id + ) + loaded_fonts.extend(font_families) + print("Registered font families: {}".format(", ".join(loaded_fonts))) + + +def load_stylesheet(): + global _STYLESHEET_CACHE + if _STYLESHEET_CACHE is None: + _STYLESHEET_CACHE = _load_stylesheet() + _load_font() + return _STYLESHEET_CACHE + + +def app_icon_path(): + return resources.pype_icon_filepath() diff --git a/openpype/style/data.json b/openpype/style/data.json new file mode 100644 index 0000000000..a58829d946 --- /dev/null +++ b/openpype/style/data.json @@ -0,0 +1,52 @@ +{ + "palette": { + "grey-base": "#2C313A", + "grey-light": "#373D48", + "grey-lighter": "#434a56", + "grey-lightest": "#4E5565", + "grey-input": "#353A45", + "grey-dark": "#21252B", + + "text-darker": "#99A3B2", + "text-base": "#D3D8DE", + "text-lighter": "#F0F2F5", + + "blue-base": "hsl(200, 60%, 60%)", + "blue-light": "hsl(200, 80%, 80%)", + + "green-base": "hsl(155, 55%, 55%)", + "green-light": "hsl(155, 80%, 80%)" + }, + "color": { + + "font": "#D3D8DE", + "font-hover": "#F0F2F5", + "font-disabled": "#99A3B2", + "font-view-selection": "#ffffff", + "font-view-hover": "#F0F2F5", + + "bg": "#2C313A", + "bg-inputs": "#21252B", + "bg-buttons": "#434a56", + "bg-button-hover": "hsla(220, 14%, 70%, .3)", + "bg-inputs-disabled": "#2C313A", + "bg-buttons-disabled": "#434a56", + + "bg-menu-separator": "rgba(75, 83, 98, 127)", + + "bg-scroll-handle": "#4B5362", + + "bg-view": "#21252B", + "bg-view-header": "#373D48", + "bg-view-hover": "hsla(220, 14%, 70%, .3)", + "bg-view-alternate": "rgb(36, 42, 50)", + "bg-view-disabled": "#434a56", + "bg-view-alternate-disabled": "#2C313A", + "bg-view-selection": "hsla(200, 60%, 60%, .4)", + "bg-view-selection-hover": "hsla(200, 60%, 60%, .8)", + + "border": "#373D48", + "border-hover": "hsla(220, 14%, 70%, .3)", + "border-focus": "hsl(200, 60%, 60%)" + } +} diff --git a/openpype/style/fonts/Montserrat/Montserrat-Black.ttf b/openpype/style/fonts/Montserrat/Montserrat-Black.ttf new file mode 100644 index 0000000000..437b1157cb Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-Black.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-BlackItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-BlackItalic.ttf new file mode 100644 index 0000000000..52348354c2 Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-BlackItalic.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-Bold.ttf b/openpype/style/fonts/Montserrat/Montserrat-Bold.ttf new file mode 100644 index 0000000000..221819bca0 Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-Bold.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-BoldItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-BoldItalic.ttf new file mode 100644 index 0000000000..9ae2bd240f Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-BoldItalic.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-ExtraBold.ttf b/openpype/style/fonts/Montserrat/Montserrat-ExtraBold.ttf new file mode 100644 index 0000000000..80ea8061b0 Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-ExtraBold.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf new file mode 100644 index 0000000000..6c961e1cc9 Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-ExtraLight.ttf b/openpype/style/fonts/Montserrat/Montserrat-ExtraLight.ttf new file mode 100644 index 0000000000..ca0bbb6569 Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-ExtraLight.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf new file mode 100644 index 0000000000..f3c1559ec7 Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-Italic.ttf b/openpype/style/fonts/Montserrat/Montserrat-Italic.ttf new file mode 100644 index 0000000000..eb4232a0c2 Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-Italic.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-Light.ttf b/openpype/style/fonts/Montserrat/Montserrat-Light.ttf new file mode 100644 index 0000000000..990857de8e Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-Light.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-LightItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-LightItalic.ttf new file mode 100644 index 0000000000..209604046b Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-LightItalic.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-Medium.ttf b/openpype/style/fonts/Montserrat/Montserrat-Medium.ttf new file mode 100644 index 0000000000..6e079f6984 Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-Medium.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-MediumItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-MediumItalic.ttf new file mode 100644 index 0000000000..0dc3ac9c29 Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-MediumItalic.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-Regular.ttf b/openpype/style/fonts/Montserrat/Montserrat-Regular.ttf new file mode 100644 index 0000000000..8d443d5d56 Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-Regular.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-SemiBold.ttf b/openpype/style/fonts/Montserrat/Montserrat-SemiBold.ttf new file mode 100644 index 0000000000..f8a43f2b20 Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-SemiBold.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf new file mode 100644 index 0000000000..336c56ec0c Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-Thin.ttf b/openpype/style/fonts/Montserrat/Montserrat-Thin.ttf new file mode 100644 index 0000000000..b9858757eb Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-Thin.ttf differ diff --git a/openpype/style/fonts/Montserrat/Montserrat-ThinItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-ThinItalic.ttf new file mode 100644 index 0000000000..e488998ec7 Binary files /dev/null and b/openpype/style/fonts/Montserrat/Montserrat-ThinItalic.ttf differ diff --git a/openpype/style/fonts/Montserrat/OFL.txt b/openpype/style/fonts/Montserrat/OFL.txt new file mode 100644 index 0000000000..f435ed8b5e --- /dev/null +++ b/openpype/style/fonts/Montserrat/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/openpype/style/fonts/Spartan/OFL.txt b/openpype/style/fonts/Spartan/OFL.txt new file mode 100644 index 0000000000..808b610ffd --- /dev/null +++ b/openpype/style/fonts/Spartan/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Spartan Project Authors (https://github.com/bghryct/Spartan-MB) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/openpype/style/fonts/Spartan/README.txt b/openpype/style/fonts/Spartan/README.txt new file mode 100644 index 0000000000..9db64aff0b --- /dev/null +++ b/openpype/style/fonts/Spartan/README.txt @@ -0,0 +1,71 @@ +Spartan Variable Font +===================== + +This download contains Spartan as both a variable font and static fonts. + +Spartan is a variable font with this axis: + wght + +This means all the styles are contained in a single file: + Spartan-VariableFont_wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Spartan: + static/Spartan-Thin.ttf + static/Spartan-ExtraLight.ttf + static/Spartan-Light.ttf + static/Spartan-Regular.ttf + static/Spartan-Medium.ttf + static/Spartan-SemiBold.ttf + static/Spartan-Bold.ttf + static/Spartan-ExtraBold.ttf + static/Spartan-Black.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them freely in your products & projects - print or digital, +commercial or otherwise. However, you can't sell the fonts on their own. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/openpype/style/fonts/Spartan/Spartan-Black.ttf b/openpype/style/fonts/Spartan/Spartan-Black.ttf new file mode 100644 index 0000000000..5d3147011e Binary files /dev/null and b/openpype/style/fonts/Spartan/Spartan-Black.ttf differ diff --git a/openpype/style/fonts/Spartan/Spartan-Bold.ttf b/openpype/style/fonts/Spartan/Spartan-Bold.ttf new file mode 100644 index 0000000000..5fe4b702b2 Binary files /dev/null and b/openpype/style/fonts/Spartan/Spartan-Bold.ttf differ diff --git a/openpype/style/fonts/Spartan/Spartan-ExtraBold.ttf b/openpype/style/fonts/Spartan/Spartan-ExtraBold.ttf new file mode 100644 index 0000000000..1030b6dec0 Binary files /dev/null and b/openpype/style/fonts/Spartan/Spartan-ExtraBold.ttf differ diff --git a/openpype/style/fonts/Spartan/Spartan-ExtraLight.ttf b/openpype/style/fonts/Spartan/Spartan-ExtraLight.ttf new file mode 100644 index 0000000000..aced3a9e94 Binary files /dev/null and b/openpype/style/fonts/Spartan/Spartan-ExtraLight.ttf differ diff --git a/openpype/style/fonts/Spartan/Spartan-Light.ttf b/openpype/style/fonts/Spartan/Spartan-Light.ttf new file mode 100644 index 0000000000..3bb6efa40e Binary files /dev/null and b/openpype/style/fonts/Spartan/Spartan-Light.ttf differ diff --git a/openpype/style/fonts/Spartan/Spartan-Medium.ttf b/openpype/style/fonts/Spartan/Spartan-Medium.ttf new file mode 100644 index 0000000000..94b22ecc08 Binary files /dev/null and b/openpype/style/fonts/Spartan/Spartan-Medium.ttf differ diff --git a/openpype/style/fonts/Spartan/Spartan-Regular.ttf b/openpype/style/fonts/Spartan/Spartan-Regular.ttf new file mode 100644 index 0000000000..7560322e3f Binary files /dev/null and b/openpype/style/fonts/Spartan/Spartan-Regular.ttf differ diff --git a/openpype/style/fonts/Spartan/Spartan-SemiBold.ttf b/openpype/style/fonts/Spartan/Spartan-SemiBold.ttf new file mode 100644 index 0000000000..7a5f74adb3 Binary files /dev/null and b/openpype/style/fonts/Spartan/Spartan-SemiBold.ttf differ diff --git a/openpype/style/fonts/Spartan/Spartan-Thin.ttf b/openpype/style/fonts/Spartan/Spartan-Thin.ttf new file mode 100644 index 0000000000..4caa0b2be9 Binary files /dev/null and b/openpype/style/fonts/Spartan/Spartan-Thin.ttf differ diff --git a/openpype/style/fonts/Spartan/Spartan-VariableFont_wght.ttf b/openpype/style/fonts/Spartan/Spartan-VariableFont_wght.ttf new file mode 100644 index 0000000000..b2dd7c3076 Binary files /dev/null and b/openpype/style/fonts/Spartan/Spartan-VariableFont_wght.ttf differ diff --git a/openpype/style/images/branch_closed.png b/openpype/style/images/branch_closed.png new file mode 100644 index 0000000000..135cd0b29d Binary files /dev/null and b/openpype/style/images/branch_closed.png differ diff --git a/openpype/style/images/branch_closed_on.png b/openpype/style/images/branch_closed_on.png new file mode 100644 index 0000000000..0ba35bd4ea Binary files /dev/null and b/openpype/style/images/branch_closed_on.png differ diff --git a/openpype/style/images/branch_open.png b/openpype/style/images/branch_open.png new file mode 100644 index 0000000000..1a83955306 Binary files /dev/null and b/openpype/style/images/branch_open.png differ diff --git a/openpype/style/images/branch_open_on.png b/openpype/style/images/branch_open_on.png new file mode 100644 index 0000000000..c09f5fd95d Binary files /dev/null and b/openpype/style/images/branch_open_on.png differ diff --git a/openpype/tools/project_manager/project_manager/style/images/combobox_arrow.png b/openpype/style/images/combobox_arrow.png similarity index 100% rename from openpype/tools/project_manager/project_manager/style/images/combobox_arrow.png rename to openpype/style/images/combobox_arrow.png diff --git a/openpype/tools/project_manager/project_manager/style/images/combobox_arrow_disabled.png b/openpype/style/images/combobox_arrow_disabled.png similarity index 100% rename from openpype/tools/project_manager/project_manager/style/images/combobox_arrow_disabled.png rename to openpype/style/images/combobox_arrow_disabled.png diff --git a/openpype/style/images/combobox_arrow_on.png b/openpype/style/images/combobox_arrow_on.png new file mode 100644 index 0000000000..5805d9842b Binary files /dev/null and b/openpype/style/images/combobox_arrow_on.png differ diff --git a/openpype/style/images/down_arrow.png b/openpype/style/images/down_arrow.png new file mode 100644 index 0000000000..e271f7f90b Binary files /dev/null and b/openpype/style/images/down_arrow.png differ diff --git a/openpype/style/images/down_arrow_disabled.png b/openpype/style/images/down_arrow_disabled.png new file mode 100644 index 0000000000..5805d9842b Binary files /dev/null and b/openpype/style/images/down_arrow_disabled.png differ diff --git a/openpype/style/images/down_arrow_on.png b/openpype/style/images/down_arrow_on.png new file mode 100644 index 0000000000..e271f7f90b Binary files /dev/null and b/openpype/style/images/down_arrow_on.png differ diff --git a/openpype/style/images/left_arrow.png b/openpype/style/images/left_arrow.png new file mode 100644 index 0000000000..f808d2d720 Binary files /dev/null and b/openpype/style/images/left_arrow.png differ diff --git a/openpype/style/images/left_arrow_disabled.png b/openpype/style/images/left_arrow_disabled.png new file mode 100644 index 0000000000..f5b9af8a34 Binary files /dev/null and b/openpype/style/images/left_arrow_disabled.png differ diff --git a/openpype/style/images/left_arrow_on.png b/openpype/style/images/left_arrow_on.png new file mode 100644 index 0000000000..f808d2d720 Binary files /dev/null and b/openpype/style/images/left_arrow_on.png differ diff --git a/openpype/style/images/right_arrow.png b/openpype/style/images/right_arrow.png new file mode 100644 index 0000000000..9b0a4e6a7a Binary files /dev/null and b/openpype/style/images/right_arrow.png differ diff --git a/openpype/style/images/right_arrow_disabled.png b/openpype/style/images/right_arrow_disabled.png new file mode 100644 index 0000000000..5c0bee402f Binary files /dev/null and b/openpype/style/images/right_arrow_disabled.png differ diff --git a/openpype/style/images/right_arrow_on.png b/openpype/style/images/right_arrow_on.png new file mode 100644 index 0000000000..9b0a4e6a7a Binary files /dev/null and b/openpype/style/images/right_arrow_on.png differ diff --git a/openpype/style/images/up_arrow.png b/openpype/style/images/up_arrow.png new file mode 100644 index 0000000000..abcc724521 Binary files /dev/null and b/openpype/style/images/up_arrow.png differ diff --git a/openpype/style/images/up_arrow_disabled.png b/openpype/style/images/up_arrow_disabled.png new file mode 100644 index 0000000000..b9c8e3b535 Binary files /dev/null and b/openpype/style/images/up_arrow_disabled.png differ diff --git a/openpype/style/images/up_arrow_on.png b/openpype/style/images/up_arrow_on.png new file mode 100644 index 0000000000..abcc724521 Binary files /dev/null and b/openpype/style/images/up_arrow_on.png differ diff --git a/openpype/style/pyqt5_resources.py b/openpype/style/pyqt5_resources.py new file mode 100644 index 0000000000..3dc21be12a --- /dev/null +++ b/openpype/style/pyqt5_resources.py @@ -0,0 +1,880 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + + +qt_resource_data = b"\ +\x00\x00\x00\xa0\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1c\x1f\x24\ +\xc6\x09\x17\x00\x00\x00\x24\x49\x44\x41\x54\x08\xd7\x63\x60\x40\ +\x05\xff\xcf\xc3\x58\x4c\xc8\x5c\x26\x64\x59\x26\x64\xc5\x70\x0e\ +\xa3\x21\x9c\xc3\x68\x88\x61\x1a\x0a\x00\x00\x6d\x84\x09\x75\x37\ +\x9e\xd9\x23\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x07\x30\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x00\x31\xac\xdc\x63\ +\x00\x00\x04\xb0\x69\x54\x58\x74\x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\ +\x61\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\x00\x00\x00\x3c\x3f\ +\x78\x70\x61\x63\x6b\x65\x74\x20\x62\x65\x67\x69\x6e\x3d\x22\xef\ +\xbb\xbf\x22\x20\x69\x64\x3d\x22\x57\x35\x4d\x30\x4d\x70\x43\x65\ +\x68\x69\x48\x7a\x72\x65\x53\x7a\x4e\x54\x63\x7a\x6b\x63\x39\x64\ +\x22\x3f\x3e\x0a\x3c\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x20\x78\ +\x6d\x6c\x6e\x73\x3a\x78\x3d\x22\x61\x64\x6f\x62\x65\x3a\x6e\x73\ +\x3a\x6d\x65\x74\x61\x2f\x22\x20\x78\x3a\x78\x6d\x70\x74\x6b\x3d\ +\x22\x58\x4d\x50\x20\x43\x6f\x72\x65\x20\x35\x2e\x35\x2e\x30\x22\ +\x3e\x0a\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x20\x78\x6d\x6c\x6e\ +\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\ +\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\ +\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\ +\x73\x23\x22\x3e\x0a\x20\x20\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\ +\x72\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\ +\x74\x3d\x22\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x65\ +\x78\x69\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\ +\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x65\x78\x69\x66\x2f\x31\x2e\ +\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x74\x69\ +\x66\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\ +\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x74\x69\x66\x66\x2f\x31\x2e\x30\ +\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x70\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\ +\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x70\x68\x6f\x74\ +\x6f\x73\x68\x6f\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\ +\x61\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\ +\x6e\x73\x3a\x78\x6d\x70\x4d\x4d\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\x61\ +\x70\x2f\x31\x2e\x30\x2f\x6d\x6d\x2f\x22\x0a\x20\x20\x20\x20\x78\ +\x6d\x6c\x6e\x73\x3a\x73\x74\x45\x76\x74\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\ +\x78\x61\x70\x2f\x31\x2e\x30\x2f\x73\x54\x79\x70\x65\x2f\x52\x65\ +\x73\x6f\x75\x72\x63\x65\x45\x76\x65\x6e\x74\x23\x22\x0a\x20\x20\ +\x20\x65\x78\x69\x66\x3a\x50\x69\x78\x65\x6c\x58\x44\x69\x6d\x65\ +\x6e\x73\x69\x6f\x6e\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x65\x78\ +\x69\x66\x3a\x50\x69\x78\x65\x6c\x59\x44\x69\x6d\x65\x6e\x73\x69\ +\x6f\x6e\x3d\x22\x37\x22\x0a\x20\x20\x20\x65\x78\x69\x66\x3a\x43\ +\x6f\x6c\x6f\x72\x53\x70\x61\x63\x65\x3d\x22\x31\x22\x0a\x20\x20\ +\x20\x74\x69\x66\x66\x3a\x49\x6d\x61\x67\x65\x57\x69\x64\x74\x68\ +\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x49\x6d\ +\x61\x67\x65\x4c\x65\x6e\x67\x74\x68\x3d\x22\x37\x22\x0a\x20\x20\ +\x20\x74\x69\x66\x66\x3a\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\ +\x55\x6e\x69\x74\x3d\x22\x32\x22\x0a\x20\x20\x20\x74\x69\x66\x66\ +\x3a\x58\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\ +\x2e\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x59\x52\x65\x73\ +\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\x2e\x30\x22\x0a\x20\ +\x20\x20\x70\x68\x6f\x74\x6f\x73\x68\x6f\x70\x3a\x43\x6f\x6c\x6f\ +\x72\x4d\x6f\x64\x65\x3d\x22\x33\x22\x0a\x20\x20\x20\x70\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x3a\x49\x43\x43\x50\x72\x6f\x66\x69\x6c\ +\x65\x3d\x22\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\x36\ +\x2d\x32\x2e\x31\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x6f\x64\ +\x69\x66\x79\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ +\x2d\x33\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\ +\x30\x30\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x65\x74\x61\x64\ +\x61\x74\x61\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ +\x2d\x33\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\ +\x30\x30\x22\x3e\x0a\x20\x20\x20\x3c\x78\x6d\x70\x4d\x4d\x3a\x48\ +\x69\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\ +\x3a\x53\x65\x71\x3e\x0a\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\ +\x6c\x69\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x61\ +\x63\x74\x69\x6f\x6e\x3d\x22\x70\x72\x6f\x64\x75\x63\x65\x64\x22\ +\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x73\x6f\x66\ +\x74\x77\x61\x72\x65\x41\x67\x65\x6e\x74\x3d\x22\x41\x66\x66\x69\ +\x6e\x69\x74\x79\x20\x44\x65\x73\x69\x67\x6e\x65\x72\x20\x31\x2e\ +\x39\x2e\x32\x22\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\ +\x3a\x77\x68\x65\x6e\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\x2d\x33\ +\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\x30\x30\ +\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x53\x65\ +\x71\x3e\x0a\x20\x20\x20\x3c\x2f\x78\x6d\x70\x4d\x4d\x3a\x48\x69\ +\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x3c\x2f\x72\x64\x66\x3a\x44\ +\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x3c\x2f\x72\ +\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\x78\x3a\x78\x6d\x70\x6d\ +\x65\x74\x61\x3e\x0a\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\x20\x65\ +\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x48\x8b\x5b\x5e\x00\x00\x01\x83\ +\x69\x43\x43\x50\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\ +\x36\x2d\x32\x2e\x31\x00\x00\x28\x91\x75\x91\xcf\x2b\x44\x51\x14\ +\xc7\x3f\x66\x68\xfc\x18\x8d\x62\x61\x31\x65\x12\x16\x42\x83\x12\ +\x1b\x8b\x99\x18\x0a\x8b\x99\x51\x7e\x6d\x66\x9e\x79\x33\x6a\xde\ +\x78\xbd\x37\xd2\x64\xab\x6c\xa7\x28\xb1\xf1\x6b\xc1\x5f\xc0\x56\ +\x59\x2b\x45\xa4\x64\xa7\xac\x89\x0d\x7a\xce\x9b\x51\x23\x99\x73\ +\x3b\xf7\x7c\xee\xf7\xde\x73\xba\xf7\x5c\x70\x44\xd3\x8a\x66\x56\ +\xfa\x41\xcb\x64\x8d\x70\x28\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ +\x85\x1a\x3a\xf1\xc6\x14\x53\x9f\x8c\x8c\x46\x29\x6b\xef\xb7\x54\ +\xd8\xf1\xba\xdb\xae\x55\xfe\xdc\xbf\x56\xb7\x98\x30\x15\xa8\xa8\ +\x16\x1e\x56\x74\x23\x2b\x3c\x26\x3c\xb1\x9a\xd5\x6d\xde\x12\x6e\ +\x52\x52\xb1\x45\xe1\x13\xe1\x2e\x43\x2e\x28\x7c\x63\xeb\xf1\x22\ +\x3f\xdb\x9c\x2c\xf2\xa7\xcd\x46\x34\x1c\x04\x47\x83\xb0\x2f\xf9\ +\x8b\xe3\xbf\x58\x49\x19\x9a\xb0\xbc\x9c\x36\x2d\xbd\xa2\xfc\xdc\ +\xc7\x7e\x89\x3b\x91\x99\x8e\x48\x6c\x15\xf7\x62\x12\x26\x44\x00\ +\x1f\xe3\x8c\x10\x64\x80\x5e\x86\x64\x1e\xa0\x9b\x3e\x7a\x64\x45\ +\x99\x7c\x7f\x21\x7f\x8a\x65\xc9\x55\x64\xd6\xc9\x61\xb0\x44\x92\ +\x14\x59\xba\x44\x5d\x91\xea\x09\x89\xaa\xe8\x09\x19\x69\x72\x76\ +\xff\xff\xf6\xd5\x54\xfb\xfb\x8a\xd5\xdd\x01\xa8\x7a\xb4\xac\xd7\ +\x76\x70\x6d\xc2\x57\xde\xb2\x3e\x0e\x2c\xeb\xeb\x10\x9c\x0f\x70\ +\x9e\x29\xe5\x2f\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9e\x75\ +\x38\xbd\x28\x69\xf1\x6d\x38\xdb\x80\xe6\x7b\x3d\x66\xc4\x0a\x92\ +\x53\xdc\xa1\xaa\xf0\x72\x0c\xf5\xb3\xd0\x78\x05\xb5\xf3\xc5\x9e\ +\xfd\xec\x73\x74\x07\xd1\x35\xf9\xaa\x4b\xd8\xd9\x85\x0e\x39\xef\ +\x59\xf8\x06\x8e\xfd\x67\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x97\x49\x44\x41\x54\x18\x95\x6d\xcf\xb1\x6a\x02\x41\ +\x14\x85\xe1\x6f\xb7\xb6\xd0\x27\x48\x3d\x56\x69\x03\xb1\xb4\x48\ +\x3b\x6c\xa5\xf1\x39\xf6\x59\x02\x56\x42\xba\x61\x0a\x0b\x3b\x1b\ +\x1b\x6b\x41\x18\x02\x29\x6d\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\ +\x9f\xff\x5c\xee\xa9\x62\x2a\x13\x4c\x73\x13\x6e\x46\x26\xa6\xf2\ +\x82\xae\x46\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a\x5b\x74\ +\xd8\xc7\x54\xc2\x40\x9a\x63\x8f\x3f\x7c\x55\x3d\x7c\xc5\x09\x77\ +\xbc\xa1\xc2\x19\x33\x2c\x72\x13\x2e\xd5\xe0\xc2\x12\x07\x5c\x51\ +\x23\xe0\x23\x37\xe1\xa8\x4f\x0e\x7f\xda\x60\xd7\xaf\x9f\xb9\x09\ +\xdf\x63\x05\xff\xe5\x75\x4c\x65\xf5\xcc\x1f\x0d\x33\x2c\x83\xb6\ +\x06\x44\x83\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa5\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\ +\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\ +\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\ +\xae\x42\x60\x82\ +\x00\x00\x00\xa0\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1f\x0d\xfc\ +\x52\x2b\x9c\x00\x00\x00\x24\x49\x44\x41\x54\x08\xd7\x63\x60\x40\ +\x05\x73\x3e\xc0\x58\x4c\xc8\x5c\x26\x64\x59\x26\x64\xc5\x70\x4e\ +\x8a\x00\x9c\x93\x22\x80\x61\x1a\x0a\x00\x00\x29\x95\x08\xaf\x88\ +\xac\xba\x34\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x07\xad\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x07\x00\x00\x00\x0a\x08\x06\x00\x00\x00\x78\xcc\x44\x0d\ +\x00\x00\x05\x52\x69\x54\x58\x74\x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\ +\x61\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\x00\x00\x00\x3c\x3f\ +\x78\x70\x61\x63\x6b\x65\x74\x20\x62\x65\x67\x69\x6e\x3d\x22\xef\ +\xbb\xbf\x22\x20\x69\x64\x3d\x22\x57\x35\x4d\x30\x4d\x70\x43\x65\ +\x68\x69\x48\x7a\x72\x65\x53\x7a\x4e\x54\x63\x7a\x6b\x63\x39\x64\ +\x22\x3f\x3e\x0a\x3c\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x20\x78\ +\x6d\x6c\x6e\x73\x3a\x78\x3d\x22\x61\x64\x6f\x62\x65\x3a\x6e\x73\ +\x3a\x6d\x65\x74\x61\x2f\x22\x20\x78\x3a\x78\x6d\x70\x74\x6b\x3d\ +\x22\x58\x4d\x50\x20\x43\x6f\x72\x65\x20\x35\x2e\x35\x2e\x30\x22\ +\x3e\x0a\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x20\x78\x6d\x6c\x6e\ +\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\ +\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\ +\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\ +\x73\x23\x22\x3e\x0a\x20\x20\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\ +\x72\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\ +\x74\x3d\x22\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x64\ +\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\x2e\x6f\ +\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\x6d\x65\x6e\x74\x73\x2f\x31\ +\x2e\x31\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x65\ +\x78\x69\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\ +\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x65\x78\x69\x66\x2f\x31\x2e\ +\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x74\x69\ +\x66\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\ +\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x74\x69\x66\x66\x2f\x31\x2e\x30\ +\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x70\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\ +\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x70\x68\x6f\x74\ +\x6f\x73\x68\x6f\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\ +\x61\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\ +\x6e\x73\x3a\x78\x6d\x70\x4d\x4d\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\x61\ +\x70\x2f\x31\x2e\x30\x2f\x6d\x6d\x2f\x22\x0a\x20\x20\x20\x20\x78\ +\x6d\x6c\x6e\x73\x3a\x73\x74\x45\x76\x74\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\ +\x78\x61\x70\x2f\x31\x2e\x30\x2f\x73\x54\x79\x70\x65\x2f\x52\x65\ +\x73\x6f\x75\x72\x63\x65\x45\x76\x65\x6e\x74\x23\x22\x0a\x20\x20\ +\x20\x65\x78\x69\x66\x3a\x50\x69\x78\x65\x6c\x58\x44\x69\x6d\x65\ +\x6e\x73\x69\x6f\x6e\x3d\x22\x37\x22\x0a\x20\x20\x20\x65\x78\x69\ +\x66\x3a\x50\x69\x78\x65\x6c\x59\x44\x69\x6d\x65\x6e\x73\x69\x6f\ +\x6e\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x65\x78\x69\x66\x3a\x43\ +\x6f\x6c\x6f\x72\x53\x70\x61\x63\x65\x3d\x22\x31\x22\x0a\x20\x20\ +\x20\x74\x69\x66\x66\x3a\x49\x6d\x61\x67\x65\x57\x69\x64\x74\x68\ +\x3d\x22\x37\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x49\x6d\x61\ +\x67\x65\x4c\x65\x6e\x67\x74\x68\x3d\x22\x31\x30\x22\x0a\x20\x20\ +\x20\x74\x69\x66\x66\x3a\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\ +\x55\x6e\x69\x74\x3d\x22\x32\x22\x0a\x20\x20\x20\x74\x69\x66\x66\ +\x3a\x58\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\ +\x2e\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x59\x52\x65\x73\ +\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\x2e\x30\x22\x0a\x20\ +\x20\x20\x70\x68\x6f\x74\x6f\x73\x68\x6f\x70\x3a\x43\x6f\x6c\x6f\ +\x72\x4d\x6f\x64\x65\x3d\x22\x33\x22\x0a\x20\x20\x20\x70\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x3a\x49\x43\x43\x50\x72\x6f\x66\x69\x6c\ +\x65\x3d\x22\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\x36\ +\x2d\x32\x2e\x31\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x6f\x64\ +\x69\x66\x79\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ +\x2d\x33\x31\x54\x31\x32\x3a\x34\x33\x3a\x33\x35\x2b\x30\x32\x3a\ +\x30\x30\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x65\x74\x61\x64\ +\x61\x74\x61\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ +\x2d\x33\x31\x54\x31\x32\x3a\x34\x33\x3a\x33\x35\x2b\x30\x32\x3a\ +\x30\x30\x22\x3e\x0a\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\ +\x65\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\x41\x6c\x74\x3e\ +\x0a\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\x6c\x69\x20\x78\x6d\ +\x6c\x3a\x6c\x61\x6e\x67\x3d\x22\x78\x2d\x64\x65\x66\x61\x75\x6c\ +\x74\x22\x3e\x62\x72\x61\x6e\x63\x68\x5f\x63\x6c\x6f\x73\x65\x3c\ +\x2f\x72\x64\x66\x3a\x6c\x69\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x72\ +\x64\x66\x3a\x41\x6c\x74\x3e\x0a\x20\x20\x20\x3c\x2f\x64\x63\x3a\ +\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x20\x3c\x78\x6d\x70\x4d\x4d\ +\x3a\x48\x69\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x20\x20\x3c\x72\ +\x64\x66\x3a\x53\x65\x71\x3e\x0a\x20\x20\x20\x20\x20\x3c\x72\x64\ +\x66\x3a\x6c\x69\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\ +\x3a\x61\x63\x74\x69\x6f\x6e\x3d\x22\x70\x72\x6f\x64\x75\x63\x65\ +\x64\x22\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x73\ +\x6f\x66\x74\x77\x61\x72\x65\x41\x67\x65\x6e\x74\x3d\x22\x41\x66\ +\x66\x69\x6e\x69\x74\x79\x20\x44\x65\x73\x69\x67\x6e\x65\x72\x20\ +\x31\x2e\x39\x2e\x32\x22\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\ +\x76\x74\x3a\x77\x68\x65\x6e\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ +\x2d\x33\x31\x54\x31\x32\x3a\x34\x33\x3a\x33\x35\x2b\x30\x32\x3a\ +\x30\x30\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\ +\x53\x65\x71\x3e\x0a\x20\x20\x20\x3c\x2f\x78\x6d\x70\x4d\x4d\x3a\ +\x48\x69\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x3c\x2f\x72\x64\x66\ +\x3a\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x3c\ +\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\x78\x3a\x78\x6d\ +\x70\x6d\x65\x74\x61\x3e\x0a\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\ +\x20\x65\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x24\xe1\x35\x97\x00\x00\ +\x01\x83\x69\x43\x43\x50\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\ +\x39\x36\x36\x2d\x32\x2e\x31\x00\x00\x28\x91\x75\x91\xcf\x2b\x44\ +\x51\x14\xc7\x3f\x66\x68\xfc\x18\x8d\x62\x61\x31\x65\x12\x16\x42\ +\x83\x12\x1b\x8b\x99\x18\x0a\x8b\x99\x51\x7e\x6d\x66\x9e\x79\x33\ +\x6a\xde\x78\xbd\x37\xd2\x64\xab\x6c\xa7\x28\xb1\xf1\x6b\xc1\x5f\ +\xc0\x56\x59\x2b\x45\xa4\x64\xa7\xac\x89\x0d\x7a\xce\x9b\x51\x23\ +\x99\x73\x3b\xf7\x7c\xee\xf7\xde\x73\xba\xf7\x5c\x70\x44\xd3\x8a\ +\x66\x56\xfa\x41\xcb\x64\x8d\x70\x28\xe0\x9b\x99\x9d\xf3\xb9\x9e\ +\xa8\xa2\x85\x1a\x3a\xf1\xc6\x14\x53\x9f\x8c\x8c\x46\x29\x6b\xef\ +\xb7\x54\xd8\xf1\xba\xdb\xae\x55\xfe\xdc\xbf\x56\xb7\x98\x30\x15\ +\xa8\xa8\x16\x1e\x56\x74\x23\x2b\x3c\x26\x3c\xb1\x9a\xd5\x6d\xde\ +\x12\x6e\x52\x52\xb1\x45\xe1\x13\xe1\x2e\x43\x2e\x28\x7c\x63\xeb\ +\xf1\x22\x3f\xdb\x9c\x2c\xf2\xa7\xcd\x46\x34\x1c\x04\x47\x83\xb0\ +\x2f\xf9\x8b\xe3\xbf\x58\x49\x19\x9a\xb0\xbc\x9c\x36\x2d\xbd\xa2\ +\xfc\xdc\xc7\x7e\x89\x3b\x91\x99\x8e\x48\x6c\x15\xf7\x62\x12\x26\ +\x44\x00\x1f\xe3\x8c\x10\x64\x80\x5e\x86\x64\x1e\xa0\x9b\x3e\x7a\ +\x64\x45\x99\x7c\x7f\x21\x7f\x8a\x65\xc9\x55\x64\xd6\xc9\x61\xb0\ +\x44\x92\x14\x59\xba\x44\x5d\x91\xea\x09\x89\xaa\xe8\x09\x19\x69\ +\x72\x76\xff\xff\xf6\xd5\x54\xfb\xfb\x8a\xd5\xdd\x01\xa8\x7a\xb4\ +\xac\xd7\x76\x70\x6d\xc2\x57\xde\xb2\x3e\x0e\x2c\xeb\xeb\x10\x9c\ +\x0f\x70\x9e\x29\xe5\x2f\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\ +\x9e\x75\x38\xbd\x28\x69\xf1\x6d\x38\xdb\x80\xe6\x7b\x3d\x66\xc4\ +\x0a\x92\x53\xdc\xa1\xaa\xf0\x72\x0c\xf5\xb3\xd0\x78\x05\xb5\xf3\ +\xc5\x9e\xfd\xec\x73\x74\x07\xd1\x35\xf9\xaa\x4b\xd8\xd9\x85\x0e\ +\x39\xef\x59\xf8\x06\x8e\xfd\x67\xf8\xfd\x8a\x18\x97\x00\x00\x00\ +\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\ +\x9c\x18\x00\x00\x00\x72\x49\x44\x41\x54\x18\x95\x6d\xcf\x31\x0a\ +\xc2\x50\x14\x44\xd1\xe8\x02\xb4\x57\x08\xd6\x49\x61\x99\x4a\x43\ +\x74\x15\x82\xab\x49\x36\x28\xee\x40\x04\xdb\xa8\x95\x58\x78\x2c\ +\xf2\x09\xe1\xf3\x07\xa6\x9a\xfb\xe0\xbe\x0c\x1b\xb4\x58\x64\x71\ +\x70\x30\xe4\x82\x55\x0a\x38\xe3\x8b\x1b\x8a\x14\x70\xc4\x1b\x3d\ +\x76\x29\x60\x8b\x07\x3e\xa8\xe6\xd1\xfe\x0b\x9d\x85\x8e\x57\x0d\ +\x5e\x78\xa2\x9e\x0e\xa7\x20\x74\x47\x39\x1d\xf6\xe1\x95\x2b\xd6\ +\xb1\x44\x8e\x0e\xcb\x58\xf0\x0f\x52\x8a\x79\x18\xdc\xe2\x02\x70\ +\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa0\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1c\x1f\x24\ +\xc6\x09\x17\x00\x00\x00\x24\x49\x44\x41\x54\x08\xd7\x63\x60\x40\ +\x05\xff\xcf\xc3\x58\x4c\xc8\x5c\x26\x64\x59\x26\x64\xc5\x70\x0e\ +\xa3\x21\x9c\xc3\x68\x88\x61\x1a\x0a\x00\x00\x6d\x84\x09\x75\x37\ +\x9e\xd9\x23\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\x9f\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x14\x1f\xf9\ +\x23\xd9\x0b\x00\x00\x00\x23\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x0d\xe6\x7c\x80\xb1\x18\x91\x05\x52\x04\xe0\x42\x08\x15\x29\x02\ +\x0c\x0c\x8c\xc8\x02\x08\x95\x68\x00\x00\xac\xac\x07\x90\x4e\x65\ +\x34\xac\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\x9e\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x0f\xfd\ +\x8f\xf8\x2e\x00\x00\x00\x22\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1\x42\x48\x2a\x0c\x19\ +\x18\x18\x91\x05\x10\x2a\xd1\x00\x00\xca\xb5\x07\xd2\x76\xbb\xb2\ +\xc5\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa5\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\ +\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\ +\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\ +\xae\x42\x60\x82\ +\x00\x00\x00\xa6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\ +\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\ +\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +\x00\x00\x07\xdd\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x07\x00\x00\x00\x0a\x08\x06\x00\x00\x00\x78\xcc\x44\x0d\ +\x00\x00\x05\x52\x69\x54\x58\x74\x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\ +\x61\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\x00\x00\x00\x3c\x3f\ +\x78\x70\x61\x63\x6b\x65\x74\x20\x62\x65\x67\x69\x6e\x3d\x22\xef\ +\xbb\xbf\x22\x20\x69\x64\x3d\x22\x57\x35\x4d\x30\x4d\x70\x43\x65\ +\x68\x69\x48\x7a\x72\x65\x53\x7a\x4e\x54\x63\x7a\x6b\x63\x39\x64\ +\x22\x3f\x3e\x0a\x3c\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x20\x78\ +\x6d\x6c\x6e\x73\x3a\x78\x3d\x22\x61\x64\x6f\x62\x65\x3a\x6e\x73\ +\x3a\x6d\x65\x74\x61\x2f\x22\x20\x78\x3a\x78\x6d\x70\x74\x6b\x3d\ +\x22\x58\x4d\x50\x20\x43\x6f\x72\x65\x20\x35\x2e\x35\x2e\x30\x22\ +\x3e\x0a\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x20\x78\x6d\x6c\x6e\ +\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\ +\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\ +\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\ +\x73\x23\x22\x3e\x0a\x20\x20\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\ +\x72\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\ +\x74\x3d\x22\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x64\ +\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\x2e\x6f\ +\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\x6d\x65\x6e\x74\x73\x2f\x31\ +\x2e\x31\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x65\ +\x78\x69\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\ +\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x65\x78\x69\x66\x2f\x31\x2e\ +\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x74\x69\ +\x66\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\ +\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x74\x69\x66\x66\x2f\x31\x2e\x30\ +\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x70\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\ +\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x70\x68\x6f\x74\ +\x6f\x73\x68\x6f\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\ +\x61\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\ +\x6e\x73\x3a\x78\x6d\x70\x4d\x4d\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\x61\ +\x70\x2f\x31\x2e\x30\x2f\x6d\x6d\x2f\x22\x0a\x20\x20\x20\x20\x78\ +\x6d\x6c\x6e\x73\x3a\x73\x74\x45\x76\x74\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\ +\x78\x61\x70\x2f\x31\x2e\x30\x2f\x73\x54\x79\x70\x65\x2f\x52\x65\ +\x73\x6f\x75\x72\x63\x65\x45\x76\x65\x6e\x74\x23\x22\x0a\x20\x20\ +\x20\x65\x78\x69\x66\x3a\x50\x69\x78\x65\x6c\x58\x44\x69\x6d\x65\ +\x6e\x73\x69\x6f\x6e\x3d\x22\x37\x22\x0a\x20\x20\x20\x65\x78\x69\ +\x66\x3a\x50\x69\x78\x65\x6c\x59\x44\x69\x6d\x65\x6e\x73\x69\x6f\ +\x6e\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x65\x78\x69\x66\x3a\x43\ +\x6f\x6c\x6f\x72\x53\x70\x61\x63\x65\x3d\x22\x31\x22\x0a\x20\x20\ +\x20\x74\x69\x66\x66\x3a\x49\x6d\x61\x67\x65\x57\x69\x64\x74\x68\ +\x3d\x22\x37\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x49\x6d\x61\ +\x67\x65\x4c\x65\x6e\x67\x74\x68\x3d\x22\x31\x30\x22\x0a\x20\x20\ +\x20\x74\x69\x66\x66\x3a\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\ +\x55\x6e\x69\x74\x3d\x22\x32\x22\x0a\x20\x20\x20\x74\x69\x66\x66\ +\x3a\x58\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\ +\x2e\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x59\x52\x65\x73\ +\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\x2e\x30\x22\x0a\x20\ +\x20\x20\x70\x68\x6f\x74\x6f\x73\x68\x6f\x70\x3a\x43\x6f\x6c\x6f\ +\x72\x4d\x6f\x64\x65\x3d\x22\x33\x22\x0a\x20\x20\x20\x70\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x3a\x49\x43\x43\x50\x72\x6f\x66\x69\x6c\ +\x65\x3d\x22\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\x36\ +\x2d\x32\x2e\x31\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x6f\x64\ +\x69\x66\x79\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ +\x2d\x33\x31\x54\x31\x32\x3a\x34\x33\x3a\x30\x39\x2b\x30\x32\x3a\ +\x30\x30\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x65\x74\x61\x64\ +\x61\x74\x61\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ +\x2d\x33\x31\x54\x31\x32\x3a\x34\x33\x3a\x30\x39\x2b\x30\x32\x3a\ +\x30\x30\x22\x3e\x0a\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\ +\x65\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\x41\x6c\x74\x3e\ +\x0a\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\x6c\x69\x20\x78\x6d\ +\x6c\x3a\x6c\x61\x6e\x67\x3d\x22\x78\x2d\x64\x65\x66\x61\x75\x6c\ +\x74\x22\x3e\x62\x72\x61\x6e\x63\x68\x5f\x63\x6c\x6f\x73\x65\x3c\ +\x2f\x72\x64\x66\x3a\x6c\x69\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x72\ +\x64\x66\x3a\x41\x6c\x74\x3e\x0a\x20\x20\x20\x3c\x2f\x64\x63\x3a\ +\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x20\x3c\x78\x6d\x70\x4d\x4d\ +\x3a\x48\x69\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x20\x20\x3c\x72\ +\x64\x66\x3a\x53\x65\x71\x3e\x0a\x20\x20\x20\x20\x20\x3c\x72\x64\ +\x66\x3a\x6c\x69\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\ +\x3a\x61\x63\x74\x69\x6f\x6e\x3d\x22\x70\x72\x6f\x64\x75\x63\x65\ +\x64\x22\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x73\ +\x6f\x66\x74\x77\x61\x72\x65\x41\x67\x65\x6e\x74\x3d\x22\x41\x66\ +\x66\x69\x6e\x69\x74\x79\x20\x44\x65\x73\x69\x67\x6e\x65\x72\x20\ +\x31\x2e\x39\x2e\x32\x22\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\ +\x76\x74\x3a\x77\x68\x65\x6e\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ +\x2d\x33\x31\x54\x31\x32\x3a\x34\x33\x3a\x30\x39\x2b\x30\x32\x3a\ +\x30\x30\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\ +\x53\x65\x71\x3e\x0a\x20\x20\x20\x3c\x2f\x78\x6d\x70\x4d\x4d\x3a\ +\x48\x69\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x3c\x2f\x72\x64\x66\ +\x3a\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x3c\ +\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\x78\x3a\x78\x6d\ +\x70\x6d\x65\x74\x61\x3e\x0a\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\ +\x20\x65\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x58\xad\xf2\x80\x00\x00\ +\x01\x83\x69\x43\x43\x50\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\ +\x39\x36\x36\x2d\x32\x2e\x31\x00\x00\x28\x91\x75\x91\xcf\x2b\x44\ +\x51\x14\xc7\x3f\x66\x68\xfc\x18\x8d\x62\x61\x31\x65\x12\x16\x42\ +\x83\x12\x1b\x8b\x99\x18\x0a\x8b\x99\x51\x7e\x6d\x66\x9e\x79\x33\ +\x6a\xde\x78\xbd\x37\xd2\x64\xab\x6c\xa7\x28\xb1\xf1\x6b\xc1\x5f\ +\xc0\x56\x59\x2b\x45\xa4\x64\xa7\xac\x89\x0d\x7a\xce\x9b\x51\x23\ +\x99\x73\x3b\xf7\x7c\xee\xf7\xde\x73\xba\xf7\x5c\x70\x44\xd3\x8a\ +\x66\x56\xfa\x41\xcb\x64\x8d\x70\x28\xe0\x9b\x99\x9d\xf3\xb9\x9e\ +\xa8\xa2\x85\x1a\x3a\xf1\xc6\x14\x53\x9f\x8c\x8c\x46\x29\x6b\xef\ +\xb7\x54\xd8\xf1\xba\xdb\xae\x55\xfe\xdc\xbf\x56\xb7\x98\x30\x15\ +\xa8\xa8\x16\x1e\x56\x74\x23\x2b\x3c\x26\x3c\xb1\x9a\xd5\x6d\xde\ +\x12\x6e\x52\x52\xb1\x45\xe1\x13\xe1\x2e\x43\x2e\x28\x7c\x63\xeb\ +\xf1\x22\x3f\xdb\x9c\x2c\xf2\xa7\xcd\x46\x34\x1c\x04\x47\x83\xb0\ +\x2f\xf9\x8b\xe3\xbf\x58\x49\x19\x9a\xb0\xbc\x9c\x36\x2d\xbd\xa2\ +\xfc\xdc\xc7\x7e\x89\x3b\x91\x99\x8e\x48\x6c\x15\xf7\x62\x12\x26\ +\x44\x00\x1f\xe3\x8c\x10\x64\x80\x5e\x86\x64\x1e\xa0\x9b\x3e\x7a\ +\x64\x45\x99\x7c\x7f\x21\x7f\x8a\x65\xc9\x55\x64\xd6\xc9\x61\xb0\ +\x44\x92\x14\x59\xba\x44\x5d\x91\xea\x09\x89\xaa\xe8\x09\x19\x69\ +\x72\x76\xff\xff\xf6\xd5\x54\xfb\xfb\x8a\xd5\xdd\x01\xa8\x7a\xb4\ +\xac\xd7\x76\x70\x6d\xc2\x57\xde\xb2\x3e\x0e\x2c\xeb\xeb\x10\x9c\ +\x0f\x70\x9e\x29\xe5\x2f\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\ +\x9e\x75\x38\xbd\x28\x69\xf1\x6d\x38\xdb\x80\xe6\x7b\x3d\x66\xc4\ +\x0a\x92\x53\xdc\xa1\xaa\xf0\x72\x0c\xf5\xb3\xd0\x78\x05\xb5\xf3\ +\xc5\x9e\xfd\xec\x73\x74\x07\xd1\x35\xf9\xaa\x4b\xd8\xd9\x85\x0e\ +\x39\xef\x59\xf8\x06\x8e\xfd\x67\xf8\xfd\x8a\x18\x97\x00\x00\x00\ +\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\ +\x9c\x18\x00\x00\x00\xa2\x49\x44\x41\x54\x18\x95\x55\xcf\xb1\x4a\ +\xc3\x31\x00\xc4\xe1\x2f\xff\xb9\x93\xa3\x93\xb8\xa5\x8b\x0f\x20\ +\x55\x44\x10\x5c\x3a\x84\x2c\x1d\x5c\x7c\x0f\xb7\x8e\x3e\x4a\x88\ +\xa3\xb8\x08\x6d\x05\xbb\x77\xc8\xea\xe2\x0b\x74\x6f\xe9\xd2\x42\ +\x7a\x70\x70\xf0\xe3\x0e\x2e\xa4\xd2\xae\xf0\x8a\xf7\x9a\xe3\x56\ +\xa7\x01\xd7\x78\xc3\x32\x95\x76\x79\x06\x6b\x8e\xdf\x78\xc1\x18\ +\xbf\xa9\xb4\xf1\x09\x86\x53\x48\xa5\x3d\xe2\x03\x3b\x4c\x6b\x8e\ +\xab\xd0\xcf\xa4\xd2\x6e\xf0\x89\x0b\xdc\x0f\xce\xb5\x3f\x3a\x20\ +\x0c\x5d\xeb\x01\x3f\x18\xe1\xa9\xe6\xb8\x1e\x8e\x60\x86\x2f\x6c\ +\x71\x5b\x73\x5c\x40\x48\xa5\xdd\x61\x81\x0d\x9e\x6b\x8e\xff\xfd\ +\xcf\x3f\xcc\x31\xe9\x01\x1c\x00\x73\x52\x2d\x71\xe4\x4a\x1b\x69\ +\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\ +\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\ +\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa5\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\ +\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\ +\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\ +\xae\x42\x60\x82\ +\x00\x00\x00\xa6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1f\x20\xb9\ +\x8d\x77\xe9\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x06\xe6\x7c\x60\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\ +\x64\x60\x60\x62\x60\x48\x11\x40\xe2\x20\x73\x19\x90\x8d\x40\x02\ +\x00\x23\xed\x08\xaf\x64\x9f\x0f\x15\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +\x00\x00\x00\x9e\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x0f\xfd\ +\x8f\xf8\x2e\x00\x00\x00\x22\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1\x42\x48\x2a\x0c\x19\ +\x18\x18\x91\x05\x10\x2a\xd1\x00\x00\xca\xb5\x07\xd2\x76\xbb\xb2\ +\xc5\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\ +\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\ +\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1d\x00\xb0\ +\xd5\x35\xa3\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x06\xfe\x9f\x67\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\ +\x64\x60\x60\x62\x60\x60\x34\x44\xe2\x20\x73\x19\x90\x8d\x40\x02\ +\x00\x64\x40\x09\x75\x86\xb3\xad\x9c\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1d\x00\xb0\ +\xd5\x35\xa3\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x06\xfe\x9f\x67\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\ +\x64\x60\x60\x62\x60\x60\x34\x44\xe2\x20\x73\x19\x90\x8d\x40\x02\ +\x00\x64\x40\x09\x75\x86\xb3\xad\x9c\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +\x00\x00\x07\x06\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x00\x31\xac\xdc\x63\ +\x00\x00\x04\xb0\x69\x54\x58\x74\x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\ +\x61\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\x00\x00\x00\x3c\x3f\ +\x78\x70\x61\x63\x6b\x65\x74\x20\x62\x65\x67\x69\x6e\x3d\x22\xef\ +\xbb\xbf\x22\x20\x69\x64\x3d\x22\x57\x35\x4d\x30\x4d\x70\x43\x65\ +\x68\x69\x48\x7a\x72\x65\x53\x7a\x4e\x54\x63\x7a\x6b\x63\x39\x64\ +\x22\x3f\x3e\x0a\x3c\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x20\x78\ +\x6d\x6c\x6e\x73\x3a\x78\x3d\x22\x61\x64\x6f\x62\x65\x3a\x6e\x73\ +\x3a\x6d\x65\x74\x61\x2f\x22\x20\x78\x3a\x78\x6d\x70\x74\x6b\x3d\ +\x22\x58\x4d\x50\x20\x43\x6f\x72\x65\x20\x35\x2e\x35\x2e\x30\x22\ +\x3e\x0a\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x20\x78\x6d\x6c\x6e\ +\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\ +\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\ +\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\ +\x73\x23\x22\x3e\x0a\x20\x20\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\ +\x72\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\ +\x74\x3d\x22\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x65\ +\x78\x69\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\ +\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x65\x78\x69\x66\x2f\x31\x2e\ +\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x74\x69\ +\x66\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\ +\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x74\x69\x66\x66\x2f\x31\x2e\x30\ +\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x70\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\ +\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x70\x68\x6f\x74\ +\x6f\x73\x68\x6f\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\ +\x61\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\ +\x6e\x73\x3a\x78\x6d\x70\x4d\x4d\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\x61\ +\x70\x2f\x31\x2e\x30\x2f\x6d\x6d\x2f\x22\x0a\x20\x20\x20\x20\x78\ +\x6d\x6c\x6e\x73\x3a\x73\x74\x45\x76\x74\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\ +\x78\x61\x70\x2f\x31\x2e\x30\x2f\x73\x54\x79\x70\x65\x2f\x52\x65\ +\x73\x6f\x75\x72\x63\x65\x45\x76\x65\x6e\x74\x23\x22\x0a\x20\x20\ +\x20\x65\x78\x69\x66\x3a\x50\x69\x78\x65\x6c\x58\x44\x69\x6d\x65\ +\x6e\x73\x69\x6f\x6e\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x65\x78\ +\x69\x66\x3a\x50\x69\x78\x65\x6c\x59\x44\x69\x6d\x65\x6e\x73\x69\ +\x6f\x6e\x3d\x22\x37\x22\x0a\x20\x20\x20\x65\x78\x69\x66\x3a\x43\ +\x6f\x6c\x6f\x72\x53\x70\x61\x63\x65\x3d\x22\x31\x22\x0a\x20\x20\ +\x20\x74\x69\x66\x66\x3a\x49\x6d\x61\x67\x65\x57\x69\x64\x74\x68\ +\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x49\x6d\ +\x61\x67\x65\x4c\x65\x6e\x67\x74\x68\x3d\x22\x37\x22\x0a\x20\x20\ +\x20\x74\x69\x66\x66\x3a\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\ +\x55\x6e\x69\x74\x3d\x22\x32\x22\x0a\x20\x20\x20\x74\x69\x66\x66\ +\x3a\x58\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\ +\x2e\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x59\x52\x65\x73\ +\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\x2e\x30\x22\x0a\x20\ +\x20\x20\x70\x68\x6f\x74\x6f\x73\x68\x6f\x70\x3a\x43\x6f\x6c\x6f\ +\x72\x4d\x6f\x64\x65\x3d\x22\x33\x22\x0a\x20\x20\x20\x70\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x3a\x49\x43\x43\x50\x72\x6f\x66\x69\x6c\ +\x65\x3d\x22\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\x36\ +\x2d\x32\x2e\x31\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x6f\x64\ +\x69\x66\x79\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ +\x2d\x33\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\ +\x30\x30\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x65\x74\x61\x64\ +\x61\x74\x61\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ +\x2d\x33\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\ +\x30\x30\x22\x3e\x0a\x20\x20\x20\x3c\x78\x6d\x70\x4d\x4d\x3a\x48\ +\x69\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\ +\x3a\x53\x65\x71\x3e\x0a\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\ +\x6c\x69\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x61\ +\x63\x74\x69\x6f\x6e\x3d\x22\x70\x72\x6f\x64\x75\x63\x65\x64\x22\ +\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x73\x6f\x66\ +\x74\x77\x61\x72\x65\x41\x67\x65\x6e\x74\x3d\x22\x41\x66\x66\x69\ +\x6e\x69\x74\x79\x20\x44\x65\x73\x69\x67\x6e\x65\x72\x20\x31\x2e\ +\x39\x2e\x32\x22\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\ +\x3a\x77\x68\x65\x6e\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\x2d\x33\ +\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\x30\x30\ +\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x53\x65\ +\x71\x3e\x0a\x20\x20\x20\x3c\x2f\x78\x6d\x70\x4d\x4d\x3a\x48\x69\ +\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x3c\x2f\x72\x64\x66\x3a\x44\ +\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x3c\x2f\x72\ +\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\x78\x3a\x78\x6d\x70\x6d\ +\x65\x74\x61\x3e\x0a\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\x20\x65\ +\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x85\x9d\x9f\x08\x00\x00\x01\x83\ +\x69\x43\x43\x50\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\ +\x36\x2d\x32\x2e\x31\x00\x00\x28\x91\x75\x91\xcf\x2b\x44\x51\x14\ +\xc7\x3f\x66\x68\xfc\x18\x8d\x62\x61\x31\x65\x12\x16\x42\x83\x12\ +\x1b\x8b\x99\x18\x0a\x8b\x99\x51\x7e\x6d\x66\x9e\x79\x33\x6a\xde\ +\x78\xbd\x37\xd2\x64\xab\x6c\xa7\x28\xb1\xf1\x6b\xc1\x5f\xc0\x56\ +\x59\x2b\x45\xa4\x64\xa7\xac\x89\x0d\x7a\xce\x9b\x51\x23\x99\x73\ +\x3b\xf7\x7c\xee\xf7\xde\x73\xba\xf7\x5c\x70\x44\xd3\x8a\x66\x56\ +\xfa\x41\xcb\x64\x8d\x70\x28\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ +\x85\x1a\x3a\xf1\xc6\x14\x53\x9f\x8c\x8c\x46\x29\x6b\xef\xb7\x54\ +\xd8\xf1\xba\xdb\xae\x55\xfe\xdc\xbf\x56\xb7\x98\x30\x15\xa8\xa8\ +\x16\x1e\x56\x74\x23\x2b\x3c\x26\x3c\xb1\x9a\xd5\x6d\xde\x12\x6e\ +\x52\x52\xb1\x45\xe1\x13\xe1\x2e\x43\x2e\x28\x7c\x63\xeb\xf1\x22\ +\x3f\xdb\x9c\x2c\xf2\xa7\xcd\x46\x34\x1c\x04\x47\x83\xb0\x2f\xf9\ +\x8b\xe3\xbf\x58\x49\x19\x9a\xb0\xbc\x9c\x36\x2d\xbd\xa2\xfc\xdc\ +\xc7\x7e\x89\x3b\x91\x99\x8e\x48\x6c\x15\xf7\x62\x12\x26\x44\x00\ +\x1f\xe3\x8c\x10\x64\x80\x5e\x86\x64\x1e\xa0\x9b\x3e\x7a\x64\x45\ +\x99\x7c\x7f\x21\x7f\x8a\x65\xc9\x55\x64\xd6\xc9\x61\xb0\x44\x92\ +\x14\x59\xba\x44\x5d\x91\xea\x09\x89\xaa\xe8\x09\x19\x69\x72\x76\ +\xff\xff\xf6\xd5\x54\xfb\xfb\x8a\xd5\xdd\x01\xa8\x7a\xb4\xac\xd7\ +\x76\x70\x6d\xc2\x57\xde\xb2\x3e\x0e\x2c\xeb\xeb\x10\x9c\x0f\x70\ +\x9e\x29\xe5\x2f\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9e\x75\ +\x38\xbd\x28\x69\xf1\x6d\x38\xdb\x80\xe6\x7b\x3d\x66\xc4\x0a\x92\ +\x53\xdc\xa1\xaa\xf0\x72\x0c\xf5\xb3\xd0\x78\x05\xb5\xf3\xc5\x9e\ +\xfd\xec\x73\x74\x07\xd1\x35\xf9\xaa\x4b\xd8\xd9\x85\x0e\x39\xef\ +\x59\xf8\x06\x8e\xfd\x67\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x6d\x49\x44\x41\x54\x18\x95\x75\xcf\xc1\x09\xc2\x50\ +\x10\x84\xe1\xd7\x85\x07\x9b\xd0\x43\x40\xd2\x82\x78\x14\x7b\x30\ +\x57\x21\x8d\x84\x60\x3f\x62\x4b\x7a\x48\xcc\x97\x83\xfb\x30\x04\ +\xdf\x9c\x86\x7f\x67\x99\xdd\x84\x0d\xaa\x54\x10\x6a\x6c\x13\x1e\ +\xbe\xba\xfe\x09\x35\x31\x7b\xe6\x8d\x0f\x26\x1c\x17\xa1\x53\xb0\ +\x11\x87\x0c\x2f\x01\x07\xec\xb0\x0f\x3f\xe1\xbc\xae\x69\xa3\xe6\ +\x85\x77\xf8\x5b\xe9\xf0\xbb\x9f\xfa\xd2\x83\x39\xdc\xa3\x5b\xf3\ +\x19\x2e\xa8\x89\xb5\x30\xf7\x43\xa0\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +" + +qt_resource_name = b"\ +\x00\x08\ +\x06\xc5\x8e\xa5\ +\x00\x6f\ +\x00\x70\x00\x65\x00\x6e\x00\x70\x00\x79\x00\x70\x00\x65\ +\x00\x06\ +\x07\x03\x7d\xc3\ +\x00\x69\ +\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\ +\x00\x0f\ +\x02\x9f\x05\x87\ +\x00\x72\ +\x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x12\ +\x05\x8f\x9d\x07\ +\x00\x62\ +\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x6f\x00\x70\x00\x65\x00\x6e\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\ +\x00\x67\ +\x00\x1b\ +\x03\x5a\x32\x27\ +\x00\x63\ +\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\ +\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x18\ +\x03\x8e\xde\x67\ +\x00\x72\ +\x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\ +\x00\x6c\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x11\ +\x0b\xda\x30\xa7\ +\x00\x62\ +\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x63\x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\ +\x00\x12\ +\x03\x8d\x04\x47\ +\x00\x72\ +\x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\ +\x00\x67\ +\x00\x15\ +\x0f\xf3\xc0\x07\ +\x00\x75\ +\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\ +\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x0f\ +\x01\x73\x8b\x07\ +\x00\x75\ +\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x0e\ +\x04\xa2\xfc\xa7\ +\x00\x64\ +\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x12\ +\x01\x2e\x03\x27\ +\x00\x63\ +\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\ +\x00\x67\ +\x00\x14\ +\x04\x5e\x2d\xa7\ +\x00\x62\ +\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x63\x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x64\x00\x5f\x00\x6f\x00\x6e\x00\x2e\ +\x00\x70\x00\x6e\x00\x67\ +\x00\x17\ +\x0c\xab\x51\x07\ +\x00\x64\ +\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\ +\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x11\ +\x01\x1f\xc3\x87\ +\x00\x64\ +\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\ +\x00\x17\ +\x0c\x65\xce\x07\ +\x00\x6c\ +\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\ +\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x0c\ +\x06\xe6\xe6\x67\ +\x00\x75\ +\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x15\ +\x03\x27\x72\x67\ +\x00\x63\ +\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\ +\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x11\ +\x00\xb8\x8c\x07\ +\x00\x6c\ +\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\ +\x00\x0e\ +\x0e\xde\xfa\xc7\ +\x00\x6c\ +\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x0f\ +\x06\x53\x25\xa7\ +\x00\x62\ +\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x6f\x00\x70\x00\x65\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ +" + +qt_resource_struct_v1 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\ +\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x1f\x3c\ +\x00\x00\x02\x3c\x00\x00\x00\x00\x00\x01\x00\x00\x1c\x9d\ +\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x13\x68\ +\x00\x00\x01\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x12\x1d\ +\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x02\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x92\ +\x00\x00\x00\x76\x00\x00\x00\x00\x00\x01\x00\x00\x07\xd8\ +\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00\x10\xd6\ +\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x08\x81\ +\x00\x00\x01\xda\x00\x00\x00\x00\x00\x01\x00\x00\x14\x12\ +\x00\x00\x01\x8e\x00\x00\x00\x00\x00\x01\x00\x00\x12\xbf\ +\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\ +\x00\x00\x03\x30\x00\x00\x00\x00\x00\x01\x00\x00\x20\x90\ +\x00\x00\x02\x98\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf0\ +\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x09\x25\ +\x00\x00\x02\x64\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x46\ +\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x1b\xf3\ +\x00\x00\x03\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x1f\xe6\ +\x00\x00\x01\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x11\x7a\ +" + +qt_resource_struct_v2 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x1f\x3c\ +\x00\x00\x01\x76\x41\x9d\xa2\x35\ +\x00\x00\x02\x3c\x00\x00\x00\x00\x00\x01\x00\x00\x1c\x9d\ +\x00\x00\x01\x76\x41\x9d\xa2\x35\ +\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x13\x68\ +\x00\x00\x01\x79\xb4\x72\xcc\x9c\ +\x00\x00\x01\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x12\x1d\ +\x00\x00\x01\x76\x41\x9d\xa2\x39\ +\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x76\x41\x9d\xa2\x37\ +\x00\x00\x02\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x92\ +\x00\x00\x01\x79\xb4\x72\xcc\x9c\ +\x00\x00\x00\x76\x00\x00\x00\x00\x00\x01\x00\x00\x07\xd8\ +\x00\x00\x01\x79\xb4\x72\xcc\x9c\ +\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00\x10\xd6\ +\x00\x00\x01\x76\x41\x9d\xa2\x37\ +\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x08\x81\ +\x00\x00\x01\x76\x41\x9d\xa2\x37\ +\x00\x00\x01\xda\x00\x00\x00\x00\x00\x01\x00\x00\x14\x12\ +\x00\x00\x01\x79\xc2\x05\x2b\x60\ +\x00\x00\x01\x8e\x00\x00\x00\x00\x00\x01\x00\x00\x12\xbf\ +\x00\x00\x01\x76\x41\x9d\xa2\x35\ +\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\ +\x00\x00\x01\x79\xc1\xfc\x16\x91\ +\x00\x00\x03\x30\x00\x00\x00\x00\x00\x01\x00\x00\x20\x90\ +\x00\x00\x01\x79\xc1\xf9\x4b\x78\ +\x00\x00\x02\x98\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf0\ +\x00\x00\x01\x76\x41\x9d\xa2\x39\ +\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x09\x25\ +\x00\x00\x01\x79\xc2\x05\x91\x2a\ +\x00\x00\x02\x64\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x46\ +\x00\x00\x01\x76\x41\x9d\xa2\x35\ +\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x1b\xf3\ +\x00\x00\x01\x76\x41\x9d\xa2\x35\ +\x00\x00\x03\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x1f\xe6\ +\x00\x00\x01\x76\x41\x9d\xa2\x35\ +\x00\x00\x01\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x11\x7a\ +\x00\x00\x01\x76\x41\x9d\xa2\x39\ +" + + +qt_version = [int(v) for v in QtCore.qVersion().split('.')] +if qt_version < [5, 8, 0]: + rcc_version = 1 + qt_resource_struct = qt_resource_struct_v1 +else: + rcc_version = 2 + qt_resource_struct = qt_resource_struct_v2 + + +def qInitResources(): + QtCore.qRegisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) + + +def qCleanupResources(): + QtCore.qUnregisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) diff --git a/openpype/style/pyside2_resources.py b/openpype/style/pyside2_resources.py new file mode 100644 index 0000000000..ee68a74b8e --- /dev/null +++ b/openpype/style/pyside2_resources.py @@ -0,0 +1,843 @@ +# Resource object code (Python 3) +# Created by: object code +# Created by: The Resource Compiler for Qt version 5.15.2 +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore + + +qt_resource_data = b"\ +\x00\x00\x00\x9f\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x14\x1f\xf9\ +#\xd9\x0b\x00\x00\x00#IDAT\x08\xd7c`\xc0\ +\x0d\xe6|\x80\xb1\x18\x91\x05R\x04\xe0B\x08\x15)\x02\ +\x0c\x0c\x8c\xc8\x02\x08\x95h\x00\x00\xac\xac\x07\x90Ne\ +4\xac\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ +;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ +\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\xa5\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ +\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ +200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ +\xaeB`\x82\ +\x00\x00\x00\xa5\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ +\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ +200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ +\xaeB`\x82\ +\x00\x00\x00\xa0\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f\x0d\xfc\ +R+\x9c\x00\x00\x00$IDAT\x08\xd7c`@\ +\x05s>\xc0XL\xc8\x5c&dY&d\xc5pN\ +\x8a\x00\x9c\x93\x22\x80a\x1a\x0a\x00\x00)\x95\x08\xaf\x88\ +\xac\xba4\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1d\x00\xb0\ +\xd55\xa3\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x06\xfe\x9fg``B0\xa1\x1c\x08\x93\x81\x81\x09\xc1\ +d``b``4D\xe2 s\x19\x90\x8d@\x02\ +\x00d@\x09u\x86\xb3\xad\x9c\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\x9e\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ +\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ +\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ +\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ +\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\x9e\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ +\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ +\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ +\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ +\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x07\x06\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\ +\x00\x00\x04\xb0iTXtXML:com.\ +adobe.xmp\x00\x00\x00\x00\x00\x0a\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a\x0a\x85\x9d\x9f\x08\x00\x00\x01\x83\ +iCCPsRGB IEC6196\ +6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\ +\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\ +\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3j\xde\ +x\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\xc0V\ +Y+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\x99s\ +;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8afV\ +\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ +\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\xb7T\ +\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\xa8\xa8\ +\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\x12n\ +RR\xb1E\xe1\x13\xe1.C.(|c\xeb\xf1\x22\ +?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0/\xf9\ +\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\xfc\xdc\ +\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&D\x00\ +\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>zdE\ +\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0D\x92\ +\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19irv\ +\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\xac\xd7\ +vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\x0fp\ +\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9eu\ +8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\x0a\x92\ +S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\ +\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\ +Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00mIDAT\x18\x95u\xcf\xc1\x09\xc2P\ +\x10\x84\xe1\xd7\x85\x07\x9b\xd0C@\xd2\x82x\x14{0\ +W!\x8d\x84`?bKzH\xcc\x97\x83\xfb0\x04\ +\xdf\x9c\x86\x7fg\x99\xdd\x84\x0d\xaaT\x10jl\x13\x1e\ +\xbe\xba\xfe\x0951{\xe6\x8d\x0f&\x1c\x17\xa1S\xb0\ +\x11\x87\x0c/\x01\x07\xec\xb0\x0f?\xe1\xbc\xaei\xa3\xe6\ +\x85w\xf8[\xe9\xf0\xbb\x9f\xfa\xd2\x839\xdc\xa3[\xf3\ +\x19.\xa8\x89\xb50\xf7C\xa0\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f \xb9\ +\x8dw\xe9\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x06\xe6|```B0\xa1\x1c\x08\x93\x81\x81\x09\xc1\ +d``b`H\x11@\xe2 s\x19\x90\x8d@\x02\ +\x00#\xed\x08\xafd\x9f\x0f\x15\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x07\xdd\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x07\x00\x00\x00\x0a\x08\x06\x00\x00\x00x\xccD\x0d\ +\x00\x00\x05RiTXtXML:com.\ +adobe.xmp\x00\x00\x00\x00\x00\x0a\x0a \x0a \x0a \x0a \ +\x0a branch_close<\ +/rdf:li>\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a <\ +/rdf:RDF>\x0a\x0aX\xad\xf2\x80\x00\x00\ +\x01\x83iCCPsRGB IEC61\ +966-2.1\x00\x00(\x91u\x91\xcf+D\ +Q\x14\xc7?fh\xfc\x18\x8dba1e\x12\x16B\ +\x83\x12\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3\ +j\xdex\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\ +\xc0VY+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\ +\x99s;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8a\ +fV\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\ +\xa8\xa2\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\ +\xb7T\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\ +\xa8\xa8\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\ +\x12nRR\xb1E\xe1\x13\xe1.C.(|c\xeb\ +\xf1\x22?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0\ +/\xf9\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\ +\xfc\xdc\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&\ +D\x00\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>z\ +dE\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0\ +D\x92\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19i\ +rv\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\ +\xac\xd7vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\ +\x0fp\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\ +\x9eu8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\ +\x0a\x92S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\ +\xc5\x9e\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e\ +9\xefY\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\ +\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\ +\x9c\x18\x00\x00\x00\xa2IDAT\x18\x95U\xcf\xb1J\ +\xc31\x00\xc4\xe1/\xff\xb9\x93\xa3\x93\xb8\xa5\x8b\x0f \ +UD\x10\x5c:\x84,\x1d\x5c|\x0f\xb7\x8e>J\x88\ +\xa3\xb8\x08m\x05\xbbw\xc8\xea\xe2\x0bto\xe9\xd2B\ +zpp\xf0\xe3\x0e.\xa4\xd2\xae\xf0\x8a\xf7\x9a\xe3V\ +\xa7\x01\xd7x\xc32\x95vy\x06k\x8e\xdfx\xc1\x18\ +\xbf\xa9\xb4\xf1\x09\x86SH\xa5=\xe2\x03;Lk\x8e\ +\xab\xd0\xcf\xa4\xd2n\xf0\x89\x0b\xdc\x0f\xce\xb5?: \ +\x0c]\xeb\x01?\x18\xe1\xa9\xe6\xb8\x1e\x8e`\x86/l\ +q[s\x5c@H\xa5\xdda\x81\x0d\x9ek\x8e\xff\xfd\ +\xcf?\xcc1\xe9\x01\x1c\x00sR-q\xe4J\x1bi\ +\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x07\xad\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x07\x00\x00\x00\x0a\x08\x06\x00\x00\x00x\xccD\x0d\ +\x00\x00\x05RiTXtXML:com.\ +adobe.xmp\x00\x00\x00\x00\x00\x0a\x0a \x0a \x0a \x0a \ +\x0a branch_close<\ +/rdf:li>\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a <\ +/rdf:RDF>\x0a\x0a$\xe15\x97\x00\x00\ +\x01\x83iCCPsRGB IEC61\ +966-2.1\x00\x00(\x91u\x91\xcf+D\ +Q\x14\xc7?fh\xfc\x18\x8dba1e\x12\x16B\ +\x83\x12\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3\ +j\xdex\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\ +\xc0VY+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\ +\x99s;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8a\ +fV\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\ +\xa8\xa2\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\ +\xb7T\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\ +\xa8\xa8\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\ +\x12nRR\xb1E\xe1\x13\xe1.C.(|c\xeb\ +\xf1\x22?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0\ +/\xf9\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\ +\xfc\xdc\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&\ +D\x00\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>z\ +dE\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0\ +D\x92\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19i\ +rv\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\ +\xac\xd7vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\ +\x0fp\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\ +\x9eu8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\ +\x0a\x92S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\ +\xc5\x9e\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e\ +9\xefY\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\ +\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\ +\x9c\x18\x00\x00\x00rIDAT\x18\x95m\xcf1\x0a\ +\xc2P\x14D\xd1\xe8\x02\xb4W\x08\xd6Ia\x99JC\ +t\x15\x82\xabI6(\xee@\x04\xdb\xa8\x95Xx,\ +\xf2\x09\xe1\xf3\x07\xa6\x9a\xfb\xe0\xbe\x0c\x1b\xb4Xdq\ +p0\xe4\x82U\x0a8\xe3\x8b\x1b\x8a\x14p\xc4\x1b=\ +v)`\x8b\x07>\xa8\xe6\xd1\xfe\x0b\x9d\x85\x8eW\x0d\ +^x\xa2\x9e\x0e\xa7 tG9\x1d\xf6\xe1\x95+\xd6\ +\xb1D\x8e\x0e\xcbX\xf0\x0fR\x8ay\x18\xdc\xe2\x02p\ +\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1d\x00\xb0\ +\xd55\xa3\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x06\xfe\x9fg``B0\xa1\x1c\x08\x93\x81\x81\x09\xc1\ +d``b``4D\xe2 s\x19\x90\x8d@\x02\ +\x00d@\x09u\x86\xb3\xad\x9c\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\xa5\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ +\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ +200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ +\xaeB`\x82\ +\x00\x00\x00\xa0\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\ +\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\ +\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ +\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ +\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x070\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\ +\x00\x00\x04\xb0iTXtXML:com.\ +adobe.xmp\x00\x00\x00\x00\x00\x0a\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a\x0aH\x8b[^\x00\x00\x01\x83\ +iCCPsRGB IEC6196\ +6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\ +\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\ +\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3j\xde\ +x\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\xc0V\ +Y+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\x99s\ +;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8afV\ +\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ +\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\xb7T\ +\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\xa8\xa8\ +\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\x12n\ +RR\xb1E\xe1\x13\xe1.C.(|c\xeb\xf1\x22\ +?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0/\xf9\ +\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\xfc\xdc\ +\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&D\x00\ +\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>zdE\ +\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0D\x92\ +\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19irv\ +\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\xac\xd7\ +vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\x0fp\ +\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9eu\ +8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\x0a\x92\ +S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\ +\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\ +Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x97IDAT\x18\x95m\xcf\xb1j\x02A\ +\x14\x85\xe1o\xb7\xb6\xd0'H=Vi\x03\xb1\xb4H\ +;l\xa5\xf19\xf6Y\x02VB\xbaa\x0a\x0b;\x1b\ +\x1bkA\x18\x02)m\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\ +\x9f\xff\x5c\xee\xa9b*\x13Ls\x13nF&\xa6\xf2\ +\x82\xaeF\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a[t\ +\xd8\xc7T\xc2@\x9ac\x8f?|U=|\xc5\x09w\ +\xbc\xa1\xc2\x193,r\x13.\xd5\xe0\xc2\x12\x07\x5cQ\ +#\xe0#7\xe1\xa8O\x0e\x7f\xda`\xd7\xaf\x9f\xb9\x09\ +\xdfc\x05\xff\xe5uLe\xf5\xcc\x1f\x0d3,\x83\xb6\ +\x06D\x83\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ +;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ +\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\xa0\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\ +\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\ +\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ +\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ +\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ +;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ +\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +" + +qt_resource_name = b"\ +\x00\x08\ +\x06\xc5\x8e\xa5\ +\x00o\ +\x00p\x00e\x00n\x00p\x00y\x00p\x00e\ +\x00\x06\ +\x07\x03}\xc3\ +\x00i\ +\x00m\x00a\x00g\x00e\x00s\ +\x00\x15\ +\x0f\xf3\xc0\x07\ +\x00u\ +\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\ +\x00.\x00p\x00n\x00g\ +\x00\x12\ +\x01.\x03'\ +\x00c\ +\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\ +\x00g\ +\x00\x0e\ +\x04\xa2\xfc\xa7\ +\x00d\ +\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x1b\ +\x03Z2'\ +\x00c\ +\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\ +\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x18\ +\x03\x8e\xdeg\ +\x00r\ +\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\ +\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x11\ +\x00\xb8\x8c\x07\ +\x00l\ +\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ +\ +\x00\x0f\ +\x01s\x8b\x07\ +\x00u\ +\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ +\x00\x0c\ +\x06\xe6\xe6g\ +\x00u\ +\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x0f\ +\x06S%\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00.\x00p\x00n\x00g\ +\x00\x17\ +\x0ce\xce\x07\ +\x00l\ +\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ +\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x14\ +\x04^-\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00_\x00o\x00n\x00.\ +\x00p\x00n\x00g\ +\x00\x11\ +\x0b\xda0\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00.\x00p\x00n\x00g\ +\ +\x00\x0e\ +\x0e\xde\xfa\xc7\ +\x00l\ +\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x11\ +\x01\x1f\xc3\x87\ +\x00d\ +\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ +\ +\x00\x0f\ +\x02\x9f\x05\x87\ +\x00r\ +\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x12\ +\x05\x8f\x9d\x07\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00_\x00o\x00n\x00.\x00p\x00n\ +\x00g\ +\x00\x17\ +\x0c\xabQ\x07\ +\x00d\ +\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ +\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x12\ +\x03\x8d\x04G\ +\x00r\ +\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\ +\x00g\ +\x00\x15\ +\x03'rg\ +\x00c\ +\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\ +\x00.\x00p\x00n\x00g\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x00\x03C\ +\x00\x00\x01vA\x9d\xa25\ +\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x00\x1d!\ +\x00\x00\x01vA\x9d\xa25\ +\x00\x00\x00X\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa3\ +\x00\x00\x01y\xb4r\xcc\x9c\ +\x00\x00\x01>\x00\x00\x00\x00\x00\x01\x00\x00\x03\xed\ +\x00\x00\x01vA\x9d\xa29\ +\x00\x00\x02x\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xca\ +\x00\x00\x01vA\x9d\xa27\ +\x00\x00\x03$\x00\x00\x00\x00\x00\x01\x00\x00&\xf0\ +\x00\x00\x01y\xb4r\xcc\x9c\ +\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x01\xf6\ +\x00\x00\x01y\xb4r\xcc\x9c\ +\x00\x00\x02\xfa\x00\x00\x00\x00\x00\x01\x00\x00&L\ +\x00\x00\x01vA\x9d\xa27\ +\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x02\x9f\ +\x00\x00\x01vA\x9d\xa27\ +\x00\x00\x01\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xe5\ +\x00\x00\x01y\xc2\x05+`\ +\x00\x00\x00\x82\x00\x00\x00\x00\x00\x01\x00\x00\x01M\ +\x00\x00\x01vA\x9d\xa25\ +\x00\x00\x02\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x1en\ +\x00\x00\x01y\xc1\xfc\x16\x91\ +\x00\x00\x01\x80\x00\x00\x00\x00\x00\x01\x00\x00\x051\ +\x00\x00\x01y\xc1\xf9Kx\ +\x00\x00\x01b\x00\x00\x00\x00\x00\x01\x00\x00\x04\x8f\ +\x00\x00\x01vA\x9d\xa29\ +\x00\x00\x02\x06\x00\x00\x00\x00\x00\x01\x00\x00\x14\xc6\ +\x00\x00\x01y\xc2\x05\x91*\ +\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x0c;\ +\x00\x00\x01vA\x9d\xa25\ +\x00\x00\x02\xc6\x00\x00\x00\x00\x00\x01\x00\x00%\xa2\ +\x00\x00\x01vA\x9d\xa25\ +\x00\x00\x02.\x00\x00\x00\x00\x00\x01\x00\x00\x1cw\ +\x00\x00\x01vA\x9d\xa25\ +\x00\x00\x00(\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01vA\x9d\xa29\ +" + + +def qInitResources(): + QtCore.qRegisterResourceData( + 0x03, qt_resource_struct, qt_resource_name, qt_resource_data + ) + + +def qCleanupResources(): + QtCore.qUnregisterResourceData( + 0x03, qt_resource_struct, qt_resource_name, qt_resource_data + ) diff --git a/openpype/tools/project_manager/project_manager/style/qrc_resources.py b/openpype/style/qrc_resources.py similarity index 100% rename from openpype/tools/project_manager/project_manager/style/qrc_resources.py rename to openpype/style/qrc_resources.py diff --git a/openpype/style/resources.qrc b/openpype/style/resources.qrc new file mode 100644 index 0000000000..a583d9458e --- /dev/null +++ b/openpype/style/resources.qrc @@ -0,0 +1,23 @@ + + + images/combobox_arrow.png + images/combobox_arrow_disabled.png + images/branch_closed.png + images/branch_closed_on.png + images/branch_open.png + images/branch_open_on.png + images/combobox_arrow_on.png + images/down_arrow.png + images/down_arrow_disabled.png + images/down_arrow_on.png + images/left_arrow.png + images/left_arrow_disabled.png + images/left_arrow_on.png + images/right_arrow.png + images/right_arrow_disabled.png + images/right_arrow_on.png + images/up_arrow.png + images/up_arrow_disabled.png + images/up_arrow_on.png + + diff --git a/openpype/style/style.css b/openpype/style/style.css new file mode 100644 index 0000000000..aa71105320 --- /dev/null +++ b/openpype/style/style.css @@ -0,0 +1,590 @@ +/* +Enabled vs Disabled logic in most of stylesheets + +- global font color + Enabled - should be same globalle except placeholders + Disabled - font color is greyed out + +- global active/hover + Enabled - color motive of borders and bg color + - combobox, slider, views, buttons, checkbox, radiobox, inputs + +- QLineEdit, QTextEdit, QPlainTextEdit, QAbstractSpinBox + Enabled - bg has lighter or darked color + Disabled - bg has same color as background + +- QComboBox, QPushButton, QToolButton + Enabled - slightly lighter color + Disabled - even lighter color +*/ + +* { + font-size: 9pt; + font-family: "Spartan"; + font-weight: 450; +} + +QWidget { + color: {color:font}; + background: {color:bg}; + border-radius: 0px; +} + +QWidget:disabled { + color: {color:font-disabled}; +} + +/* Inputs */ +QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { + border: 1px solid {color:border}; + border-radius: 0.3em; + background: {color:bg-inputs}; + padding: 0.1em; +} + +QAbstractSpinBox:disabled, QLineEdit:disabled, QPlainTextEdit:disabled, QTextEdit:disabled { + background: {color:bg-inputs-disabled}; +} +QAbstractSpinBox:hover, QLineEdit:hover, QPlainTextEdit:hover, QTextEdit:hover{ + border-color: {color:border-hover}; +} +QAbstractSpinBox:focus, QLineEdit:focus, QPlainTextEdit:focus, QTextEdit:focus{ + border-color: {color:border-focus}; +} + +/* Buttons */ +QPushButton { + text-align:center center; + border: 1px solid transparent; + border-radius: 0.2em; + padding: 3px 5px 3px 5px; + background: {color:bg-buttons}; +} + +QPushButton:hover { + background: {color:bg-button-hover}; + color: {color:font-hover}; +} + +QPushButton:pressed {} + +QPushButton:disabled { + background: {color:bg-buttons-disabled}; +} + +QPushButton::menu-indicator { + subcontrol-origin: padding; + subcontrol-position: right; + width: 8px; + height: 8px; + padding-right: 5px; +} + +QToolButton { + border: none; + background: transparent; + border-radius: 0.2em; + padding: 2px; +} + +QToolButton:hover { + background: #333840; + border-color: {color:border-hover}; +} + +QToolButton:disabled { + background: {color:bg-buttons-disabled}; +} + +/* QMenu */ +QMenu { + border: 1px solid #555555; + background: {color:bg-inputs}; +} + +QMenu::icon { + padding-left: 7px; +} + +QMenu::item { + padding: 6px 25px 6px 10px; +} + +QMenu::item:selected { + background: {color:bg-view-hover}; +} + +QMenu::item:selected:hover { + background: {color:bg-view-hover}; +} + +QMenu::right-arrow { + min-width: 10px; +} +QMenu::separator { + background: {color:bg-menu-separator}; + height: 2px; + margin-right: 5px; +} + +/* Combobox */ +QComboBox { + border: 1px solid {color:border}; + border-radius: 3px; + padding: 1px 3px 1px 3px; + background: {color:bg-inputs}; +} +QComboBox:hover { + border-color: {color:border-hover}; +} +QComboBox:disabled { + background: {color:bg-inputs-disabled}; +} + +/* QComboBox must have explicitly set Styled delegate! */ +QComboBox QAbstractItemView { + border: 1px solid {color:border}; + background: {color:bg-inputs}; + outline: none; +} + +QComboBox QAbstractItemView::item:selected { + background: {color:bg-view-hover}; + color: {color:font}; + padding-left: 0px; +} + +QComboBox QAbstractItemView::item:selected:hover { + background: {color:bg-view-hover}; +} + +QComboBox::drop-down { + subcontrol-origin: padding; + subcontrol-position: center right; + width: 15px; + border-style: none; + border-left-style: solid; + border-left-color: {color:border}; + border-left-width: 1px; +} +QComboBox::down-arrow, QComboBox::down-arrow:on, QComboBox::down-arrow:hover, QComboBox::down-arrow:focus +{ + image: url(:/openpype/images/combobox_arrow.png); +} + +/* Splitter */ +QSplitter { + border: none; +} + +QSplitter::handle { + border: 1px dotted {color:bg-menu-separator}; +} + +/* SLider */ +QSlider::groove { + border: 1px solid #464b54; + border-radius: 0.3em; + background: {color:bg-inputs}; +} +QSlider::groove:horizontal { + height: 8px; +} + +QSlider::groove:vertical { + width: 8px; +} + +QSlider::groove:hover { + border-color: {color:border-hover}; +} +QSlider::groove:disabled { + background: {color:bg-inputs-disabled}; +} +QSlider::groove:focus { + border-color: {color:border-focus}; +} +QSlider::handle { + background: qlineargradient( + x1: 0, y1: 0.5, + x2: 1, y2: 0.5, + stop: 0 {palette:blue-base}, + stop: 1 {palette:green-base} + ); + border: 1px solid #5c5c5c; + width: 10px; + height: 10px; + + border-radius: 5px; +} +QSlider::handle:horizontal { + margin: -2px 0; +} +QSlider::handle:vertical { + margin: 0 -2px; +} + +QSlider::handle:disabled { + background: qlineargradient( + x1:0, y1:0, + x2:1, y2:1, + stop:0 {color:bg-buttons}, + stop:1 {color:bg-buttons-disabled} + ); +} + +/* Tab widget*/ +QTabWidget::pane { + border-top-style: none; +} + +/* move to the right to not mess with borders of widget underneath */ +QTabWidget::tab-bar { + left: 2px; +} + +QTabBar::tab { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + padding: 5px; + +} + +QTabBar::tab:selected { + background: {color:grey-lighter}; + /* background: qradialgradient( + cx:0.5, cy:0.5, radius: 2, + fx:0.5, fy:1, + stop:0.3 {color:bg}, stop:1 white + ) */ + /* background: qlineargradient( + x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 {color:bg-inputs}, stop: 1.0 {color:bg} + ); */ +} + +QTabBar::tab:!selected { + /* Make it smaller*/ + margin-top: 3px; + background: {color:grey-light}; +} + +QTabBar::tab:!selected:hover { + background: {color:grey-lighter}; +} + +QTabBar::tab:first:selected { + margin-left: 0; +} + +QTabBar::tab:last:selected { + margin-right: 0; +} + +QTabBar::tab:only-one { + margin: 0; +} + +QHeaderView { + border: none; + border-radius: 2px; + margin: 0px; + padding: 0px; +} + +QHeaderView::section { + background: {color:bg-view-header}; + padding: 4px; + border-right: 1px solid {color:bg-view}; + border-radius: 0px; + text-align: center; + color: {color:font}; + font-weight: bold; +} +QHeaderView::section:first { + border-left: none; +} +QHeaderView::section:last { + border-right: none; +} +/* Views QListView QTreeView QTableView */ +QAbstractItemView { + border: 0px solid {color:border}; + border-radius: 0.2em; + background: {color:bg-view}; + alternate-background-color: {color:bg-view-alternate}; +} + +QAbstractItemView:disabled{ + background: {color:bg-view-disabled}; + alternate-background-color: {color:bg-view-alternate-disabled}; +} + +QAbstractItemView::item:hover { + /* color: {color:bg-view-hover}; */ + background: {color:bg-view-hover}; +} + +QAbstractItemView::item:selected { + background: {color:bg-view-selection}; + color: {color:font-view-selection}; +} + +QAbstractItemView::item:selected:active { + color: {color:font-view-selection}; +} + +/* Same as selected but give ability to easy change it */ +QAbstractItemView::item:selected:!active { + background: {color:bg-view-selection}; + color: {color:font-view-selection}; +} + +QAbstractItemView::item:selected:hover { + background: {color:bg-view-selection-hover}; +} + +QAbstractItemView::branch:open:has-children:!has-siblings, +QAbstractItemView::branch:open:has-children:has-siblings { + border-image: none; + image: url(:/openpype/images/branch_open.png); + background: {color:bg-view}; +} +QAbstractItemView::branch:open:has-children:!has-siblings:hover, +QAbstractItemView::branch:open:has-children:has-siblings:hover { + border-image: none; + image: url(:/openpype/images//branch_open_on.png); + /* background: {color:bg-view-hover}; */ +} + +QAbstractItemView::branch:has-children:!has-siblings:closed, +QAbstractItemView::branch:closed:has-children:has-siblings { + border-image: none; + image: url(:/openpype/images//branch_closed.png); + background: {color:bg-view}; +} +QAbstractItemView::branch:has-children:!has-siblings:closed:hover, +QAbstractItemView::branch:closed:has-children:has-siblings:hover { + border-image: none; + image: url(:/openpype/images//branch_closed_on.png); + /* background: {color:bg-view-hover}; */ +} + +/* Progress bar */ +QProgressBar { + border: 1px solid {color:border}; + font-weight: bold; + text-align: center; +} + +QProgressBar:horizontal { + height: 20px; +} +QProgressBar:vertical { + width: 20px; +} + +QProgressBar::chunk { + background: qlineargradient( + x1: 0, y1: 0.5, + x2: 1, y2: 0.5, + stop: 0 {palette:blue-base}, + stop: 1 {palette:green-base} + ); +} + +/* Scroll bars */ +QScrollBar { + background: {color:bg-inputs}; + border-radius: 4px; + border: 1px transparent {color:bg-inputs}; +} + +QScrollBar:horizontal { + height: 15px; + margin: 3px 3px 3px 6px; +} + +QScrollBar:vertical { + width: 15px; + margin: 6px 3px 3px 3px; +} + +QScrollBar::handle { + background: {color:bg-scroll-handle}; + border-radius: 4px; +} + +QScrollBar::handle:horizontal { + min-width: 5px; +} + +QScrollBar::handle:vertical { + min-height: 5px; +} + +QScrollBar::add-line:horizontal { + margin: 0px 3px 0px 3px; + width: 0px; + height: 0px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal { + margin: 0px 3px 0px 3px; + height: 0px; + width: 0px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on { + height: 0px; + width: 0px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on { + height: 0px; + width: 0px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal { + background: none; +} + +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { + background: none; +} + +QScrollBar::sub-line:vertical { + margin: 3px 0px 3px 0px; + height: 0px; + width: 0px; + subcontrol-position: top; + subcontrol-origin: margin; +} + +QScrollBar::add-line:vertical { + margin: 3px 0px 3px 0px; + height: 0px; + width: 0px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on { + subcontrol-position: top; + subcontrol-origin: margin; +} + + +QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on { + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { + background: none; +} + + +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: none; +} + +/* Globally used names */ +#Separator { + background: {color:bg-menu-separator}; +} + +#IconBtn {} + +/* Password dialog*/ +#PasswordBtn { + border: none; + padding:0.1em; + background: transparent; +} + +#PasswordBtn:hover { + background: {color:bg-buttons}; +} + +#RememberCheckbox { + spacing: 0.5em; +} + +/* Project Manager stylesheets */ +#HierarchyView::item { + padding-top: 3px; + padding-bottom: 3px; + padding-right: 3px; +} + +#TypeEditor, #ToolEditor, #NameEditor, #NumberEditor { + background: transparent; + border-radius: 0.3em; +} + +#TypeEditor:focus, #ToolEditor:focus, #NameEditor:focus, #NumberEditor:focus { + background: {color:bg-inputs}; +} + +#CompleterView { + border: 1px solid {color:border}; + background: {color:bg-inputs}; + outline: none; +} + +#CompleterView::item { + padding: 2px 4px 2px 4px; + border-left: 3px solid {color:bg-view}; +} + +#CompleterView::item:hover { + border-left-color: {palette:blue-base}; + background: {color:bg-view-selection}; + color: {color:font}; +} + +/* Launcher specific stylesheets */ +#IconView[mode="icon"] { + /* font size can't be set on items */ + font-size: 8pt; + border: 0px; + padding: 0px; + margin: 0px; +} + +#IconView[mode="icon"]::item { + margin-top: 6px; + border: 0px; +} + +#IconView[mode="icon"]::item:hover { + background: rgba(0, 0, 0, 0); + color: {color:font-hover}; +} + +#IconView[mode="icon"]::icon { + top: 3px; +} + +/* Standalone publisher */ +#ComponentList { + outline: none; +} + +#ComponentItem { + background: transparent; +} + +#ComponentFrame { + border: 1px solid {color:border}; + border-radius: 0.1em; +} diff --git a/openpype/tools/launcher/actions.py b/openpype/tools/launcher/actions.py index 72c7aece72..14c6aff4ad 100644 --- a/openpype/tools/launcher/actions.py +++ b/openpype/tools/launcher/actions.py @@ -1,7 +1,8 @@ import os -from avalon import api, style +from avalon import api from openpype import PLUGINS_DIR +from openpype import style from openpype.api import Logger, resources from openpype.lib import ( ApplictionExecutableNotFound, diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 9a7d8ca772..22b08d7d15 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -27,22 +27,30 @@ class ProjectBar(QtWidgets.QWidget): self.dbcon = dbcon - self.model = ProjectModel(self.dbcon) - self.model.hide_invisible = True + model = ProjectModel(dbcon) + model.hide_invisible = True - self.project_combobox = QtWidgets.QComboBox() - self.project_combobox.setModel(self.model) - self.project_combobox.setRootModelIndex(QtCore.QModelIndex()) + 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.setRootModelIndex(QtCore.QModelIndex()) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.project_combobox) + layout.addWidget(project_combobox) self.setSizePolicy( QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Maximum ) + self.model = model + self.project_delegate = project_delegate + self.project_combobox = project_combobox + # Initialize self.refresh() diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index c0aeec7d2f..af749814b7 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -2,9 +2,10 @@ import copy import logging from Qt import QtWidgets, QtCore, QtGui -from avalon import style from avalon.api import AvalonMongoDB + +from openpype import style from openpype.api import resources from avalon.tools import lib as tools_lib @@ -23,7 +24,7 @@ from .widgets import ( from .flickcharm import FlickCharm -class IconListView(QtWidgets.QListView): +class ProjectIconView(QtWidgets.QListView): """Styled ListView that allows to toggle between icon and list mode. Toggling between the two modes is done by Right Mouse Click. @@ -34,7 +35,7 @@ class IconListView(QtWidgets.QListView): ListMode = 1 def __init__(self, parent=None, mode=ListMode): - super(IconListView, self).__init__(parent=parent) + super(ProjectIconView, self).__init__(parent=parent) # Workaround for scrolling being super slow or fast when # toggling between the two visual modes @@ -83,7 +84,7 @@ class IconListView(QtWidgets.QListView): def mousePressEvent(self, event): if event.button() == QtCore.Qt.RightButton: self.set_mode(int(not self._mode)) - return super(IconListView, self).mousePressEvent(event) + return super(ProjectIconView, self).mousePressEvent(event) class ProjectsPanel(QtWidgets.QWidget): @@ -99,7 +100,7 @@ class ProjectsPanel(QtWidgets.QWidget): self.dbcon = dbcon self.dbcon.install() - view = IconListView(parent=self) + view = ProjectIconView(parent=self) view.setSelectionMode(QtWidgets.QListView.NoSelection) flick = FlickCharm(parent=self) flick.activateOn(view) @@ -140,8 +141,6 @@ class AssetsPanel(QtWidgets.QWidget): btn_back_icon = qtawesome.icon("fa.angle-left", color="white") btn_back = QtWidgets.QPushButton(project_bar_widget) btn_back.setIcon(btn_back_icon) - btn_back.setFixedWidth(23) - btn_back.setFixedHeight(23) project_bar = ProjectBar(self.dbcon, project_bar_widget) @@ -185,10 +184,6 @@ class AssetsPanel(QtWidgets.QWidget): layout.addWidget(project_bar_widget) layout.addWidget(body) - self.project_bar = project_bar - self.assets_widget = assets_widget - self.tasks_widget = tasks_widget - # signals project_bar.project_changed.connect(self.on_project_changed) assets_widget.selection_changed.connect(self.on_asset_changed) @@ -197,12 +192,25 @@ class AssetsPanel(QtWidgets.QWidget): btn_back.clicked.connect(self.back_clicked) + 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) + + # Change size of a btn + # WARNING does not handle situation if combobox is bigger + 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: diff --git a/openpype/tools/project_manager/project_manager/style/__init__.py b/openpype/tools/project_manager/project_manager/style.py similarity index 70% rename from openpype/tools/project_manager/project_manager/style/__init__.py rename to openpype/tools/project_manager/project_manager/style.py index b686967ddd..17e269c1f6 100644 --- a/openpype/tools/project_manager/project_manager/style/__init__.py +++ b/openpype/tools/project_manager/project_manager/style.py @@ -1,11 +1,9 @@ -import os -from openpype import resources from avalon.vendor import qtawesome class ResourceCache: colors = { - "standard": "#333333", + "standard": "#bfccd6", "new": "#2d9a4c", "warning": "#c83232" } @@ -68,31 +66,3 @@ class ResourceCache: @classmethod def get_color(cls, color_name): return cls.colors[color_name] - - @classmethod - def style_fill_data(cls): - output = {} - for color_name, color_value in cls.colors.items(): - key = "color:{}".format(color_name) - output[key] = color_value - return output - - -def load_stylesheet(): - from . import qrc_resources - - qrc_resources.qInitResources() - - current_dir = os.path.dirname(os.path.abspath(__file__)) - style_path = os.path.join(current_dir, "style.css") - with open(style_path, "r") as style_file: - stylesheet = style_file.read() - - for key, value in ResourceCache.style_fill_data().items(): - replacement_key = "{" + key + "}" - stylesheet = stylesheet.replace(replacement_key, value) - return stylesheet - - -def app_icon_path(): - return resources.pype_icon_filepath() diff --git a/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py b/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py deleted file mode 100644 index 836934019d..0000000000 --- a/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore - - -qt_resource_data = b"\ -\x00\x00\x00\xa5\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ -\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\ -\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\ -\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ -\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\ -\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\ -\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\ -\xae\x42\x60\x82\ -\x00\x00\x00\xa6\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ -\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ -\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\ -\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ -\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\ -\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\ -\x44\xae\x42\x60\x82\ -" - - -qt_resource_name = b"\ -\x00\x08\ -\x06\xc5\x8e\xa5\ -\x00\x6f\ -\x00\x70\x00\x65\x00\x6e\x00\x70\x00\x79\x00\x70\x00\x65\ -\x00\x06\ -\x07\x03\x7d\xc3\ -\x00\x69\ -\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\ -\x00\x12\ -\x01\x2e\x03\x27\ -\x00\x63\ -\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\ -\x00\x67\ -\x00\x1b\ -\x03\x5a\x32\x27\ -\x00\x63\ -\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\ -\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ -" - - -qt_resource_struct_v1 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x16\x00\x02\x00\x00\x00\x02\x00\x00\x00\x03\ -\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\ -" - - -qt_resource_struct_v2 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x16\x00\x02\x00\x00\x00\x02\x00\x00\x00\x03\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x76\x41\x9d\xa2\x35\ -\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\ -\x00\x00\x01\x76\x41\x9d\xa2\x35\ -" - - -qt_version = [int(v) for v in QtCore.qVersion().split('.')] -if qt_version < [5, 8, 0]: - rcc_version = 1 - qt_resource_struct = qt_resource_struct_v1 -else: - rcc_version = 2 - qt_resource_struct = qt_resource_struct_v2 - - -def qInitResources(): - QtCore.qRegisterResourceData( - rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data - ) - - -def qCleanupResources(): - QtCore.qUnregisterResourceData( - rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data - ) diff --git a/openpype/tools/project_manager/project_manager/style/pyside2_resources.py b/openpype/tools/project_manager/project_manager/style/pyside2_resources.py deleted file mode 100644 index b73d5e334a..0000000000 --- a/openpype/tools/project_manager/project_manager/style/pyside2_resources.py +++ /dev/null @@ -1,84 +0,0 @@ -# Resource object code (Python 3) -# Created by: object code -# Created by: The Resource Compiler for Qt version 5.15.2 -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore - - -qt_resource_data = b"\ -\x00\x00\x00\xa5\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ -\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ -200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ -\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ -\xaeB`\x82\ -\x00\x00\x00\xa6\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ -;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ -\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ -D\xaeB`\x82\ -" - - -qt_resource_name = b"\ -\x00\x08\ -\x06\xc5\x8e\xa5\ -\x00o\ -\x00p\x00e\x00n\x00p\x00y\x00p\x00e\ -\x00\x06\ -\x07\x03}\xc3\ -\x00i\ -\x00m\x00a\x00g\x00e\x00s\ -\x00\x12\ -\x01.\x03'\ -\x00c\ -\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\ -\x00g\ -\x00\x1b\ -\x03Z2'\ -\x00c\ -\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\ -\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ -" - - -qt_resource_struct = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x16\x00\x02\x00\x00\x00\x02\x00\x00\x00\x03\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00(\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01vA\x9d\xa25\ -\x00\x00\x00R\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\ -\x00\x00\x01vA\x9d\xa25\ -" - - -def qInitResources(): - QtCore.qRegisterResourceData( - 0x03, qt_resource_struct, qt_resource_name, qt_resource_data - ) - - -def qCleanupResources(): - QtCore.qUnregisterResourceData( - 0x03, qt_resource_struct, qt_resource_name, qt_resource_data - ) diff --git a/openpype/tools/project_manager/project_manager/style/resources.qrc b/openpype/tools/project_manager/project_manager/style/resources.qrc deleted file mode 100644 index 9281c69479..0000000000 --- a/openpype/tools/project_manager/project_manager/style/resources.qrc +++ /dev/null @@ -1,6 +0,0 @@ - - - images/combobox_arrow.png - images/combobox_arrow_disabled.png - - diff --git a/openpype/tools/project_manager/project_manager/style/style.css b/openpype/tools/project_manager/project_manager/style/style.css deleted file mode 100644 index 31196b7cc6..0000000000 --- a/openpype/tools/project_manager/project_manager/style/style.css +++ /dev/null @@ -1,21 +0,0 @@ -QTreeView::item { - padding-top: 3px; - padding-bottom: 3px; - padding-right: 3px; -} - - -QTreeView::item:selected, QTreeView::item:selected:!active { - background: rgba(0, 122, 204, 127); - color: black; -} - -#RefreshBtn { - padding: 2px; -} - -#TypeEditor, #ToolEditor, #NameEditor, #NumberEditor { - background: transparent; - border: 1px solid #005c99; - border-radius: 0.3em; -} diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 70af11e68d..4d75af3405 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -124,6 +124,9 @@ class HierarchyView(QtWidgets.QTreeView): def __init__(self, dbcon, source_model, parent): super(HierarchyView, self).__init__(parent) + + self.setObjectName("HierarchyView") + # Direct access to model self._source_model = source_model self._editors_mapping = {} @@ -409,6 +412,9 @@ class HierarchyView(QtWidgets.QTreeView): return self._source_model.add_new_task(parent_index) + def _add_asset_action(self): + self._add_asset_and_edit() + def _add_asset_and_edit(self, parent_index=None): new_index = self.add_asset(parent_index) if new_index is None: @@ -423,6 +429,9 @@ class HierarchyView(QtWidgets.QTreeView): # Start editing self.edit(new_index) + def _add_task_action(self): + self._add_task_and_edit() + def _add_task_and_edit(self): new_index = self.add_task() if new_index is None: @@ -574,14 +583,14 @@ class HierarchyView(QtWidgets.QTreeView): if item_type in ("asset", "project"): add_asset_action = QtWidgets.QAction("Add Asset", context_menu) add_asset_action.triggered.connect( - self._add_asset_and_edit + self._add_asset_action ) actions.append(add_asset_action) if item_type in ("asset", "task"): add_task_action = QtWidgets.QAction("Add Task", context_menu) add_task_action.triggered.connect( - self._add_task_and_edit + self._add_task_action ) actions.append(add_task_action) diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 9c57febcf6..8c2f693f11 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -9,6 +9,7 @@ from openpype.lib import ( PROJECT_NAME_ALLOWED_SYMBOLS, PROJECT_NAME_REGEX ) +from openpype.style import load_stylesheet from avalon.api import AvalonMongoDB from Qt import QtWidgets, QtCore @@ -44,6 +45,8 @@ class FilterComboBox(QtWidgets.QComboBox): self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setEditable(True) + combobox_delegate = QtWidgets.QStyledItemDelegate(self) + self.setItemDelegate(combobox_delegate) filter_proxy_model = QtCore.QSortFilterProxyModel(self) filter_proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) @@ -55,6 +58,12 @@ class FilterComboBox(QtWidgets.QComboBox): ) self.setCompleter(completer) + completer_view = completer.popup() + completer_view.setObjectName("CompleterView") + delegate = QtWidgets.QStyledItemDelegate(completer_view) + completer_view.setItemDelegate(delegate) + completer_view.setStyleSheet(load_stylesheet()) + self.lineEdit().textEdited.connect( filter_proxy_model.setFilterFixedString ) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index a800214517..37092bc4a9 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -9,7 +9,10 @@ from . import ( CreateProjectDialog ) -from .style import load_stylesheet, ResourceCache +from openpype.style import load_stylesheet +from .style import ResourceCache +from openpype.lib import is_admin_password_required +from openpype.widgets import PasswordDialog from openpype import resources from avalon.api import AvalonMongoDB @@ -19,6 +22,10 @@ class ProjectManagerWindow(QtWidgets.QWidget): def __init__(self, parent=None): super(ProjectManagerWindow, self).__init__(parent) + self._initial_reset = False + self._password_dialog = None + self._user_passed = False + self.setWindowTitle("OpenPype Project Manager") self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath())) @@ -32,13 +39,18 @@ class ProjectManagerWindow(QtWidgets.QWidget): project_model = ProjectModel(dbcon) project_combobox = QtWidgets.QComboBox(project_widget) + project_combobox.setSizeAdjustPolicy( + QtWidgets.QComboBox.AdjustToContents + ) project_combobox.setModel(project_model) project_combobox.setRootModelIndex(QtCore.QModelIndex()) + style_delegate = QtWidgets.QStyledItemDelegate() + project_combobox.setItemDelegate(style_delegate) refresh_projects_btn = QtWidgets.QPushButton(project_widget) refresh_projects_btn.setIcon(ResourceCache.get_icon("refresh")) refresh_projects_btn.setToolTip("Refresh projects") - refresh_projects_btn.setObjectName("RefreshBtn") + refresh_projects_btn.setObjectName("IconBtn") create_project_btn = QtWidgets.QPushButton( "Create project...", project_widget @@ -65,6 +77,8 @@ class ProjectManagerWindow(QtWidgets.QWidget): "Task", helper_btns_widget ) + add_asset_btn.setObjectName("IconBtn") + add_task_btn.setObjectName("IconBtn") helper_btns_layout = QtWidgets.QHBoxLayout(helper_btns_widget) helper_btns_layout.setContentsMargins(0, 0, 0, 0) @@ -113,41 +127,57 @@ class ProjectManagerWindow(QtWidgets.QWidget): add_asset_btn.clicked.connect(self._on_add_asset) add_task_btn.clicked.connect(self._on_add_task) - self.project_model = project_model - self.project_combobox = project_combobox + self._project_model = project_model self.hierarchy_view = hierarchy_view self.hierarchy_model = hierarchy_model self.message_label = message_label + self._refresh_projects_btn = refresh_projects_btn + self._project_combobox = project_combobox + self._create_project_btn = create_project_btn + + self._add_asset_btn = add_asset_btn + self._add_task_btn = add_task_btn + self.resize(1200, 600) self.setStyleSheet(load_stylesheet()) - self.refresh_projects() - def _set_project(self, project_name=None): self.hierarchy_view.set_project(project_name) + def showEvent(self, event): + super(ProjectManagerWindow, self).showEvent(event) + + if not self._initial_reset: + self.reset() + + font_size = self._refresh_projects_btn.fontMetrics().height() + icon_size = QtCore.QSize(font_size, font_size) + self._refresh_projects_btn.setIconSize(icon_size) + self._add_asset_btn.setIconSize(icon_size) + self._add_task_btn.setIconSize(icon_size) + def refresh_projects(self, project_name=None): if project_name is None: - if self.project_combobox.count() > 0: - project_name = self.project_combobox.currentText() + if self._project_combobox.count() > 0: + project_name = self._project_combobox.currentText() - self.project_model.refresh() + self._project_model.refresh() - if self.project_combobox.count() == 0: + if self._project_combobox.count() == 0: return self._set_project() if project_name: - row = self.project_combobox.findText(project_name) + row = self._project_combobox.findText(project_name) if row >= 0: - self.project_combobox.setCurrentIndex(row) + self._project_combobox.setCurrentIndex(row) - self._set_project(self.project_combobox.currentText()) + self._set_project(self._project_combobox.currentText()) def _on_project_change(self): - self._set_project(self.project_combobox.currentText()) + self._set_project(self._project_combobox.currentText()) def _on_project_refresh(self): self.refresh_projects() @@ -174,3 +204,45 @@ class ProjectManagerWindow(QtWidgets.QWidget): project_name = dialog.project_name self.show_message("Created project \"{}\"".format(project_name)) self.refresh_projects(project_name) + + def _show_password_dialog(self): + if self._password_dialog: + self._password_dialog.open() + + def _on_password_dialog_close(self, password_passed): + # Store result for future settings reset + self._user_passed = password_passed + # Remove reference to password dialog + self._password_dialog = None + if password_passed: + self.reset() + else: + self.close() + + def reset(self): + if self._password_dialog: + return + + if not self._user_passed: + self._user_passed = not is_admin_password_required() + + if not self._user_passed: + self.setEnabled(False) + # Avoid doubled dialog + dialog = PasswordDialog(self) + dialog.setModal(True) + dialog.finished.connect(self._on_password_dialog_close) + + self._password_dialog = dialog + + QtCore.QTimer.singleShot(100, self._show_password_dialog) + + return + + self.setEnabled(True) + + # Mark as was reset + if not self._initial_reset: + self._initial_reset = True + + self.refresh_projects() diff --git a/openpype/tools/settings/__init__.py b/openpype/tools/settings/__init__.py index 8f60276cc4..a156228dc1 100644 --- a/openpype/tools/settings/__init__.py +++ b/openpype/tools/settings/__init__.py @@ -1,11 +1,9 @@ import sys from Qt import QtWidgets, QtGui from .lib import ( - is_password_required, BTN_FIXED_SIZE, CHILD_OFFSET ) -from .widgets import PasswordDialog from .local_settings import LocalSettingsWindow from .settings import ( style, @@ -16,14 +14,14 @@ from .settings import ( def main(user_role=None): if user_role is None: - user_role = "artist" - else: - user_role_low = user_role.lower() - allowed_roles = ("developer", "manager", "artist") - if user_role_low not in allowed_roles: - raise ValueError("Invalid user role \"{}\". Expected {}".format( - user_role, ", ".join(allowed_roles) - )) + user_role = "manager" + + user_role_low = user_role.lower() + allowed_roles = ("developer", "manager") + if user_role_low not in allowed_roles: + raise ValueError("Invalid user role \"{}\". Expected {}".format( + user_role, ", ".join(allowed_roles) + )) app = QtWidgets.QApplication(sys.argv) app.setWindowIcon(QtGui.QIcon(style.app_icon_path())) @@ -35,13 +33,11 @@ def main(user_role=None): __all__ = ( - "is_password_required", "BTN_FIXED_SIZE", "CHILD_OFFSET", "style", - "PasswordDialog", "MainWidget", "ProjectListWidget", "LocalSettingsWindow", diff --git a/openpype/tools/settings/lib.py b/openpype/tools/settings/lib.py index 4b48746a18..9520e268dd 100644 --- a/openpype/tools/settings/lib.py +++ b/openpype/tools/settings/lib.py @@ -1,20 +1,2 @@ CHILD_OFFSET = 15 BTN_FIXED_SIZE = 20 - - -def is_password_required(): - from openpype.settings import ( - get_system_settings, - get_local_settings - ) - - system_settings = get_system_settings() - password = system_settings["general"].get("admin_password") - if not password: - return False - - local_settings = get_local_settings() - is_admin = local_settings.get("general", {}).get("is_admin", False) - if is_admin: - return False - return True diff --git a/openpype/tools/settings/local_settings/general_widget.py b/openpype/tools/settings/local_settings/general_widget.py index d01c16ff82..5bb2bcf378 100644 --- a/openpype/tools/settings/local_settings/general_widget.py +++ b/openpype/tools/settings/local_settings/general_widget.py @@ -1,10 +1,8 @@ import getpass from Qt import QtWidgets, QtCore -from openpype.tools.settings import ( - is_password_required, - PasswordDialog -) +from openpype.lib import is_admin_password_required +from openpype.widgets import PasswordDialog class LocalGeneralWidgets(QtWidgets.QWidget): @@ -57,7 +55,7 @@ class LocalGeneralWidgets(QtWidgets.QWidget): if not self.is_admin_input.isChecked(): return - if not is_password_required(): + if not is_admin_password_required(): return dialog = PasswordDialog(self, False) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 01d4babd0f..34ab4c464a 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -73,6 +73,7 @@ class IgnoreInputChangesObj: class SettingsCategoryWidget(QtWidgets.QWidget): state_changed = QtCore.Signal() saved = QtCore.Signal(QtWidgets.QWidget) + restart_required_trigger = QtCore.Signal() def __init__(self, user_role, parent=None): super(SettingsCategoryWidget, self).__init__(parent) @@ -185,9 +186,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): if self.user_role == "developer": self._add_developer_ui(footer_layout) - save_btn = QtWidgets.QPushButton("Save") - spacer_widget = QtWidgets.QWidget() - footer_layout.addWidget(spacer_widget, 1) + save_btn = QtWidgets.QPushButton("Save", footer_widget) + require_restart_label = QtWidgets.QLabel(footer_widget) + require_restart_label.setAlignment(QtCore.Qt.AlignCenter) + footer_layout.addWidget(require_restart_label, 1) footer_layout.addWidget(save_btn, 0) configurations_layout = QtWidgets.QVBoxLayout(configurations_widget) @@ -205,6 +207,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): save_btn.clicked.connect(self._save) self.save_btn = save_btn + self.require_restart_label = require_restart_label self.scroll_widget = scroll_widget self.content_layout = content_layout self.content_widget = content_widget @@ -323,6 +326,15 @@ class SettingsCategoryWidget(QtWidgets.QWidget): def _on_reset_start(self): return + def _on_require_restart_change(self): + value = "" + if self.entity.require_restart: + value = ( + "Your changes require restart of" + " all running OpenPype processes to take affect." + ) + self.require_restart_label.setText(value) + def reset(self): self.set_state(CategoryState.Working) @@ -340,6 +352,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): try: self._create_root_entity() + self.entity.add_require_restart_change_callback( + self._on_require_restart_change + ) + self.add_children_gui() self.ignore_input_changes.set_ignore(True) @@ -349,6 +365,16 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.ignore_input_changes.set_ignore(False) + except DefaultsNotDefined: + dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle("Missing default values") + dialog.setText(( + "Default values are not set and you" + " don't have permissions to modify them." + " Please contact OpenPype team." + )) + dialog.setIcon(QtWidgets.QMessageBox.Critical) + except SchemaError as exc: dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle("Schema error") @@ -433,6 +459,15 @@ class SettingsCategoryWidget(QtWidgets.QWidget): return def _save(self): + # Don't trigger restart if defaults are modified + if ( + self.modify_defaults_checkbox + and self.modify_defaults_checkbox.isChecked() + ): + require_restart = False + else: + require_restart = self.entity.require_restart + self.set_state(CategoryState.Working) if self.items_are_valid(): @@ -442,6 +477,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.saved.emit(self) + if require_restart: + self.restart_required_trigger.emit() + self.require_restart_label.setText("") + def _on_refresh(self): self.reset() @@ -466,12 +505,7 @@ class SystemWidget(SettingsCategoryWidget): self.modify_defaults_checkbox.setEnabled(True) except DefaultsNotDefined: if not self.modify_defaults_checkbox: - msg_box = QtWidgets.QMessageBox( - "BUG: Default values are not set and you" - " don't have permissions to modify them." - ) - msg_box.exec_() - return + raise self.entity.set_defaults_state() self.modify_defaults_checkbox.setChecked(True) @@ -543,12 +577,7 @@ class ProjectWidget(SettingsCategoryWidget): except DefaultsNotDefined: if not self.modify_defaults_checkbox: - msg_box = QtWidgets.QMessageBox( - "BUG: Default values are not set and you" - " don't have permissions to modify them." - ) - msg_box.exec_() - return + raise self.entity.set_defaults_state() self.modify_defaults_checkbox.setChecked(True) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 11ccb60ae4..b23372e9ac 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -508,6 +508,8 @@ class PathWidget(BaseWidget): self.content_layout = QtWidgets.QGridLayout(self) self.content_layout.setContentsMargins(0, 0, 0, 0) self.content_layout.setSpacing(5) + # Add stretch to second column + self.content_layout.setColumnStretch(1, 1) self.body_widget = None self.setAttribute(QtCore.Qt.WA_TranslucentBackground) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 249b4e305d..b20ce5ed66 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -275,8 +275,6 @@ class UnsavedChangesDialog(QtWidgets.QDialog): layout.addWidget(message_label) layout.addWidget(btns_widget) - self.state = None - def on_cancel_pressed(self): self.done(0) @@ -287,6 +285,48 @@ class UnsavedChangesDialog(QtWidgets.QDialog): self.done(2) +class RestartDialog(QtWidgets.QDialog): + message = ( + "Your changes require restart of process to take effect." + " Do you want to restart now?" + ) + + def __init__(self, parent=None): + super(RestartDialog, self).__init__(parent) + message_label = QtWidgets.QLabel(self.message) + + btns_widget = QtWidgets.QWidget(self) + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + + btn_restart = QtWidgets.QPushButton("Restart") + btn_restart.clicked.connect(self.on_restart_pressed) + btn_cancel = QtWidgets.QPushButton("Cancel") + btn_cancel.clicked.connect(self.on_cancel_pressed) + + btns_layout.addStretch(1) + btns_layout.addWidget(btn_restart) + btns_layout.addWidget(btn_cancel) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(message_label) + layout.addWidget(btns_widget) + + self.btn_cancel = btn_cancel + self.btn_restart = btn_restart + + def showEvent(self, event): + super(RestartDialog, self).showEvent(event) + btns_width = max(self.btn_cancel.width(), self.btn_restart.width()) + self.btn_cancel.setFixedWidth(btns_width) + self.btn_restart.setFixedWidth(btns_width) + + def on_cancel_pressed(self): + self.done(0) + + def on_restart_pressed(self): + self.done(1) + + class SpacerWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(SpacerWidget, self).__init__(parent) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 9b368588c3..a60a2a1d88 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -4,24 +4,24 @@ from .categories import ( SystemWidget, ProjectWidget ) -from .widgets import ShadowWidget +from .widgets import ShadowWidget, RestartDialog from . import style -from openpype.tools.settings import ( - is_password_required, - PasswordDialog -) +from openpype.lib import is_admin_password_required +from openpype.widgets import PasswordDialog class MainWidget(QtWidgets.QWidget): + trigger_restart = QtCore.Signal() + widget_width = 1000 widget_height = 600 - def __init__(self, user_role, parent=None): + def __init__(self, user_role, parent=None, reset_on_show=True): super(MainWidget, self).__init__(parent) self._user_passed = False - self._reset_on_show = True + self._reset_on_show = reset_on_show self._password_dialog = None @@ -60,6 +60,9 @@ class MainWidget(QtWidgets.QWidget): for tab_widget in tab_widgets: tab_widget.saved.connect(self._on_tab_save) tab_widget.state_changed.connect(self._on_state_change) + tab_widget.restart_required_trigger.connect( + self._on_restart_required + ) self.tab_widgets = tab_widgets @@ -90,6 +93,7 @@ class MainWidget(QtWidgets.QWidget): def showEvent(self, event): super(MainWidget, self).showEvent(event) if self._reset_on_show: + self._reset_on_show = False self.reset() def _show_password_dialog(self): @@ -111,7 +115,7 @@ class MainWidget(QtWidgets.QWidget): return if not self._user_passed: - self._user_passed = not is_password_required() + self._user_passed = not is_admin_password_required() self._on_state_change() @@ -132,3 +136,15 @@ class MainWidget(QtWidgets.QWidget): for tab_widget in self.tab_widgets: tab_widget.reset() + + def _on_restart_required(self): + # Don't show dialog if there are not registered slots for + # `trigger_restart` signal. + # - For example when settings are runnin as standalone tool + if self.receivers(self.trigger_restart) < 1: + return + + dialog = RestartDialog(self) + result = dialog.exec_() + if result == 1: + self.trigger_restart.emit() diff --git a/openpype/tools/standalonepublish/app.py b/openpype/tools/standalonepublish/app.py index 7d25a8ca55..169abe530a 100644 --- a/openpype/tools/standalonepublish/app.py +++ b/openpype/tools/standalonepublish/app.py @@ -10,7 +10,7 @@ from .widgets import ( AssetWidget, FamilyWidget, ComponentsWidget, ShadowWidget ) from .widgets.constants import HOST_NAME -from avalon import style +from openpype import style from openpype.api import resources from avalon.api import AvalonMongoDB from openpype.modules import ModulesManager @@ -22,7 +22,6 @@ class Window(QtWidgets.QDialog): :param parent: Main widget that cares about all GUIs :type parent: QtWidgets.QMainWindow """ - _db = AvalonMongoDB() _jobs = {} valid_family = False valid_components = False @@ -32,6 +31,7 @@ class Window(QtWidgets.QDialog): def __init__(self, pyblish_paths, parent=None): super(Window, self).__init__(parent=parent) + self._db = AvalonMongoDB() self._db.install() self.pyblish_paths = pyblish_paths diff --git a/openpype/tools/standalonepublish/widgets/widget_component_item.py b/openpype/tools/standalonepublish/widgets/widget_component_item.py index 3850d68b96..186c8024db 100644 --- a/openpype/tools/standalonepublish/widgets/widget_component_item.py +++ b/openpype/tools/standalonepublish/widgets/widget_component_item.py @@ -16,6 +16,9 @@ class ComponentItem(QtWidgets.QFrame): def __init__(self, parent, main_parent): super().__init__() + + self.setObjectName("ComponentItem") + self.has_valid_repre = True self.actions = [] self.resize(290, 70) @@ -32,6 +35,7 @@ class ComponentItem(QtWidgets.QFrame): # Main widgets frame = QtWidgets.QFrame(self) + frame.setObjectName("ComponentFrame") frame.setFrameShape(QtWidgets.QFrame.StyledPanel) frame.setFrameShadow(QtWidgets.QFrame.Raised) diff --git a/openpype/tools/standalonepublish/widgets/widget_components_list.py b/openpype/tools/standalonepublish/widgets/widget_components_list.py index 4e502a2e5f..0ee90ae4de 100644 --- a/openpype/tools/standalonepublish/widgets/widget_components_list.py +++ b/openpype/tools/standalonepublish/widgets/widget_components_list.py @@ -5,6 +5,8 @@ class ComponentsList(QtWidgets.QTableWidget): def __init__(self, parent=None): super().__init__(parent=parent) + self.setObjectName("ComponentList") + self._main_column = 0 self.setColumnCount(1) diff --git a/openpype/tools/standalonepublish/widgets/widget_drop_empty.py b/openpype/tools/standalonepublish/widgets/widget_drop_empty.py index ed526f2a78..a890f38426 100644 --- a/openpype/tools/standalonepublish/widgets/widget_drop_empty.py +++ b/openpype/tools/standalonepublish/widgets/widget_drop_empty.py @@ -21,16 +21,12 @@ class DropEmpty(QtWidgets.QWidget): self._label = QtWidgets.QLabel('Drag & Drop') self._label.setFont(font) - self._label.setStyleSheet( - 'background-color: transparent;' - ) + self._label.setAttribute(QtCore.Qt.WA_TranslucentBackground) font.setPointSize(12) self._sub_label = QtWidgets.QLabel('(drop files here)') self._sub_label.setFont(font) - self._sub_label.setStyleSheet( - 'background-color: transparent;' - ) + self._sub_label.setAttribute(QtCore.Qt.WA_TranslucentBackground) layout.addWidget(self._label, alignment=BottomCenterAlignment) layout.addWidget(self._sub_label, alignment=TopCenterAlignment) diff --git a/openpype/tools/standalonepublish/widgets/widget_family.py b/openpype/tools/standalonepublish/widgets/widget_family.py index 86663c8ee0..682a6fc974 100644 --- a/openpype/tools/standalonepublish/widgets/widget_family.py +++ b/openpype/tools/standalonepublish/widgets/widget_family.py @@ -37,28 +37,26 @@ class FamilyWidget(QtWidgets.QWidget): input_subset = QtWidgets.QLineEdit() input_result = QtWidgets.QLineEdit() - input_result.setStyleSheet("color: #BBBBBB;") input_result.setEnabled(False) # region Menu for default subset names btn_subset = QtWidgets.QPushButton() btn_subset.setFixedWidth(18) - btn_subset.setFixedHeight(20) menu_subset = QtWidgets.QMenu(btn_subset) btn_subset.setMenu(menu_subset) # endregion name_layout = QtWidgets.QHBoxLayout() - name_layout.addWidget(input_subset) - name_layout.addWidget(btn_subset) + name_layout.addWidget(input_subset, 1) + name_layout.addWidget(btn_subset, 0) name_layout.setContentsMargins(0, 0, 0, 0) # version version_spinbox = QtWidgets.QSpinBox() + version_spinbox.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) version_spinbox.setMinimum(1) version_spinbox.setMaximum(9999) version_spinbox.setEnabled(False) - version_spinbox.setStyleSheet("color: #BBBBBB;") version_checkbox = QtWidgets.QCheckBox("Next Available Version") version_checkbox.setCheckState(QtCore.Qt.CheckState(2)) diff --git a/openpype/tools/tray/pype_info_widget.py b/openpype/tools/tray/pype_info_widget.py index bbb92f175f..f95a31f7c2 100644 --- a/openpype/tools/tray/pype_info_widget.py +++ b/openpype/tools/tray/pype_info_widget.py @@ -2,8 +2,9 @@ import os import json import collections -from avalon import style from Qt import QtCore, QtGui, QtWidgets + +from openpype import style from openpype.api import resources from openpype.settings.lib import get_local_settings from openpype.lib.pype_info import ( @@ -118,7 +119,6 @@ class EnvironmentsView(QtWidgets.QTreeView): return super(EnvironmentsView, self).wheelEvent(event) - class ClickableWidget(QtWidgets.QWidget): clicked = QtCore.Signal() @@ -144,16 +144,14 @@ class CollapsibleWidget(QtWidgets.QWidget): button_toggle.setChecked(False) label_widget = QtWidgets.QLabel(label, parent=top_part) - spacer_widget = QtWidgets.QWidget(top_part) top_part_layout = QtWidgets.QHBoxLayout(top_part) top_part_layout.setContentsMargins(0, 0, 0, 5) top_part_layout.addWidget(button_toggle) top_part_layout.addWidget(label_widget) - top_part_layout.addWidget(spacer_widget, 1) + top_part_layout.addStretch(1) label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.button_toggle = button_toggle @@ -297,7 +295,7 @@ class PypeInfoSubWidget(QtWidgets.QWidget): def _create_separator(self): separator_widget = QtWidgets.QWidget(self) - separator_widget.setStyleSheet("background: #222222;") + separator_widget.setObjectName("Separator") separator_widget.setMinimumHeight(2) separator_widget.setMaximumHeight(2) return separator_widget diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 534c99bd90..794312f389 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -1,13 +1,27 @@ +import collections import os import sys +import atexit +import subprocess import platform -from avalon import style + from Qt import QtCore, QtGui, QtWidgets -from openpype.api import Logger, resources -from openpype.modules import TrayModulesManager, ITrayService -from openpype.settings.lib import get_system_settings + import openpype.version +from openpype.api import ( + Logger, + resources, + get_system_settings +) +from openpype.lib import get_pype_execute_args +from openpype.modules import ( + TrayModulesManager, + ITrayAction, + ITrayService +) +from openpype import style + from .pype_info_widget import PypeInfoWidget @@ -20,7 +34,6 @@ class TrayManager: def __init__(self, tray_widget, main_window): self.tray_widget = tray_widget self.main_window = main_window - self.pype_info_widget = None self.log = Logger.get_logger(self.__class__.__name__) @@ -31,11 +44,48 @@ class TrayManager: self.errors = [] + self.main_thread_timer = None + self._main_thread_callbacks = collections.deque() + self._execution_in_progress = None + + @property + def doubleclick_callback(self): + """Doubleclick callback for Tray icon.""" + callback_name = self.modules_manager.doubleclick_callback + return self.modules_manager.doubleclick_callbacks.get(callback_name) + + def execute_doubleclick(self): + """Execute double click callback in main thread.""" + callback = self.doubleclick_callback + if callback: + self.execute_in_main_thread(callback) + + def execute_in_main_thread(self, callback): + self._main_thread_callbacks.append(callback) + + def _main_thread_execution(self): + if self._execution_in_progress: + return + self._execution_in_progress = True + while self._main_thread_callbacks: + try: + callback = self._main_thread_callbacks.popleft() + callback() + except: + self.log.warning( + "Failed to execute {} in main thread".format(callback), + exc_info=True) + + self._execution_in_progress = False + def initialize_modules(self): """Add modules to tray.""" self.modules_manager.initialize(self, self.tray_widget.menu) + admin_submenu = ITrayAction.admin_submenu(self.tray_widget.menu) + self.tray_widget.menu.addMenu(admin_submenu) + # Add services if they are services_submenu = ITrayService.services_submenu(self.tray_widget.menu) self.tray_widget.menu.addMenu(services_submenu) @@ -56,6 +106,14 @@ class TrayManager: # Print time report self.modules_manager.print_report() + # create timer loop to check callback functions + main_thread_timer = QtCore.QTimer() + main_thread_timer.setInterval(300) + main_thread_timer.timeout.connect(self._main_thread_execution) + main_thread_timer.start() + + self.main_thread_timer = main_thread_timer + def show_tray_message(self, title, message, icon=None, msecs=None): """Show tray message. @@ -92,6 +150,34 @@ class TrayManager: self.tray_widget.menu.addAction(version_action) self.tray_widget.menu.addSeparator() + def restart(self): + """Restart Tray tool. + + First creates new process with same argument and close current tray. + """ + args = get_pype_execute_args() + # Create a copy of sys.argv + additional_args = list(sys.argv) + # Check last argument from `get_pype_execute_args` + # - when running from code it is the same as first from sys.argv + if args[-1] == additional_args[0]: + additional_args.pop(0) + args.extend(additional_args) + + kwargs = {} + if platform.system().lower() == "windows": + flags = ( + subprocess.CREATE_NEW_PROCESS_GROUP + | subprocess.DETACHED_PROCESS + ) + kwargs["creationflags"] = flags + + subprocess.Popen(args, **kwargs) + self.exit() + + def exit(self): + self.tray_widget.exit() + def on_exit(self): self.modules_manager.on_exit() @@ -111,11 +197,15 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): :type parent: QtWidgets.QMainWindow """ + doubleclick_time_ms = 100 + def __init__(self, parent): icon = QtGui.QIcon(resources.pype_icon_filepath()) super(SystemTrayIcon, self).__init__(icon, parent) + self._exited = False + # Store parent - QtWidgets.QMainWindow() self.parent = parent @@ -127,24 +217,60 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): self.tray_man = TrayManager(self, self.parent) self.tray_man.initialize_modules() - # Catch activate event for left click if not on MacOS - # - MacOS has this ability by design so menu would be doubled - if platform.system().lower() != "darwin": - self.activated.connect(self.on_systray_activated) # Add menu to Context of SystemTrayIcon self.setContextMenu(self.menu) + atexit.register(self.exit) + + # Catch activate event for left click if not on MacOS + # - MacOS has this ability by design and is harder to modify this + # behavior + if platform.system().lower() == "darwin": + return + + self.activated.connect(self.on_systray_activated) + + click_timer = QtCore.QTimer() + click_timer.setInterval(self.doubleclick_time_ms) + click_timer.timeout.connect(self._click_timer_timeout) + + self._click_timer = click_timer + self._doubleclick = False + + def _click_timer_timeout(self): + self._click_timer.stop() + doubleclick = self._doubleclick + # Reset bool value + self._doubleclick = False + if doubleclick: + self.tray_man.execute_doubleclick() + else: + self._show_context_menu() + + def _show_context_menu(self): + pos = QtGui.QCursor().pos() + self.contextMenu().popup(pos) + def on_systray_activated(self, reason): # show contextMenu if left click if reason == QtWidgets.QSystemTrayIcon.Trigger: - position = QtGui.QCursor().pos() - self.contextMenu().popup(position) + if self.tray_man.doubleclick_callback: + self._click_timer.start() + else: + self._show_context_menu() + + elif reason == QtWidgets.QSystemTrayIcon.DoubleClick: + self._doubleclick = True def exit(self): """ Exit whole application. - Icon won't stay in tray after exit. """ + if self._exited: + return + self._exited = True + self.hide() self.tray_man.on_exit() QtCore.QCoreApplication.exit() diff --git a/openpype/vendor/python/common/capture_gui/vendor/__init__.py b/openpype/tools/tray_app/__init__.py similarity index 100% rename from openpype/vendor/python/common/capture_gui/vendor/__init__.py rename to openpype/tools/tray_app/__init__.py diff --git a/openpype/tools/tray_app/app.py b/openpype/tools/tray_app/app.py new file mode 100644 index 0000000000..339e6343f8 --- /dev/null +++ b/openpype/tools/tray_app/app.py @@ -0,0 +1,331 @@ +import os +import sys +import re +import collections +import queue +import websocket +import json +import itertools +from datetime import datetime + +from avalon import style +from openpype.modules.webserver import host_console_listener + +from Qt import QtWidgets, QtCore + + +class ConsoleTrayApp: + """ + Application showing console in Services tray for non python hosts + instead of cmd window. + """ + callback_queue = None + process = None + webserver_client = None + + MAX_LINES = 10000 + + sdict = { + r">>> ": + ' >>> ', + r"!!!(?!\sCRI|\sERR)": + ' !!! ', + r"\-\-\- ": + ' --- ', + r"\*\*\*(?!\sWRN)": + ' *** ', + r"\*\*\* WRN": + ' *** WRN', + r" \- ": + ' - ', + r"\[ ": + '[', + r"\]": + ']', + r"{": + '{', + r"}": + r"}", + r"\(": + '(', + r"\)": + r")", + r"^\.\.\. ": + ' ... ', + r"!!! ERR: ": + ' !!! ERR: ', + r"!!! CRI: ": + ' !!! CRI: ', + r"(?i)failed": + ' FAILED ', + r"(?i)error": + ' ERROR ' + } + + def __init__(self, host, launch_method, subprocess_args, is_host_connected, + parent=None): + self.host = host + + self.initialized = False + self.websocket_server = None + self.initializing = False + self.tray = False + self.launch_method = launch_method + self.subprocess_args = subprocess_args + self.is_host_connected = is_host_connected + self.tray_reconnect = True + + self.original_stdout_write = None + self.original_stderr_write = None + self.new_text = collections.deque() + + timer = QtCore.QTimer() + timer.timeout.connect(self.on_timer) + timer.setInterval(200) + timer.start() + + self.timer = timer + + self.catch_std_outputs() + date_str = datetime.now().strftime("%d%m%Y%H%M%S") + self.host_id = "{}_{}".format(self.host, date_str) + + def _connect(self): + """ Connect to Tray webserver to pass console output. """ + ws = websocket.WebSocket() + webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") + + if not webserver_url: + print("Unknown webserver url, cannot connect to pass log") + self.tray_reconnect = False + return + + webserver_url = webserver_url.replace("http", "ws") + ws.connect("{}/ws/host_listener".format(webserver_url)) + ConsoleTrayApp.webserver_client = ws + + payload = { + "host": self.host_id, + "action": host_console_listener.MsgAction.CONNECTING, + "text": "Integration with {}".format(str.capitalize(self.host)) + } + self.tray_reconnect = False + self._send(payload) + + def _connected(self): + """ Send to Tray console that host is ready - icon change. """ + print("Host {} connected".format(self.host)) + if not ConsoleTrayApp.webserver_client: + return + + payload = { + "host": self.host_id, + "action": host_console_listener.MsgAction.INITIALIZED, + "text": "Integration with {}".format(str.capitalize(self.host)) + } + self.tray_reconnect = False + self._send(payload) + + def _close(self): + """ Send to Tray that host is closing - remove from Services. """ + print("Host {} closing".format(self.host)) + if not ConsoleTrayApp.webserver_client: + return + + payload = { + "host": self.host_id, + "action": host_console_listener.MsgAction.CLOSE, + "text": "Integration with {}".format(str.capitalize(self.host)) + } + + self._send(payload) + self.tray_reconnect = False + ConsoleTrayApp.webserver_client.close() + + def _send_text(self, new_text): + """ Send console content. """ + if not ConsoleTrayApp.webserver_client: + return + + if isinstance(new_text, str): + new_text = collections.deque(new_text.split("\n")) + + payload = { + "host": self.host_id, + "action": host_console_listener.MsgAction.ADD, + "text": "\n".join(new_text) + } + + self._send(payload) + + def _send(self, payload): + """ Worker method to send to existing websocket connection. """ + if not ConsoleTrayApp.webserver_client: + return + + try: + ConsoleTrayApp.webserver_client.send(json.dumps(payload)) + except ConnectionResetError: # Tray closed + ConsoleTrayApp.webserver_client = None + self.tray_reconnect = True + + def on_timer(self): + """Called periodically to initialize and run function on main thread""" + if self.tray_reconnect: + self._connect() # reconnect + + if ConsoleTrayApp.webserver_client and self.new_text: + self._send_text(self.new_text) + self.new_text = collections.deque() + + if self.new_text: # no webserver_client, text keeps stashing + start = max(len(self.new_text) - self.MAX_LINES, 0) + self.new_text = itertools.islice(self.new_text, + start, self.MAX_LINES) + + if not self.initialized: + if self.initializing: + host_connected = self.is_host_connected() + if host_connected is None: # keep trying + return + elif not host_connected: + text = "{} process is not alive. Exiting".format(self.host) + print(text) + self._send_text([text]) + ConsoleTrayApp.websocket_server.stop() + sys.exit(1) + elif host_connected: + self.initialized = True + self.initializing = False + self._connected() + + return + + ConsoleTrayApp.callback_queue = queue.Queue() + self.initializing = True + + self.launch_method(*self.subprocess_args) + elif ConsoleTrayApp.process.poll() is not None: + self.exit() + elif ConsoleTrayApp.callback_queue: + try: + callback = ConsoleTrayApp.callback_queue.get(block=False) + callback() + except queue.Empty: + pass + + @classmethod + def execute_in_main_thread(cls, func_to_call_from_main_thread): + """Put function to the queue to be picked by 'on_timer'""" + if not cls.callback_queue: + cls.callback_queue = queue.Queue() + cls.callback_queue.put(func_to_call_from_main_thread) + + @classmethod + def restart_server(cls): + if ConsoleTrayApp.websocket_server: + ConsoleTrayApp.websocket_server.stop_server(restart=True) + + # obsolete + def exit(self): + """ Exit whole application. """ + self._close() + if ConsoleTrayApp.websocket_server: + ConsoleTrayApp.websocket_server.stop() + ConsoleTrayApp.process.kill() + ConsoleTrayApp.process.wait() + if self.timer: + self.timer.stop() + QtCore.QCoreApplication.exit() + + def catch_std_outputs(self): + """Redirects standard out and error to own functions""" + if not sys.stdout: + self.dialog.append_text("Cannot read from stdout!") + else: + self.original_stdout_write = sys.stdout.write + sys.stdout.write = self.my_stdout_write + + if not sys.stderr: + self.dialog.append_text("Cannot read from stderr!") + else: + self.original_stderr_write = sys.stderr.write + sys.stderr.write = self.my_stderr_write + + def my_stdout_write(self, text): + """Appends outputted text to queue, keep writing to original stdout""" + if self.original_stdout_write is not None: + self.original_stdout_write(text) + self.new_text.append(text) + + def my_stderr_write(self, text): + """Appends outputted text to queue, keep writing to original stderr""" + if self.original_stderr_write is not None: + self.original_stderr_write(text) + self.new_text.append(text) + + @staticmethod + def _multiple_replace(text, adict): + """Replace multiple tokens defined in dict. + + Find and replace all occurances of strings defined in dict is + supplied string. + + Args: + text (str): string to be searched + adict (dict): dictionary with `{'search': 'replace'}` + + Returns: + str: string with replaced tokens + + """ + for r, v in adict.items(): + text = re.sub(r, v, text) + + return text + + @staticmethod + def color(message): + """ Color message with html tags. """ + message = ConsoleTrayApp._multiple_replace(message, + ConsoleTrayApp.sdict) + + return message + + +class ConsoleDialog(QtWidgets.QDialog): + """Qt dialog to show stdout instead of unwieldy cmd window""" + WIDTH = 720 + HEIGHT = 450 + MAX_LINES = 10000 + + def __init__(self, text, parent=None): + super(ConsoleDialog, self).__init__(parent) + layout = QtWidgets.QHBoxLayout(parent) + + plain_text = QtWidgets.QPlainTextEdit(self) + plain_text.setReadOnly(True) + plain_text.resize(self.WIDTH, self.HEIGHT) + plain_text.maximumBlockCount = self.MAX_LINES + + while text: + plain_text.appendPlainText(text.popleft().strip()) + + layout.addWidget(plain_text) + + self.setWindowTitle("Console output") + + self.plain_text = plain_text + + self.setStyleSheet(style.load_stylesheet()) + + self.resize(self.WIDTH, self.HEIGHT) + + def append_text(self, new_text): + if isinstance(new_text, str): + new_text = collections.deque(new_text.split("\n")) + while new_text: + text = new_text.popleft() + if text: + self.plain_text.appendHtml( + ConsoleTrayApp.color(text)) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index c08f06066e..c79e55a143 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -925,6 +925,8 @@ class Window(QtWidgets.QMainWindow): home_body_widget = QtWidgets.QWidget(home_page_widget) assets_widget = AssetWidget(io, parent=home_body_widget) + assets_widget.set_current_asset_btn_visibility(True) + tasks_widget = TasksWidget(home_body_widget) files_widget = FilesWidget(home_body_widget) side_panel = SidePanelWidget(home_body_widget) diff --git a/openpype/vendor/python/common/capture_gui/__init__.py b/openpype/vendor/python/common/capture_gui/__init__.py deleted file mode 100644 index 6c6a813636..0000000000 --- a/openpype/vendor/python/common/capture_gui/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -from .vendor.Qt import QtWidgets -from . import app -from . import lib - - -def main(show=True): - """Convenience method to run the Application inside Maya. - - Args: - show (bool): Whether to directly show the instantiated application. - Defaults to True. Set this to False if you want to manage the - application (like callbacks) prior to showing the interface. - - Returns: - capture_gui.app.App: The pyblish gui application instance. - - """ - # get main maya window to parent widget to - parent = lib.get_maya_main_window() - instance = parent.findChild(QtWidgets.QWidget, app.App.object_name) - if instance: - instance.close() - - # launch app - window = app.App(title="Capture GUI", parent=parent) - if show: - window.show() - - return window diff --git a/openpype/vendor/python/common/capture_gui/accordion.py b/openpype/vendor/python/common/capture_gui/accordion.py deleted file mode 100644 index f721837c57..0000000000 --- a/openpype/vendor/python/common/capture_gui/accordion.py +++ /dev/null @@ -1,624 +0,0 @@ -from .vendor.Qt import QtCore, QtWidgets, QtGui - - -class AccordionItem(QtWidgets.QGroupBox): - trigger = QtCore.Signal(bool) - - def __init__(self, accordion, title, widget): - QtWidgets.QGroupBox.__init__(self, parent=accordion) - - # create the layout - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(6, 12, 6, 6) - layout.setSpacing(0) - layout.addWidget(widget) - - self._accordianWidget = accordion - self._rolloutStyle = 2 - self._dragDropMode = 0 - - self.setAcceptDrops(True) - self.setLayout(layout) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.showMenu) - - # create custom properties - self._widget = widget - self._collapsed = False - self._collapsible = True - self._clicked = False - self._customData = {} - - # set common properties - self.setTitle(title) - - def accordionWidget(self): - """ - \remarks grabs the parent item for the accordian widget - \return - """ - return self._accordianWidget - - def customData(self, key, default=None): - """ - \remarks return a custom pointer to information stored with this item - \param key - \param default default value to return if the key was not found - \return data - """ - return self._customData.get(str(key), default) - - def dragEnterEvent(self, event): - if not self._dragDropMode: - return - - source = event.source() - if source != self and source.parent() == self.parent() and isinstance( - source, AccordionItem): - event.acceptProposedAction() - - def dragDropRect(self): - return QtCore.QRect(25, 7, 10, 6) - - def dragDropMode(self): - return self._dragDropMode - - def dragMoveEvent(self, event): - if not self._dragDropMode: - return - - source = event.source() - if source != self and source.parent() == self.parent() and isinstance( - source, AccordionItem): - event.acceptProposedAction() - - def dropEvent(self, event): - widget = event.source() - layout = self.parent().layout() - layout.insertWidget(layout.indexOf(self), widget) - self._accordianWidget.emitItemsReordered() - - def expandCollapseRect(self): - return QtCore.QRect(0, 0, self.width(), 20) - - def enterEvent(self, event): - self.accordionWidget().leaveEvent(event) - event.accept() - - def leaveEvent(self, event): - self.accordionWidget().enterEvent(event) - event.accept() - - def mouseReleaseEvent(self, event): - if self._clicked and self.expandCollapseRect().contains(event.pos()): - self.toggleCollapsed() - event.accept() - else: - event.ignore() - - self._clicked = False - - def mouseMoveEvent(self, event): - event.ignore() - - def mousePressEvent(self, event): - # handle an internal move - - # start a drag event - if event.button() == QtCore.Qt.LeftButton and self.dragDropRect().contains( - event.pos()): - # create the pixmap - pixmap = QtGui.QPixmap.grabWidget(self, self.rect()) - - # create the mimedata - mimeData = QtCore.QMimeData() - mimeData.setText('ItemTitle::%s' % (self.title())) - - # create the drag - drag = QtGui.QDrag(self) - drag.setMimeData(mimeData) - drag.setPixmap(pixmap) - drag.setHotSpot(event.pos()) - - if not drag.exec_(): - self._accordianWidget.emitItemDragFailed(self) - - event.accept() - - # determine if the expand/collapse should occur - elif event.button() == QtCore.Qt.LeftButton and self.expandCollapseRect().contains( - event.pos()): - self._clicked = True - event.accept() - - else: - event.ignore() - - def isCollapsed(self): - return self._collapsed - - def isCollapsible(self): - return self._collapsible - - def __drawTriangle(self, painter, x, y): - - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 160), - QtCore.Qt.SolidPattern) - if not self.isCollapsed(): - tl, tr, tp = QtCore.QPoint(x + 9, y + 8), QtCore.QPoint(x + 19, - y + 8), QtCore.QPoint( - x + 14, y + 13.0) - points = [tl, tr, tp] - triangle = QtGui.QPolygon(points) - else: - tl, tr, tp = QtCore.QPoint(x + 11, y + 6), QtCore.QPoint(x + 16, - y + 11), QtCore.QPoint( - x + 11, y + 16.0) - points = [tl, tr, tp] - triangle = QtGui.QPolygon(points) - - currentBrush = painter.brush() - painter.setBrush(brush) - painter.drawPolygon(triangle) - painter.setBrush(currentBrush) - - def paintEvent(self, event): - painter = QtGui.QPainter() - painter.begin(self) - painter.setRenderHint(painter.Antialiasing) - font = painter.font() - font.setBold(True) - painter.setFont(font) - - x = self.rect().x() - y = self.rect().y() - w = self.rect().width() - 1 - h = self.rect().height() - 1 - r = 8 - - # draw a rounded style - if self._rolloutStyle == 2: - # draw the text - painter.drawText(x + 33, y + 3, w, 16, - QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop, - self.title()) - - # draw the triangle - self.__drawTriangle(painter, x, y) - - # draw the borders - pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Light)) - pen.setWidthF(0.6) - painter.setPen(pen) - - painter.drawRoundedRect(x + 1, y + 1, w - 1, h - 1, r, r) - - pen.setColor(self.palette().color(QtGui.QPalette.Shadow)) - painter.setPen(pen) - - painter.drawRoundedRect(x, y, w - 1, h - 1, r, r) - - # draw a square style - if self._rolloutStyle == 3: - # draw the text - painter.drawText(x + 33, y + 3, w, 16, - QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop, - self.title()) - - self.__drawTriangle(painter, x, y) - - # draw the borders - pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Light)) - pen.setWidthF(0.6) - painter.setPen(pen) - - painter.drawRect(x + 1, y + 1, w - 1, h - 1) - - pen.setColor(self.palette().color(QtGui.QPalette.Shadow)) - painter.setPen(pen) - - painter.drawRect(x, y, w - 1, h - 1) - - # draw a Maya style - if self._rolloutStyle == 4: - # draw the text - painter.drawText(x + 33, y + 3, w, 16, - QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop, - self.title()) - - painter.setRenderHint(QtGui.QPainter.Antialiasing, False) - - self.__drawTriangle(painter, x, y) - - # draw the borders - top - headerHeight = 20 - - headerRect = QtCore.QRect(x + 1, y + 1, w - 1, headerHeight) - headerRectShadow = QtCore.QRect(x - 1, y - 1, w + 1, - headerHeight + 2) - - # Highlight - pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Light)) - pen.setWidthF(0.4) - painter.setPen(pen) - - painter.drawRect(headerRect) - painter.fillRect(headerRect, QtGui.QColor(255, 255, 255, 18)) - - # Shadow - pen.setColor(self.palette().color(QtGui.QPalette.Dark)) - painter.setPen(pen) - painter.drawRect(headerRectShadow) - - if not self.isCollapsed(): - # draw the lover border - pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Dark)) - pen.setWidthF(0.8) - painter.setPen(pen) - - offSet = headerHeight + 3 - bodyRect = QtCore.QRect(x, y + offSet, w, h - offSet) - bodyRectShadow = QtCore.QRect(x + 1, y + offSet, w + 1, - h - offSet + 1) - painter.drawRect(bodyRect) - - pen.setColor(self.palette().color(QtGui.QPalette.Light)) - pen.setWidthF(0.4) - painter.setPen(pen) - - painter.drawRect(bodyRectShadow) - - # draw a boxed style - elif self._rolloutStyle == 1: - if self.isCollapsed(): - arect = QtCore.QRect(x + 1, y + 9, w - 1, 4) - brect = QtCore.QRect(x, y + 8, w - 1, 4) - text = '+' - else: - arect = QtCore.QRect(x + 1, y + 9, w - 1, h - 9) - brect = QtCore.QRect(x, y + 8, w - 1, h - 9) - text = '-' - - # draw the borders - pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Light)) - pen.setWidthF(0.6) - painter.setPen(pen) - - painter.drawRect(arect) - - pen.setColor(self.palette().color(QtGui.QPalette.Shadow)) - painter.setPen(pen) - - painter.drawRect(brect) - - painter.setRenderHint(painter.Antialiasing, False) - painter.setBrush( - self.palette().color(QtGui.QPalette.Window).darker(120)) - painter.drawRect(x + 10, y + 1, w - 20, 16) - painter.drawText(x + 16, y + 1, - w - 32, 16, - QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, - text) - painter.drawText(x + 10, y + 1, - w - 20, 16, - QtCore.Qt.AlignCenter, - self.title()) - - if self.dragDropMode(): - rect = self.dragDropRect() - - # draw the lines - l = rect.left() - r = rect.right() - cy = rect.center().y() - - for y in (cy - 3, cy, cy + 3): - painter.drawLine(l, y, r, y) - - painter.end() - - def setCollapsed(self, state=True): - if self.isCollapsible(): - accord = self.accordionWidget() - accord.setUpdatesEnabled(False) - - self._collapsed = state - - if state: - self.setMinimumHeight(22) - self.setMaximumHeight(22) - self.widget().setVisible(False) - else: - self.setMinimumHeight(0) - self.setMaximumHeight(1000000) - self.widget().setVisible(True) - - self._accordianWidget.emitItemCollapsed(self) - accord.setUpdatesEnabled(True) - - def setCollapsible(self, state=True): - self._collapsible = state - - def setCustomData(self, key, value): - """ - \remarks set a custom pointer to information stored on this item - \param key - \param value - """ - self._customData[str(key)] = value - - def setDragDropMode(self, mode): - self._dragDropMode = mode - - def setRolloutStyle(self, style): - self._rolloutStyle = style - - def showMenu(self): - if QtCore.QRect(0, 0, self.width(), 20).contains( - self.mapFromGlobal(QtGui.QCursor.pos())): - self._accordianWidget.emitItemMenuRequested(self) - - def rolloutStyle(self): - return self._rolloutStyle - - def toggleCollapsed(self): - # enable signaling here - collapse_state = not self.isCollapsed() - self.setCollapsed(collapse_state) - return collapse_state - - def widget(self): - return self._widget - - -class AccordionWidget(QtWidgets.QScrollArea): - """Accordion style widget. - - A collapsible accordion widget like Maya's attribute editor. - - This is a modified version bsed on Blur's Accordion Widget to - include a Maya style. - - """ - itemCollapsed = QtCore.Signal(AccordionItem) - itemMenuRequested = QtCore.Signal(AccordionItem) - itemDragFailed = QtCore.Signal(AccordionItem) - itemsReordered = QtCore.Signal() - - Boxed = 1 - Rounded = 2 - Square = 3 - Maya = 4 - - NoDragDrop = 0 - InternalMove = 1 - - def __init__(self, parent): - - QtWidgets.QScrollArea.__init__(self, parent) - - self.setFrameShape(QtWidgets.QScrollArea.NoFrame) - self.setAutoFillBackground(False) - self.setWidgetResizable(True) - self.setMouseTracking(True) - self.verticalScrollBar().setMaximumWidth(10) - - widget = QtWidgets.QWidget(self) - - # define custom properties - self._rolloutStyle = AccordionWidget.Rounded - self._dragDropMode = AccordionWidget.NoDragDrop - self._scrolling = False - self._scrollInitY = 0 - self._scrollInitVal = 0 - self._itemClass = AccordionItem - - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(2, 2, 2, 6) - layout.setSpacing(2) - layout.addStretch(1) - - widget.setLayout(layout) - - self.setWidget(widget) - - def setSpacing(self, spaceInt): - self.widget().layout().setSpacing(spaceInt) - - def addItem(self, title, widget, collapsed=False): - self.setUpdatesEnabled(False) - item = self._itemClass(self, title, widget) - item.setRolloutStyle(self.rolloutStyle()) - item.setDragDropMode(self.dragDropMode()) - layout = self.widget().layout() - layout.insertWidget(layout.count() - 1, item) - layout.setStretchFactor(item, 0) - - if collapsed: - item.setCollapsed(collapsed) - - self.setUpdatesEnabled(True) - - return item - - def clear(self): - self.setUpdatesEnabled(False) - layout = self.widget().layout() - while layout.count() > 1: - item = layout.itemAt(0) - - # remove the item from the layout - w = item.widget() - layout.removeItem(item) - - # close the widget and delete it - w.close() - w.deleteLater() - - self.setUpdatesEnabled(True) - - def eventFilter(self, object, event): - if event.type() == QtCore.QEvent.MouseButtonPress: - self.mousePressEvent(event) - return True - - elif event.type() == QtCore.QEvent.MouseMove: - self.mouseMoveEvent(event) - return True - - elif event.type() == QtCore.QEvent.MouseButtonRelease: - self.mouseReleaseEvent(event) - return True - - return False - - def canScroll(self): - return self.verticalScrollBar().maximum() > 0 - - def count(self): - return self.widget().layout().count() - 1 - - def dragDropMode(self): - return self._dragDropMode - - def indexOf(self, widget): - """ - \remarks Searches for widget(not including child layouts). - Returns the index of widget, or -1 if widget is not found - \return - """ - layout = self.widget().layout() - for index in range(layout.count()): - if layout.itemAt(index).widget().widget() == widget: - return index - return -1 - - def isBoxedMode(self): - return self._rolloutStyle == AccordionWidget.Maya - - def itemClass(self): - return self._itemClass - - def itemAt(self, index): - layout = self.widget().layout() - if 0 <= index and index < layout.count() - 1: - return layout.itemAt(index).widget() - return None - - def emitItemCollapsed(self, item): - if not self.signalsBlocked(): - self.itemCollapsed.emit(item) - - def emitItemDragFailed(self, item): - if not self.signalsBlocked(): - self.itemDragFailed.emit(item) - - def emitItemMenuRequested(self, item): - if not self.signalsBlocked(): - self.itemMenuRequested.emit(item) - - def emitItemsReordered(self): - if not self.signalsBlocked(): - self.itemsReordered.emit() - - def enterEvent(self, event): - if self.canScroll(): - QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.OpenHandCursor) - - def leaveEvent(self, event): - if self.canScroll(): - QtWidgets.QApplication.restoreOverrideCursor() - - def mouseMoveEvent(self, event): - if self._scrolling: - sbar = self.verticalScrollBar() - smax = sbar.maximum() - - # calculate the distance moved for the moust point - dy = event.globalY() - self._scrollInitY - - # calculate the percentage that is of the scroll bar - dval = smax * (dy / float(sbar.height())) - - # calculate the new value - sbar.setValue(self._scrollInitVal - dval) - - event.accept() - - def mousePressEvent(self, event): - # handle a scroll event - if event.button() == QtCore.Qt.LeftButton and self.canScroll(): - self._scrolling = True - self._scrollInitY = event.globalY() - self._scrollInitVal = self.verticalScrollBar().value() - - QtWidgets.QApplication.setOverrideCursor( - QtCore.Qt.ClosedHandCursor) - - event.accept() - - def mouseReleaseEvent(self, event): - if self._scrolling: - QtWidgets.QApplication.restoreOverrideCursor() - - self._scrolling = False - self._scrollInitY = 0 - self._scrollInitVal = 0 - event.accept() - - def moveItemDown(self, index): - layout = self.widget().layout() - if (layout.count() - 1) > (index + 1): - widget = layout.takeAt(index).widget() - layout.insertWidget(index + 1, widget) - - def moveItemUp(self, index): - if index > 0: - layout = self.widget().layout() - widget = layout.takeAt(index).widget() - layout.insertWidget(index - 1, widget) - - def setBoxedMode(self, state): - if state: - self._rolloutStyle = AccordionWidget.Boxed - else: - self._rolloutStyle = AccordionWidget.Rounded - - def setDragDropMode(self, dragDropMode): - self._dragDropMode = dragDropMode - - for item in self.findChildren(AccordionItem): - item.setDragDropMode(self._dragDropMode) - - def setItemClass(self, itemClass): - self._itemClass = itemClass - - def setRolloutStyle(self, rolloutStyle): - self._rolloutStyle = rolloutStyle - - for item in self.findChildren(AccordionItem): - item.setRolloutStyle(self._rolloutStyle) - - def rolloutStyle(self): - return self._rolloutStyle - - def takeAt(self, index): - self.setUpdatesEnabled(False) - layout = self.widget().layout() - widget = None - if 0 <= index and index < layout.count() - 1: - item = layout.itemAt(index) - widget = item.widget() - - layout.removeItem(item) - widget.close() - self.setUpdatesEnabled(True) - return widget - - def widgetAt(self, index): - item = self.itemAt(index) - if item: - return item.widget() - return None - - pyBoxedMode = QtCore.Property('bool', isBoxedMode, setBoxedMode) diff --git a/openpype/vendor/python/common/capture_gui/app.py b/openpype/vendor/python/common/capture_gui/app.py deleted file mode 100644 index 1860b084ba..0000000000 --- a/openpype/vendor/python/common/capture_gui/app.py +++ /dev/null @@ -1,711 +0,0 @@ -import json -import logging -import os -import tempfile - -import capture -import maya.cmds as cmds - -from .vendor.Qt import QtCore, QtWidgets, QtGui -from . import lib -from . import plugin -from . import presets -from . import version -from . import tokens -from .accordion import AccordionWidget - -log = logging.getLogger("Capture Gui") - - -class ClickLabel(QtWidgets.QLabel): - """A QLabel that emits a clicked signal when clicked upon.""" - clicked = QtCore.Signal() - - def mouseReleaseEvent(self, event): - self.clicked.emit() - return super(ClickLabel, self).mouseReleaseEvent(event) - - -class PreviewWidget(QtWidgets.QWidget): - """The playblast image preview widget. - - Upon refresh it will retrieve the options through the function set as - `options_getter` and make a call to `capture.capture()` for a single - frame (playblasted) snapshot. The result is displayed as image. - """ - - preview_width = 320 - preview_height = 180 - - def __init__(self, options_getter, validator, parent=None): - QtWidgets.QWidget.__init__(self, parent=parent) - - # Add attributes - self.options_getter = options_getter - self.validator = validator - self.preview = ClickLabel() - self.preview.setFixedWidth(self.preview_width) - self.preview.setFixedHeight(self.preview_height) - - tip = "Click to force a refresh" - self.preview.setToolTip(tip) - self.preview.setStatusTip(tip) - - # region Build - self.layout = QtWidgets.QVBoxLayout() - self.layout.setAlignment(QtCore.Qt.AlignHCenter) - self.layout.setContentsMargins(0, 0, 0, 0) - - self.setLayout(self.layout) - self.layout.addWidget(self.preview) - # endregion Build - - # Connect widgets to functions - self.preview.clicked.connect(self.refresh) - - def refresh(self): - """Refresh the playblast preview""" - - frame = cmds.currentTime(query=True) - - # When playblasting outside of an undo queue it seems that undoing - # actually triggers a reset to frame 0. As such we sneak in the current - # time into the undo queue to enforce correct undoing. - cmds.currentTime(frame, update=True) - - # check if plugin outputs are correct - valid = self.validator() - if not valid: - return - - with lib.no_undo(): - options = self.options_getter() - tempdir = tempfile.mkdtemp() - - # override settings that are constants for the preview - options = options.copy() - options['filename'] = None - options['complete_filename'] = os.path.join(tempdir, "temp.jpg") - options['width'] = self.preview_width - options['height'] = self.preview_height - options['viewer'] = False - options['frame'] = frame - options['off_screen'] = True - options['format'] = "image" - options['compression'] = "jpg" - options['sound'] = None - - fname = capture.capture(**options) - if not fname: - log.warning("Preview failed") - return - - image = QtGui.QPixmap(fname) - self.preview.setPixmap(image) - os.remove(fname) - - def showEvent(self, event): - """Initialize when shown""" - self.refresh() - event.accept() - - -class PresetWidget(QtWidgets.QWidget): - """Preset Widget - - Allows the user to set preferences and create presets to load before - capturing. - - """ - - preset_loaded = QtCore.Signal(dict) - config_opened = QtCore.Signal() - - id = "Presets" - label = "Presets" - - def __init__(self, inputs_getter, parent=None): - QtWidgets.QWidget.__init__(self, parent=parent) - - self.inputs_getter = inputs_getter - - layout = QtWidgets.QHBoxLayout(self) - layout.setAlignment(QtCore.Qt.AlignCenter) - layout.setContentsMargins(0, 0, 0, 0) - - presets = QtWidgets.QComboBox() - presets.setFixedWidth(220) - presets.addItem("*") - - # Icons - icon_path = os.path.join(os.path.dirname(__file__), "resources") - save_icon = os.path.join(icon_path, "save.png") - load_icon = os.path.join(icon_path, "import.png") - config_icon = os.path.join(icon_path, "config.png") - - # Create buttons - save = QtWidgets.QPushButton() - save.setIcon(QtGui.QIcon(save_icon)) - save.setFixedWidth(30) - save.setToolTip("Save Preset") - save.setStatusTip("Save Preset") - - load = QtWidgets.QPushButton() - load.setIcon(QtGui.QIcon(load_icon)) - load.setFixedWidth(30) - load.setToolTip("Load Preset") - load.setStatusTip("Load Preset") - - config = QtWidgets.QPushButton() - config.setIcon(QtGui.QIcon(config_icon)) - config.setFixedWidth(30) - config.setToolTip("Preset configuration") - config.setStatusTip("Preset configuration") - - layout.addWidget(presets) - layout.addWidget(save) - layout.addWidget(load) - layout.addWidget(config) - - # Make available for all methods - self.presets = presets - self.config = config - self.load = load - self.save = save - - # Signals - self.save.clicked.connect(self.on_save_preset) - self.load.clicked.connect(self.import_preset) - self.config.clicked.connect(self.config_opened) - self.presets.currentIndexChanged.connect(self.load_active_preset) - - self._process_presets() - - def _process_presets(self): - """Adds all preset files from preset paths to the Preset widget. - - Returns: - None - - """ - for presetfile in presets.discover(): - self.add_preset(presetfile) - - def import_preset(self): - """Load preset files to override output values""" - - path = self._default_browse_path() - filters = "Text file (*.json)" - dialog = QtWidgets.QFileDialog - filename, _ = dialog.getOpenFileName(self, "Open preference file", - path, filters) - if not filename: - return - - # create new entry in combobox - self.add_preset(filename) - - # read file - return self.load_active_preset() - - def load_active_preset(self): - """Load the active preset. - - Returns: - dict: The preset inputs. - - """ - current_index = self.presets.currentIndex() - filename = self.presets.itemData(current_index) - if not filename: - return {} - - preset = lib.load_json(filename) - - # Emit preset load signal - log.debug("Emitting preset_loaded: {0}".format(filename)) - self.preset_loaded.emit(preset) - - # Ensure we preserve the index after loading the changes - # for all the plugin widgets - self.presets.blockSignals(True) - self.presets.setCurrentIndex(current_index) - self.presets.blockSignals(False) - - return preset - - def add_preset(self, filename): - """Add the filename to the preset list. - - This also sets the index to the filename. - - Returns: - None - - """ - - filename = os.path.normpath(filename) - if not os.path.exists(filename): - log.warning("Preset file does not exist: {0}".format(filename)) - return - - label = os.path.splitext(os.path.basename(filename))[0] - item_count = self.presets.count() - - paths = [self.presets.itemData(i) for i in range(item_count)] - if filename in paths: - log.info("Preset is already in the " - "presets list: {0}".format(filename)) - item_index = paths.index(filename) - else: - self.presets.addItem(label, userData=filename) - item_index = item_count - - self.presets.blockSignals(True) - self.presets.setCurrentIndex(item_index) - self.presets.blockSignals(False) - - return item_index - - def _default_browse_path(self): - """Return the current browse path for save/load preset. - - If a preset is currently loaded it will use that specific path - otherwise it will go to the last registered preset path. - - Returns: - str: Path to use as default browse location. - - """ - - current_index = self.presets.currentIndex() - path = self.presets.itemData(current_index) - - if not path: - # Fallback to last registered preset path - paths = presets.preset_paths() - if paths: - path = paths[-1] - - return path - - def save_preset(self, inputs): - """Save inputs to a file""" - - path = self._default_browse_path() - filters = "Text file (*.json)" - filename, _ = QtWidgets.QFileDialog.getSaveFileName(self, - "Save preferences", - path, - filters) - if not filename: - return - - with open(filename, "w") as f: - json.dump(inputs, f, sort_keys=True, - indent=4, separators=(',', ': ')) - - self.add_preset(filename) - - return filename - - def get_presets(self): - """Return all currently listed presets""" - configurations = [self.presets.itemText(i) for - i in range(self.presets.count())] - - return configurations - - def on_save_preset(self): - """Save the inputs of all the plugins in a preset.""" - - inputs = self.inputs_getter(as_preset=True) - self.save_preset(inputs) - - def apply_inputs(self, settings): - - path = settings.get("selected", None) - index = self.presets.findData(path) - if index == -1: - # If the last loaded preset still exists but wasn't on the - # "discovered preset paths" then add it. - if os.path.exists(path): - log.info("Adding previously selected preset explicitly: %s", - path) - self.add_preset(path) - return - else: - log.warning("Previously selected preset is not available: %s", - path) - index = 0 - - self.presets.setCurrentIndex(index) - - def get_inputs(self, as_preset=False): - - if as_preset: - # Don't save the current preset into the preset because - # that would just be recursive and make no sense - return {} - else: - current_index = self.presets.currentIndex() - selected = self.presets.itemData(current_index) - return {"selected": selected} - - -class App(QtWidgets.QWidget): - """The main application in which the widgets are placed""" - - # Signals - options_changed = QtCore.Signal(dict) - playblast_start = QtCore.Signal(dict) - playblast_finished = QtCore.Signal(dict) - viewer_start = QtCore.Signal(dict) - - # Attributes - object_name = "CaptureGUI" - application_sections = ["config", "app"] - - def __init__(self, title, parent=None): - QtWidgets.QWidget.__init__(self, parent=parent) - - # Settings - # Remove pointer for memory when closed - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.settingfile = self._ensure_config_exist() - self.plugins = {"app": list(), - "config": list()} - - self._config_dialog = None - self._build_configuration_dialog() - - # region Set Attributes - title_version = "{} v{}".format(title, version.version) - self.setObjectName(self.object_name) - self.setWindowTitle(title_version) - self.setMinimumWidth(380) - - # Set dialog window flags so the widget can be correctly parented - # to Maya main window - self.setWindowFlags(self.windowFlags() | QtCore.Qt.Dialog) - self.setProperty("saveWindowPref", True) - # endregion Set Attributes - - self.layout = QtWidgets.QVBoxLayout() - self.layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.layout) - - # Add accordion widget (Maya attribute editor style) - self.widgetlibrary = AccordionWidget(self) - self.widgetlibrary.setRolloutStyle(AccordionWidget.Maya) - - # Add separate widgets - self.widgetlibrary.addItem("Preview", - PreviewWidget(self.get_outputs, - self.validate, - parent=self), - collapsed=True) - - self.presetwidget = PresetWidget(inputs_getter=self.get_inputs, - parent=self) - self.widgetlibrary.addItem("Presets", self.presetwidget) - - # add plug-in widgets - for widget in plugin.discover(): - self.add_plugin(widget) - - self.layout.addWidget(self.widgetlibrary) - - # add standard buttons - self.apply_button = QtWidgets.QPushButton("Capture") - self.layout.addWidget(self.apply_button) - - # default actions - self.apply_button.clicked.connect(self.apply) - - # signals and slots - self.presetwidget.config_opened.connect(self.show_config) - self.presetwidget.preset_loaded.connect(self.apply_inputs) - - self.apply_inputs(self._read_widget_configuration()) - - def apply(self): - """Run capture action with current settings""" - - valid = self.validate() - if not valid: - return - - options = self.get_outputs() - filename = options.get("filename", None) - - self.playblast_start.emit(options) - - # The filename can be `None` when the - # playblast will *not* be saved. - if filename is not None: - # Format the tokens in the filename - filename = tokens.format_tokens(filename, options) - - # expand environment variables - filename = os.path.expandvars(filename) - - # Make relative paths absolute to the "images" file rule by default - if not os.path.isabs(filename): - root = lib.get_project_rule("images") - filename = os.path.join(root, filename) - - # normalize (to remove double slashes and alike) - filename = os.path.normpath(filename) - - options["filename"] = filename - - # Perform capture and store returned filename with extension - options["filename"] = lib.capture_scene(options) - - self.playblast_finished.emit(options) - filename = options["filename"] # get filename after callbacks - - # Show viewer - viewer = options.get("viewer", False) - if viewer: - if filename and os.path.exists(filename): - self.viewer_start.emit(options) - lib.open_file(filename) - else: - raise RuntimeError("Can't open playblast because file " - "doesn't exist: {0}".format(filename)) - - return filename - - def apply_inputs(self, inputs): - """Apply all the settings of the widgets. - - Arguments: - inputs (dict): input values per plug-in widget - - Returns: - None - - """ - if not inputs: - return - - widgets = self._get_plugin_widgets() - widgets.append(self.presetwidget) - for widget in widgets: - widget_inputs = inputs.get(widget.id, None) - if not widget_inputs: - continue - widget.apply_inputs(widget_inputs) - - def show_config(self): - """Show the advanced configuration""" - # calculate center of main widget - geometry = self.geometry() - self._config_dialog.move(QtCore.QPoint(geometry.x()+30, - geometry.y())) - self._config_dialog.show() - - def add_plugin(self, plugin): - """Add an options widget plug-in to the UI""" - - if plugin.section not in self.application_sections: - log.warning("{}'s section is invalid: " - "{}".format(plugin.label, plugin.section)) - return - - widget = plugin(parent=self) - widget.initialize() - widget.options_changed.connect(self.on_widget_settings_changed) - self.playblast_finished.connect(widget.on_playblast_finished) - - # Add to plug-ins in its section - self.plugins[widget.section].append(widget) - - # Implement additional settings depending on section - if widget.section == "app": - if not widget.hidden: - item = self.widgetlibrary.addItem(widget.label, widget) - # connect label change behaviour - widget.label_changed.connect(item.setTitle) - - # Add the plugin in a QGroupBox to the configuration dialog - if widget.section == "config": - layout = self._config_dialog.layout() - # create group box - group_widget = QtWidgets.QGroupBox(widget.label) - group_layout = QtWidgets.QVBoxLayout(group_widget) - group_layout.addWidget(widget) - - layout.addWidget(group_widget) - - def validate(self): - """Validate whether the outputs of the widgets are good. - - Returns: - bool: Whether it's valid to capture the current settings. - - """ - - errors = list() - for widget in self._get_plugin_widgets(): - widget_errors = widget.validate() - if widget_errors: - errors.extend(widget_errors) - - if errors: - message_title = "%s Validation Error(s)" % len(errors) - message = "\n".join(errors) - QtWidgets.QMessageBox.critical(self, - message_title, - message, - QtWidgets.QMessageBox.Ok) - return False - - return True - - def get_outputs(self): - """Return settings for a capture as currently set in the Application. - - Returns: - dict: Current output settings - - """ - - # Get settings from widgets - outputs = dict() - for widget in self._get_plugin_widgets(): - widget_outputs = widget.get_outputs() - if not widget_outputs: - continue - - for key, value in widget_outputs.items(): - - # We merge dictionaries by updating them so we have - # the "mixed" values of both settings - if isinstance(value, dict) and key in outputs: - outputs[key].update(value) - else: - outputs[key] = value - - return outputs - - def get_inputs(self, as_preset=False): - """Return the inputs per plug-in widgets by `plugin.id`. - - Returns: - dict: The inputs per widget - - """ - - inputs = dict() - # Here we collect all the widgets from which we want to store the - # current inputs. This will be restored in the next session - # The preset widget is added to make sure the user starts with the - # previously selected preset configuration - config_widgets = self._get_plugin_widgets() - config_widgets.append(self.presetwidget) - for widget in config_widgets: - widget_inputs = widget.get_inputs(as_preset=as_preset) - if not isinstance(widget_inputs, dict): - log.debug("Widget inputs are not a dictionary " - "'{}': {}".format(widget.id, widget_inputs)) - return - - if not widget_inputs: - continue - - inputs[widget.id] = widget_inputs - - return inputs - - def on_widget_settings_changed(self): - """Set current preset to '*' on settings change""" - - self.options_changed.emit(self.get_outputs) - self.presetwidget.presets.setCurrentIndex(0) - - def _build_configuration_dialog(self): - """Build a configuration to store configuration widgets in""" - - dialog = QtWidgets.QDialog(self) - dialog.setWindowTitle("Capture - Preset Configuration") - QtWidgets.QVBoxLayout(dialog) - - self._config_dialog = dialog - - def _ensure_config_exist(self): - """Create the configuration file if it does not exist yet. - - Returns: - unicode: filepath of the configuration file - - """ - - userdir = os.path.expanduser("~") - capturegui_dir = os.path.join(userdir, "CaptureGUI") - capturegui_inputs = os.path.join(capturegui_dir, "capturegui.json") - if not os.path.exists(capturegui_dir): - os.makedirs(capturegui_dir) - - if not os.path.isfile(capturegui_inputs): - config = open(capturegui_inputs, "w") - config.close() - - return capturegui_inputs - - def _store_widget_configuration(self): - """Store all used widget settings in the local json file""" - - inputs = self.get_inputs(as_preset=False) - path = self.settingfile - - with open(path, "w") as f: - log.debug("Writing JSON file: {0}".format(path)) - json.dump(inputs, f, sort_keys=True, - indent=4, separators=(',', ': ')) - - def _read_widget_configuration(self): - """Read the stored widget inputs""" - - inputs = {} - path = self.settingfile - - if not os.path.isfile(path) or os.stat(path).st_size == 0: - return inputs - - with open(path, "r") as f: - log.debug("Reading JSON file: {0}".format(path)) - try: - inputs = json.load(f) - except ValueError as error: - log.error(str(error)) - - return inputs - - def _get_plugin_widgets(self): - """List all plug-in widgets. - - Returns: - list: The plug-in widgets in *all* sections - - """ - - widgets = list() - for section in self.plugins.values(): - widgets.extend(section) - - return widgets - - # override close event to ensure the input are stored - - def closeEvent(self, event): - """Store current configuration upon closing the application.""" - - self._store_widget_configuration() - for section_widgets in self.plugins.values(): - for widget in section_widgets: - widget.uninitialize() - - event.accept() diff --git a/openpype/vendor/python/common/capture_gui/colorpicker.py b/openpype/vendor/python/common/capture_gui/colorpicker.py deleted file mode 100644 index aa00a7386d..0000000000 --- a/openpype/vendor/python/common/capture_gui/colorpicker.py +++ /dev/null @@ -1,55 +0,0 @@ -from capture_gui.vendor.Qt import QtCore, QtWidgets, QtGui - - -class ColorPicker(QtWidgets.QPushButton): - """Custom color pick button to store and retrieve color values""" - - valueChanged = QtCore.Signal() - - def __init__(self): - QtWidgets.QPushButton.__init__(self) - - self.clicked.connect(self.show_color_dialog) - self._color = None - - self.color = [1, 1, 1] - - # region properties - @property - def color(self): - return self._color - - @color.setter - def color(self, values): - """Set the color value and update the stylesheet - - Arguments: - values (list): the color values; red, green, blue - - Returns: - None - - """ - self._color = values - self.valueChanged.emit() - - values = [int(x*255) for x in values] - self.setStyleSheet("background: rgb({},{},{})".format(*values)) - - # endregion properties - - def show_color_dialog(self): - """Display a color picker to change color. - - When a color has been chosen this updates the color of the button - and its current value - - :return: the red, green and blue values - :rtype: list - """ - current = QtGui.QColor() - current.setRgbF(*self._color) - colors = QtWidgets.QColorDialog.getColor(current) - if not colors: - return - self.color = [colors.redF(), colors.greenF(), colors.blueF()] diff --git a/openpype/vendor/python/common/capture_gui/lib.py b/openpype/vendor/python/common/capture_gui/lib.py deleted file mode 100644 index 823ca8f7c8..0000000000 --- a/openpype/vendor/python/common/capture_gui/lib.py +++ /dev/null @@ -1,396 +0,0 @@ -# TODO: fetch Maya main window without shiboken that also doesn't crash - -import sys -import logging -import json -import os -import glob -import subprocess -import contextlib -from collections import OrderedDict - -import datetime -import maya.cmds as cmds -import maya.mel as mel -import maya.OpenMayaUI as omui -import capture - -from .vendor.Qt import QtWidgets -try: - # PySide1 - import shiboken -except ImportError: - # PySide2 - import shiboken2 as shiboken - -log = logging.getLogger(__name__) - -# region Object types -OBJECT_TYPES = OrderedDict() -OBJECT_TYPES['NURBS Curves'] = 'nurbsCurves' -OBJECT_TYPES['NURBS Surfaces'] = 'nurbsSurfaces' -OBJECT_TYPES['NURBS CVs'] = 'controlVertices' -OBJECT_TYPES['NURBS Hulls'] = 'hulls' -OBJECT_TYPES['Polygons'] = 'polymeshes' -OBJECT_TYPES['Subdiv Surfaces'] = 'subdivSurfaces' -OBJECT_TYPES['Planes'] = 'planes' -OBJECT_TYPES['Lights'] = 'lights' -OBJECT_TYPES['Cameras'] = 'cameras' -OBJECT_TYPES['Image Planes'] = 'imagePlane' -OBJECT_TYPES['Joints'] = 'joints' -OBJECT_TYPES['IK Handles'] = 'ikHandles' -OBJECT_TYPES['Deformers'] = 'deformers' -OBJECT_TYPES['Dynamics'] = 'dynamics' -OBJECT_TYPES['Particle Instancers'] = 'particleInstancers' -OBJECT_TYPES['Fluids'] = 'fluids' -OBJECT_TYPES['Hair Systems'] = 'hairSystems' -OBJECT_TYPES['Follicles'] = 'follicles' -OBJECT_TYPES['nCloths'] = 'nCloths' -OBJECT_TYPES['nParticles'] = 'nParticles' -OBJECT_TYPES['nRigids'] = 'nRigids' -OBJECT_TYPES['Dynamic Constraints'] = 'dynamicConstraints' -OBJECT_TYPES['Locators'] = 'locators' -OBJECT_TYPES['Dimensions'] = 'dimensions' -OBJECT_TYPES['Pivots'] = 'pivots' -OBJECT_TYPES['Handles'] = 'handles' -OBJECT_TYPES['Textures Placements'] = 'textures' -OBJECT_TYPES['Strokes'] = 'strokes' -OBJECT_TYPES['Motion Trails'] = 'motionTrails' -OBJECT_TYPES['Plugin Shapes'] = 'pluginShapes' -OBJECT_TYPES['Clip Ghosts'] = 'clipGhosts' -OBJECT_TYPES['Grease Pencil'] = 'greasePencils' -OBJECT_TYPES['Manipulators'] = 'manipulators' -OBJECT_TYPES['Grid'] = 'grid' -OBJECT_TYPES['HUD'] = 'hud' -# endregion Object types - - -def get_show_object_types(): - - results = OrderedDict() - - # Add the plug-in shapes - plugin_shapes = get_plugin_shapes() - results.update(plugin_shapes) - - # We add default shapes last so plug-in shapes could - # never potentially overwrite any built-ins. - results.update(OBJECT_TYPES) - - return results - - -def get_current_scenename(): - path = cmds.file(query=True, sceneName=True) - if path: - return os.path.splitext(os.path.basename(path))[0] - return None - - -def get_current_camera(): - """Returns the currently active camera. - - Searched in the order of: - 1. Active Panel - 2. Selected Camera Shape - 3. Selected Camera Transform - - Returns: - str: name of active camera transform - - """ - - # Get camera from active modelPanel (if any) - panel = cmds.getPanel(withFocus=True) - if cmds.getPanel(typeOf=panel) == "modelPanel": - cam = cmds.modelEditor(panel, query=True, camera=True) - # In some cases above returns the shape, but most often it returns the - # transform. Still we need to make sure we return the transform. - if cam: - if cmds.nodeType(cam) == "transform": - return cam - # camera shape is a shape type - elif cmds.objectType(cam, isAType="shape"): - parent = cmds.listRelatives(cam, parent=True, fullPath=True) - if parent: - return parent[0] - - # Check if a camShape is selected (if so use that) - cam_shapes = cmds.ls(selection=True, type="camera") - if cam_shapes: - return cmds.listRelatives(cam_shapes, - parent=True, - fullPath=True)[0] - - # Check if a transform of a camShape is selected - # (return cam transform if any) - transforms = cmds.ls(selection=True, type="transform") - if transforms: - cam_shapes = cmds.listRelatives(transforms, shapes=True, type="camera") - if cam_shapes: - return cmds.listRelatives(cam_shapes, - parent=True, - fullPath=True)[0] - - -def get_active_editor(): - """Return the active editor panel to playblast with""" - # fixes `cmds.playblast` undo bug - cmds.currentTime(cmds.currentTime(query=True)) - panel = cmds.playblast(activeEditor=True) - return panel.split("|")[-1] - - -def get_current_frame(): - return cmds.currentTime(query=True) - - -def get_time_slider_range(highlighted=True, - withinHighlighted=True, - highlightedOnly=False): - """Return the time range from Maya's time slider. - - Arguments: - highlighted (bool): When True if will return a selected frame range - (if there's any selection of more than one frame!) otherwise it - will return min and max playback time. - withinHighlighted (bool): By default Maya returns the highlighted range - end as a plus one value. When this is True this will be fixed by - removing one from the last number. - - Returns: - list: List of two floats of start and end frame numbers. - - """ - if highlighted is True: - gPlaybackSlider = mel.eval("global string $gPlayBackSlider; " - "$gPlayBackSlider = $gPlayBackSlider;") - if cmds.timeControl(gPlaybackSlider, query=True, rangeVisible=True): - highlightedRange = cmds.timeControl(gPlaybackSlider, - query=True, - rangeArray=True) - if withinHighlighted: - highlightedRange[-1] -= 1 - return highlightedRange - if not highlightedOnly: - return [cmds.playbackOptions(query=True, minTime=True), - cmds.playbackOptions(query=True, maxTime=True)] - - -def get_current_renderlayer(): - return cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) - - -def get_plugin_shapes(): - """Get all currently available plugin shapes - - Returns: - dict: plugin shapes by their menu label and script name - - """ - filters = cmds.pluginDisplayFilter(query=True, listFilters=True) - labels = [cmds.pluginDisplayFilter(f, query=True, label=True) for f in - filters] - return OrderedDict(zip(labels, filters)) - - -def open_file(filepath): - """Open file using OS default settings""" - if sys.platform.startswith('darwin'): - subprocess.call(('open', filepath)) - elif os.name == 'nt': - os.startfile(filepath) - elif os.name == 'posix': - subprocess.call(('xdg-open', filepath)) - else: - raise NotImplementedError("OS not supported: {0}".format(os.name)) - - -def load_json(filepath): - """open and read json, return read values""" - with open(filepath, "r") as f: - return json.load(f) - - -def _fix_playblast_output_path(filepath): - """Workaround a bug in maya.cmds.playblast to return correct filepath. - - When the `viewer` argument is set to False and maya.cmds.playblast does not - automatically open the playblasted file the returned filepath does not have - the file's extension added correctly. - - To workaround this we just glob.glob() for any file extensions and assume - the latest modified file is the correct file and return it. - - """ - # Catch cancelled playblast - if filepath is None: - log.warning("Playblast did not result in output path. " - "Playblast is probably interrupted.") - return - - # Fix: playblast not returning correct filename (with extension) - # Lets assume the most recently modified file is the correct one. - if not os.path.exists(filepath): - directory = os.path.dirname(filepath) - filename = os.path.basename(filepath) - # check if the filepath is has frame based filename - # example : capture.####.png - parts = filename.split(".") - if len(parts) == 3: - query = os.path.join(directory, "{}.*.{}".format(parts[0], - parts[-1])) - files = glob.glob(query) - else: - files = glob.glob("{}.*".format(filepath)) - - if not files: - raise RuntimeError("Couldn't find playblast from: " - "{0}".format(filepath)) - filepath = max(files, key=os.path.getmtime) - - return filepath - - -def capture_scene(options): - """Capture using scene settings. - - Uses the view settings from "panel". - - This ensures playblast is done as quicktime H.264 100% quality. - It forces showOrnaments to be off and does not render off screen. - - Arguments: - options (dict): a collection of output options - - Returns: - str: Full path to playblast file. - - """ - - filename = options.get("filename", "%TEMP%") - log.info("Capturing to: {0}".format(filename)) - - options = options.copy() - - # Force viewer to False in call to capture because we have our own - # viewer opening call to allow a signal to trigger between playblast - # and viewer - options['viewer'] = False - - # Remove panel key since it's internal value to capture_gui - options.pop("panel", None) - - path = capture.capture(**options) - path = _fix_playblast_output_path(path) - - return path - - -def browse(path=None): - """Open a pop-up browser for the user""" - - # Acquire path from user input if none defined - if path is None: - - scene_path = cmds.file(query=True, sceneName=True) - - # use scene file name as default name - default_filename = os.path.splitext(os.path.basename(scene_path))[0] - if not default_filename: - # Scene wasn't saved yet so found no valid name for playblast. - default_filename = "playblast" - - # Default to images rule - default_root = os.path.normpath(get_project_rule("images")) - default_path = os.path.join(default_root, default_filename) - path = cmds.fileDialog2(fileMode=0, - dialogStyle=2, - startingDirectory=default_path) - - if not path: - return - - if isinstance(path, (tuple, list)): - path = path[0] - - if path.endswith(".*"): - path = path[:-2] - - # Bug-Fix/Workaround: - # Fix for playblasts that result in nesting of the - # extension (eg. '.mov.mov.mov') which happens if the format - # is defined in the filename used for saving. - extension = os.path.splitext(path)[-1] - if extension: - path = path[:-len(extension)] - - return path - - -def default_output(): - """Return filename based on current scene name. - - Returns: - str: A relative filename - - """ - - scene = get_current_scenename() or "playblast" - - # get current datetime - timestamp = datetime.datetime.today() - str_timestamp = timestamp.strftime("%Y-%m-%d_%H-%M-%S") - filename = "{}_{}".format(scene, str_timestamp) - - return filename - - -def get_project_rule(rule): - """Get the full path of the rule of the project""" - - workspace = cmds.workspace(query=True, rootDirectory=True) - folder = cmds.workspace(fileRuleEntry=rule) - if not folder: - log.warning("File Rule Entry '{}' has no value, please check if the " - "rule name is typed correctly".format(rule)) - - return os.path.join(workspace, folder) - - -def list_formats(): - # Workaround for Maya playblast bug where undo would - # move the currentTime to frame one. - cmds.currentTime(cmds.currentTime(query=True)) - return cmds.playblast(query=True, format=True) - - -def list_compressions(format='avi'): - # Workaround for Maya playblast bug where undo would - # move the currentTime to frame one. - cmds.currentTime(cmds.currentTime(query=True)) - - cmd = 'playblast -format "{0}" -query -compression'.format(format) - return mel.eval(cmd) - - -@contextlib.contextmanager -def no_undo(): - """Disable undo during the context""" - try: - cmds.undoInfo(stateWithoutFlush=False) - yield - finally: - cmds.undoInfo(stateWithoutFlush=True) - - -def get_maya_main_window(): - """Get the main Maya window as a QtGui.QMainWindow instance - - Returns: - QtGui.QMainWindow: instance of the top level Maya windows - - """ - ptr = omui.MQtUtil.mainWindow() - if ptr is not None: - return shiboken.wrapInstance(long(ptr), QtWidgets.QWidget) diff --git a/openpype/vendor/python/common/capture_gui/plugin.py b/openpype/vendor/python/common/capture_gui/plugin.py deleted file mode 100644 index 7d087936d7..0000000000 --- a/openpype/vendor/python/common/capture_gui/plugin.py +++ /dev/null @@ -1,401 +0,0 @@ -"""Plug-in system - -Works similar to how OSs look for executables; i.e. a number of -absolute paths are searched for a given match. The predicate for -executables is whether or not an extension matches a number of -options, such as ".exe" or ".bat". - -In this system, the predicate is whether or not a fname ends with ".py" - -""" - -# Standard library -import os -import sys -import types -import logging -import inspect - -from .vendor.Qt import QtCore, QtWidgets - -log = logging.getLogger(__name__) - -_registered_paths = list() -_registered_plugins = dict() - - -class classproperty(object): - def __init__(self, getter): - self.getter = getter - - def __get__(self, instance, owner): - return self.getter(owner) - - -class Plugin(QtWidgets.QWidget): - """Base class for Option plug-in Widgets. - - This is a regular Qt widget that can be added to the capture interface - as an additional component, like a plugin. - - The plug-ins are sorted in the interface by their `order` attribute and - will be displayed in the main interface when `section` is set to "app" - and displayed in the additional settings pop-up when set to "config". - - When `hidden` is set to True the widget will not be shown in the interface. - This could be useful as a plug-in that supplies solely default values to - the capture GUI command. - - """ - - label = "" - section = "app" # "config" or "app" - hidden = False - options_changed = QtCore.Signal() - label_changed = QtCore.Signal(str) - order = 0 - highlight = "border: 1px solid red;" - validate_state = True - - def on_playblast_finished(self, options): - pass - - def validate(self): - """ - Ensure outputs of the widget are possible, when errors are raised it - will return a message with what has caused the error - :return: - """ - errors = [] - return errors - - def get_outputs(self): - """Return the options as set in this plug-in widget. - - This is used to identify the settings to be used for the playblast. - As such the values should be returned in a way that a call to - `capture.capture()` would understand as arguments. - - Args: - panel (str): The active modelPanel of the user. This is passed so - values could potentially be parsed from the active panel - - Returns: - dict: The options for this plug-in. (formatted `capture` style) - - """ - return dict() - - def get_inputs(self, as_preset): - """Return widget's child settings. - - This should provide a dictionary of input settings of the plug-in - that results in a dictionary that can be supplied to `apply_input()` - This is used to save the settings of the preset to a widget. - - :param as_preset: - :param as_presets: Toggle to mute certain input values of the widget - :type as_presets: bool - - Returns: - dict: The currently set inputs of this widget. - - """ - return dict() - - def apply_inputs(self, settings): - """Apply a dictionary of settings to the widget. - - This should update the widget's inputs to the settings provided in - the dictionary. This is used to apply settings from a preset. - - Returns: - None - - """ - pass - - def initialize(self): - """ - This method is used to register any callbacks - :return: - """ - pass - - def uninitialize(self): - """ - Unregister any callback created when deleting the widget - - A general explation: - - The deletion method is an attribute that lives inside the object to be - deleted, and that is the problem: - Destruction seems not to care about the order of destruction, - and the __dict__ that also holds the onDestroy bound method - gets destructed before it is called. - - Another solution is to use a weakref - - :return: None - """ - pass - - def __str__(self): - return self.label or type(self).__name__ - - def __repr__(self): - return u"%s.%s(%r)" % (__name__, type(self).__name__, self.__str__()) - - id = classproperty(lambda cls: cls.__name__) - - -def register_plugin_path(path): - """Plug-ins are looked up at run-time from directories registered here - - To register a new directory, run this command along with the absolute - path to where you"re plug-ins are located. - - Example: - >>> import os - >>> my_plugins = "/server/plugins" - >>> register_plugin_path(my_plugins) - '/server/plugins' - - Returns: - Actual path added, including any post-processing - - """ - - if path in _registered_paths: - return log.warning("Path already registered: {0}".format(path)) - - _registered_paths.append(path) - - return path - - -def deregister_plugin_path(path): - """Remove a _registered_paths path - - Raises: - KeyError if `path` isn't registered - - """ - - _registered_paths.remove(path) - - -def deregister_all_plugin_paths(): - """Mainly used in tests""" - _registered_paths[:] = [] - - -def registered_plugin_paths(): - """Return paths added via registration - - ..note:: This returns a copy of the registered paths - and can therefore not be modified directly. - - """ - - return list(_registered_paths) - - -def registered_plugins(): - """Return plug-ins added via :func:`register_plugin` - - .. note:: This returns a copy of the registered plug-ins - and can therefore not be modified directly - - """ - - return _registered_plugins.values() - - -def register_plugin(plugin): - """Register a new plug-in - - Arguments: - plugin (Plugin): Plug-in to register - - Raises: - TypeError if `plugin` is not callable - - """ - - if not hasattr(plugin, "__call__"): - raise TypeError("Plug-in must be callable " - "returning an instance of a class") - - if not plugin_is_valid(plugin): - raise TypeError("Plug-in invalid: %s", plugin) - - _registered_plugins[plugin.__name__] = plugin - - -def plugin_paths(): - """Collect paths from all sources. - - This function looks at the three potential sources of paths - and returns a list with all of them together. - - The sources are: - - - Registered paths using :func:`register_plugin_path` - - Returns: - list of paths in which plugins may be locat - - """ - - paths = list() - - for path in registered_plugin_paths(): - if path in paths: - continue - paths.append(path) - - return paths - - -def discover(paths=None): - """Find and return available plug-ins - - This function looks for files within paths registered via - :func:`register_plugin_path`. - - Arguments: - paths (list, optional): Paths to discover plug-ins from. - If no paths are provided, all paths are searched. - - """ - - plugins = dict() - - # Include plug-ins from registered paths - for path in paths or plugin_paths(): - path = os.path.normpath(path) - - if not os.path.isdir(path): - continue - - for fname in os.listdir(path): - if fname.startswith("_"): - continue - - abspath = os.path.join(path, fname) - - if not os.path.isfile(abspath): - continue - - mod_name, mod_ext = os.path.splitext(fname) - - if not mod_ext == ".py": - continue - - module = types.ModuleType(mod_name) - module.__file__ = abspath - - try: - execfile(abspath, module.__dict__) - - # Store reference to original module, to avoid - # garbage collection from collecting it's global - # imports, such as `import os`. - sys.modules[mod_name] = module - - except Exception as err: - log.debug("Skipped: \"%s\" (%s)", mod_name, err) - continue - - for plugin in plugins_from_module(module): - if plugin.id in plugins: - log.debug("Duplicate plug-in found: %s", plugin) - continue - - plugins[plugin.id] = plugin - - # Include plug-ins from registration. - # Directly registered plug-ins take precedence. - for name, plugin in _registered_plugins.items(): - if name in plugins: - log.debug("Duplicate plug-in found: %s", plugin) - continue - plugins[name] = plugin - - plugins = list(plugins.values()) - sort(plugins) # In-place - - return plugins - - -def plugins_from_module(module): - """Return plug-ins from module - - Arguments: - module (types.ModuleType): Imported module from which to - parse valid plug-ins. - - Returns: - List of plug-ins, or empty list if none is found. - - """ - - plugins = list() - - for name in dir(module): - if name.startswith("_"): - continue - - # It could be anything at this point - obj = getattr(module, name) - - if not inspect.isclass(obj): - continue - - if not issubclass(obj, Plugin): - continue - - if not plugin_is_valid(obj): - log.debug("Plug-in invalid: %s", obj) - continue - - plugins.append(obj) - - return plugins - - -def plugin_is_valid(plugin): - """Determine whether or not plug-in `plugin` is valid - - Arguments: - plugin (Plugin): Plug-in to assess - - """ - - if not plugin: - return False - - return True - - -def sort(plugins): - """Sort `plugins` in-place - - Their order is determined by their `order` attribute. - - Arguments: - plugins (list): Plug-ins to sort - - """ - - if not isinstance(plugins, list): - raise TypeError("plugins must be of type list") - - plugins.sort(key=lambda p: p.order) - return plugins - - -# Register default paths -default_plugins_path = os.path.join(os.path.dirname(__file__), "plugins") -register_plugin_path(default_plugins_path) diff --git a/openpype/vendor/python/common/capture_gui/plugins/cameraplugin.py b/openpype/vendor/python/common/capture_gui/plugins/cameraplugin.py deleted file mode 100644 index 1902330622..0000000000 --- a/openpype/vendor/python/common/capture_gui/plugins/cameraplugin.py +++ /dev/null @@ -1,141 +0,0 @@ -import maya.cmds as cmds -from capture_gui.vendor.Qt import QtCore, QtWidgets - -import capture_gui.lib as lib -import capture_gui.plugin - - -class CameraPlugin(capture_gui.plugin.Plugin): - """Camera widget. - - Allows to select a camera. - - """ - id = "Camera" - section = "app" - order = 10 - - def __init__(self, parent=None): - super(CameraPlugin, self).__init__(parent=parent) - - self._layout = QtWidgets.QHBoxLayout() - self._layout.setContentsMargins(5, 0, 5, 0) - self.setLayout(self._layout) - - self.cameras = QtWidgets.QComboBox() - self.cameras.setMinimumWidth(200) - - self.get_active = QtWidgets.QPushButton("Get active") - self.get_active.setToolTip("Set camera from currently active view") - self.refresh = QtWidgets.QPushButton("Refresh") - self.refresh.setToolTip("Refresh the list of cameras") - - self._layout.addWidget(self.cameras) - self._layout.addWidget(self.get_active) - self._layout.addWidget(self.refresh) - - # Signals - self.connections() - - # Force update of the label - self.set_active_cam() - self.on_update_label() - - def connections(self): - self.get_active.clicked.connect(self.set_active_cam) - self.refresh.clicked.connect(self.on_refresh) - - self.cameras.currentIndexChanged.connect(self.on_update_label) - self.cameras.currentIndexChanged.connect(self.validate) - - def set_active_cam(self): - cam = lib.get_current_camera() - self.on_refresh(camera=cam) - - def select_camera(self, cam): - if cam: - # Ensure long name - cameras = cmds.ls(cam, long=True) - if not cameras: - return - cam = cameras[0] - - # Find the index in the list - for i in range(self.cameras.count()): - value = str(self.cameras.itemText(i)) - if value == cam: - self.cameras.setCurrentIndex(i) - return - - def validate(self): - - errors = [] - camera = self.cameras.currentText() - if not cmds.objExists(camera): - errors.append("{} : Selected camera '{}' " - "does not exist!".format(self.id, camera)) - self.cameras.setStyleSheet(self.highlight) - else: - self.cameras.setStyleSheet("") - - return errors - - def get_outputs(self): - """Return currently selected camera from combobox.""" - - idx = self.cameras.currentIndex() - camera = str(self.cameras.itemText(idx)) if idx != -1 else None - - return {"camera": camera} - - def on_refresh(self, camera=None): - """Refresh the camera list with all current cameras in scene. - - A currentIndexChanged signal is only emitted for the cameras combobox - when the camera is different at the end of the refresh. - - Args: - camera (str): When name of camera is passed it will try to select - the camera with this name after the refresh. - - Returns: - None - - """ - - cam = self.get_outputs()['camera'] - - # Get original selection - if camera is None: - index = self.cameras.currentIndex() - if index != -1: - camera = self.cameras.currentText() - - self.cameras.blockSignals(True) - - # Update the list with available cameras - self.cameras.clear() - - cam_shapes = cmds.ls(type="camera") - cam_transforms = cmds.listRelatives(cam_shapes, - parent=True, - fullPath=True) - self.cameras.addItems(cam_transforms) - - # If original selection, try to reselect - self.select_camera(camera) - - self.cameras.blockSignals(False) - - # If camera changed emit signal - if cam != self.get_outputs()['camera']: - idx = self.cameras.currentIndex() - self.cameras.currentIndexChanged.emit(idx) - - def on_update_label(self): - - cam = self.cameras.currentText() - cam = cam.rsplit("|", 1)[-1] # ensure short name - self.label = "Camera ({0})".format(cam) - - self.label_changed.emit(self.label) diff --git a/openpype/vendor/python/common/capture_gui/plugins/codecplugin.py b/openpype/vendor/python/common/capture_gui/plugins/codecplugin.py deleted file mode 100644 index 694194aafe..0000000000 --- a/openpype/vendor/python/common/capture_gui/plugins/codecplugin.py +++ /dev/null @@ -1,95 +0,0 @@ -from capture_gui.vendor.Qt import QtCore, QtWidgets - -import capture_gui.lib as lib -import capture_gui.plugin - - -class CodecPlugin(capture_gui.plugin.Plugin): - """Codec widget. - - Allows to set format, compression and quality. - - """ - id = "Codec" - label = "Codec" - section = "config" - order = 50 - - def __init__(self, parent=None): - super(CodecPlugin, self).__init__(parent=parent) - - self._layout = QtWidgets.QHBoxLayout() - self._layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self._layout) - - self.format = QtWidgets.QComboBox() - self.compression = QtWidgets.QComboBox() - self.quality = QtWidgets.QSpinBox() - self.quality.setMinimum(0) - self.quality.setMaximum(100) - self.quality.setValue(100) - self.quality.setToolTip("Compression quality percentage") - - self._layout.addWidget(self.format) - self._layout.addWidget(self.compression) - self._layout.addWidget(self.quality) - - self.format.currentIndexChanged.connect(self.on_format_changed) - - self.refresh() - - # Default to format 'qt' - index = self.format.findText("qt") - if index != -1: - self.format.setCurrentIndex(index) - - # Default to compression 'H.264' - index = self.compression.findText("H.264") - if index != -1: - self.compression.setCurrentIndex(index) - - self.connections() - - def connections(self): - self.compression.currentIndexChanged.connect(self.options_changed) - self.format.currentIndexChanged.connect(self.options_changed) - self.quality.valueChanged.connect(self.options_changed) - - def refresh(self): - formats = sorted(lib.list_formats()) - self.format.clear() - self.format.addItems(formats) - - def on_format_changed(self): - """Refresh the available compressions.""" - - format = self.format.currentText() - compressions = lib.list_compressions(format) - self.compression.clear() - self.compression.addItems(compressions) - - def get_outputs(self): - """Get the plugin outputs that matches `capture.capture` arguments - - Returns: - dict: Plugin outputs - - """ - - return {"format": self.format.currentText(), - "compression": self.compression.currentText(), - "quality": self.quality.value()} - - def get_inputs(self, as_preset): - # a bit redundant but it will work when iterating over widgets - # so we don't have to write an exception - return self.get_outputs() - - def apply_inputs(self, settings): - codec_format = settings.get("format", 0) - compr = settings.get("compression", 4) - quality = settings.get("quality", 100) - - self.format.setCurrentIndex(self.format.findText(codec_format)) - self.compression.setCurrentIndex(self.compression.findText(compr)) - self.quality.setValue(int(quality)) diff --git a/openpype/vendor/python/common/capture_gui/plugins/defaultoptionsplugin.py b/openpype/vendor/python/common/capture_gui/plugins/defaultoptionsplugin.py deleted file mode 100644 index f56897e562..0000000000 --- a/openpype/vendor/python/common/capture_gui/plugins/defaultoptionsplugin.py +++ /dev/null @@ -1,47 +0,0 @@ -import capture -import capture_gui.plugin - - -class DefaultOptionsPlugin(capture_gui.plugin.Plugin): - """Invisible Plugin that supplies some default values to the gui. - - This enures: - - no HUD is present in playblasts - - no overscan (`overscan` set to 1.0) - - no title safe, action safe, gate mask, etc. - - active sound is included in video playblasts - - """ - order = -1 - hidden = True - - def get_outputs(self): - """Get the plugin outputs that matches `capture.capture` arguments - - Returns: - dict: Plugin outputs - - """ - - outputs = dict() - - # use active sound track - scene = capture.parse_active_scene() - outputs['sound'] = scene['sound'] - - # override default settings - outputs['show_ornaments'] = True # never show HUD or overlays - - # override camera options - outputs['camera_options'] = dict() - outputs['camera_options']['overscan'] = 1.0 - outputs['camera_options']['displayFieldChart'] = False - outputs['camera_options']['displayFilmGate'] = False - outputs['camera_options']['displayFilmOrigin'] = False - outputs['camera_options']['displayFilmPivot'] = False - outputs['camera_options']['displayGateMask'] = False - outputs['camera_options']['displayResolution'] = False - outputs['camera_options']['displaySafeAction'] = False - outputs['camera_options']['displaySafeTitle'] = False - - return outputs diff --git a/openpype/vendor/python/common/capture_gui/plugins/displayplugin.py b/openpype/vendor/python/common/capture_gui/plugins/displayplugin.py deleted file mode 100644 index 3dffb98654..0000000000 --- a/openpype/vendor/python/common/capture_gui/plugins/displayplugin.py +++ /dev/null @@ -1,179 +0,0 @@ -import maya.cmds as cmds - -from capture_gui.vendor.Qt import QtCore, QtWidgets -import capture_gui.plugin -import capture_gui.colorpicker as colorpicker - - -# region GLOBALS - -BACKGROUND_DEFAULT = [0.6309999823570251, - 0.6309999823570251, - 0.6309999823570251] - -TOP_DEFAULT = [0.5350000262260437, - 0.6169999837875366, - 0.7020000219345093] - -BOTTOM_DEFAULT = [0.052000001072883606, - 0.052000001072883606, - 0.052000001072883606] - -COLORS = {"background": BACKGROUND_DEFAULT, - "backgroundTop": TOP_DEFAULT, - "backgroundBottom": BOTTOM_DEFAULT} - -LABELS = {"background": "Background", - "backgroundTop": "Top", - "backgroundBottom": "Bottom"} -# endregion GLOBALS - - -class DisplayPlugin(capture_gui.plugin.Plugin): - """Plugin to apply viewport visibilities and settings""" - - id = "Display Options" - label = "Display Options" - section = "config" - order = 70 - - def __init__(self, parent=None): - super(DisplayPlugin, self).__init__(parent=parent) - - self._colors = dict() - - self._layout = QtWidgets.QVBoxLayout() - self._layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self._layout) - - self.override = QtWidgets.QCheckBox("Override Display Options") - - self.display_type = QtWidgets.QComboBox() - self.display_type.addItems(["Solid", "Gradient"]) - - # create color columns - self._color_layout = QtWidgets.QHBoxLayout() - for label, default in COLORS.items(): - self.add_color_picker(self._color_layout, label, default) - - # populate layout - self._layout.addWidget(self.override) - self._layout.addWidget(self.display_type) - self._layout.addLayout(self._color_layout) - - # ensure widgets are in the correct enable state - self.on_toggle_override() - - self.connections() - - def connections(self): - self.override.toggled.connect(self.on_toggle_override) - self.override.toggled.connect(self.options_changed) - self.display_type.currentIndexChanged.connect(self.options_changed) - - def add_color_picker(self, layout, label, default): - """Create a column with a label and a button to select a color - - Arguments: - layout (QtWidgets.QLayout): Layout to add color picker to - label (str): system name for the color type, e.g. : backgroundTop - default (list): The default color values to start with - - Returns: - colorpicker.ColorPicker: a color picker instance - - """ - - column = QtWidgets.QVBoxLayout() - label_widget = QtWidgets.QLabel(LABELS[label]) - - color_picker = colorpicker.ColorPicker() - color_picker.color = default - - column.addWidget(label_widget) - column.addWidget(color_picker) - - column.setAlignment(label_widget, QtCore.Qt.AlignCenter) - - layout.addLayout(column) - - # connect signal - color_picker.valueChanged.connect(self.options_changed) - - # store widget - self._colors[label] = color_picker - - return color_picker - - def on_toggle_override(self): - """Callback when override is toggled. - - Enable or disable the color pickers and background type widgets bases - on the current state of the override checkbox - - Returns: - None - - """ - state = self.override.isChecked() - self.display_type.setEnabled(state) - for widget in self._colors.values(): - widget.setEnabled(state) - - def display_gradient(self): - """Return whether the background should be displayed as gradient. - - When True the colors will use the top and bottom color to define the - gradient otherwise the background color will be used as solid color. - - Returns: - bool: Whether background is gradient - - """ - return self.display_type.currentText() == "Gradient" - - def apply_inputs(self, settings): - """Apply the saved inputs from the inputs configuration - - Arguments: - settings (dict): The input settings to apply. - - """ - - for label, widget in self._colors.items(): - default = COLORS.get(label, [0, 0, 0]) # fallback default to black - value = settings.get(label, default) - widget.color = value - - override = settings.get("override_display", False) - self.override.setChecked(override) - - def get_inputs(self, as_preset): - inputs = {"override_display": self.override.isChecked()} - for label, widget in self._colors.items(): - inputs[label] = widget.color - - return inputs - - def get_outputs(self): - """Get the plugin outputs that matches `capture.capture` arguments - - Returns: - dict: Plugin outputs - - """ - - outputs = {} - if self.override.isChecked(): - outputs["displayGradient"] = self.display_gradient() - for label, widget in self._colors.items(): - outputs[label] = widget.color - else: - # Parse active color settings - outputs["displayGradient"] = cmds.displayPref(query=True, - displayGradient=True) - for key in COLORS.keys(): - color = cmds.displayRGBColor(key, query=True) - outputs[key] = color - - return {"display_options": outputs} diff --git a/openpype/vendor/python/common/capture_gui/plugins/genericplugin.py b/openpype/vendor/python/common/capture_gui/plugins/genericplugin.py deleted file mode 100644 index a43d43f3cc..0000000000 --- a/openpype/vendor/python/common/capture_gui/plugins/genericplugin.py +++ /dev/null @@ -1,95 +0,0 @@ -import maya.cmds as mc -from capture_gui.vendor.Qt import QtCore, QtWidgets - -import capture_gui.plugin -import capture_gui.lib - - -class GenericPlugin(capture_gui.plugin.Plugin): - """Widget for generic options""" - id = "Generic" - label = "Generic" - section = "config" - order = 100 - - def __init__(self, parent=None): - super(GenericPlugin, self).__init__(parent=parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(layout) - - isolate_view = QtWidgets.QCheckBox( - "Use isolate view from active panel") - off_screen = QtWidgets.QCheckBox("Render offscreen") - - layout.addWidget(isolate_view) - layout.addWidget(off_screen) - - isolate_view.stateChanged.connect(self.options_changed) - off_screen.stateChanged.connect(self.options_changed) - - self.widgets = { - "off_screen": off_screen, - "isolate_view": isolate_view - } - - self.apply_inputs(self.get_defaults()) - - def get_defaults(self): - return { - "off_screen": True, - "isolate_view": False - } - - def get_inputs(self, as_preset): - """Return the widget options - - Returns: - dict: The input settings of the widgets. - - """ - - inputs = dict() - for key, widget in self.widgets.items(): - state = widget.isChecked() - inputs[key] = state - - return inputs - - def apply_inputs(self, inputs): - """Apply the saved inputs from the inputs configuration - - Arguments: - inputs (dict): The input settings to apply. - - """ - - for key, widget in self.widgets.items(): - state = inputs.get(key, None) - if state is not None: - widget.setChecked(state) - - return inputs - - def get_outputs(self): - """Returns all the options from the widget - - Returns: dictionary with the settings - - """ - - inputs = self.get_inputs(as_preset=False) - outputs = dict() - outputs['off_screen'] = inputs['off_screen'] - - import capture_gui.lib - - # Get isolate view members of the active panel - if inputs['isolate_view']: - panel = capture_gui.lib.get_active_editor() - filter_set = mc.modelEditor(panel, query=True, viewObjects=True) - isolate = mc.sets(filter_set, query=True) if filter_set else None - outputs['isolate'] = isolate - - return outputs diff --git a/openpype/vendor/python/common/capture_gui/plugins/ioplugin.py b/openpype/vendor/python/common/capture_gui/plugins/ioplugin.py deleted file mode 100644 index defdc190df..0000000000 --- a/openpype/vendor/python/common/capture_gui/plugins/ioplugin.py +++ /dev/null @@ -1,254 +0,0 @@ -import os -import logging -from functools import partial - -from capture_gui.vendor.Qt import QtCore, QtWidgets -from capture_gui import plugin, lib -from capture_gui import tokens - -log = logging.getLogger("IO") - - -class IoAction(QtWidgets.QAction): - - def __init__(self, parent, filepath): - super(IoAction, self).__init__(parent) - - action_label = os.path.basename(filepath) - - self.setText(action_label) - self.setData(filepath) - - # check if file exists and disable when false - self.setEnabled(os.path.isfile(filepath)) - - # get icon from file - info = QtCore.QFileInfo(filepath) - icon_provider = QtWidgets.QFileIconProvider() - self.setIcon(icon_provider.icon(info)) - - self.triggered.connect(self.open_object_data) - - def open_object_data(self): - lib.open_file(self.data()) - - -class IoPlugin(plugin.Plugin): - """Codec widget. - - Allows to set format, compression and quality. - - """ - id = "IO" - label = "Save" - section = "app" - order = 40 - max_recent_playblasts = 5 - - def __init__(self, parent=None): - super(IoPlugin, self).__init__(parent=parent) - - self.recent_playblasts = list() - - self._layout = QtWidgets.QVBoxLayout() - self._layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self._layout) - - # region Checkboxes - self.save_file = QtWidgets.QCheckBox(text="Save") - self.open_viewer = QtWidgets.QCheckBox(text="View when finished") - self.raw_frame_numbers = QtWidgets.QCheckBox(text="Raw frame numbers") - - checkbox_hlayout = QtWidgets.QHBoxLayout() - checkbox_hlayout.setContentsMargins(5, 0, 5, 0) - checkbox_hlayout.addWidget(self.save_file) - checkbox_hlayout.addWidget(self.open_viewer) - checkbox_hlayout.addWidget(self.raw_frame_numbers) - checkbox_hlayout.addStretch(True) - # endregion Checkboxes - - # region Path - self.path_widget = QtWidgets.QWidget() - - self.browse = QtWidgets.QPushButton("Browse") - self.file_path = QtWidgets.QLineEdit() - self.file_path.setPlaceholderText("(not set; using scene name)") - tip = "Right click in the text field to insert tokens" - self.file_path.setToolTip(tip) - self.file_path.setStatusTip(tip) - self.file_path.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.file_path.customContextMenuRequested.connect(self.show_token_menu) - - path_hlayout = QtWidgets.QHBoxLayout() - path_hlayout.setContentsMargins(0, 0, 0, 0) - path_label = QtWidgets.QLabel("Path:") - path_label.setFixedWidth(30) - - path_hlayout.addWidget(path_label) - path_hlayout.addWidget(self.file_path) - path_hlayout.addWidget(self.browse) - self.path_widget.setLayout(path_hlayout) - # endregion Path - - # region Recent Playblast - self.play_recent = QtWidgets.QPushButton("Play recent playblast") - self.recent_menu = QtWidgets.QMenu() - self.play_recent.setMenu(self.recent_menu) - # endregion Recent Playblast - - self._layout.addLayout(checkbox_hlayout) - self._layout.addWidget(self.path_widget) - self._layout.addWidget(self.play_recent) - - # Signals / connections - self.browse.clicked.connect(self.show_browse_dialog) - self.file_path.textChanged.connect(self.options_changed) - self.save_file.stateChanged.connect(self.options_changed) - self.raw_frame_numbers.stateChanged.connect(self.options_changed) - self.save_file.stateChanged.connect(self.on_save_changed) - - # Ensure state is up-to-date with current settings - self.on_save_changed() - - def on_save_changed(self): - """Update the visibility of the path field""" - - state = self.save_file.isChecked() - if state: - self.path_widget.show() - else: - self.path_widget.hide() - - def show_browse_dialog(self): - """Set the filepath using a browser dialog. - - :return: None - """ - - path = lib.browse() - if not path: - return - - # Maya's browser return Linux based file paths to ensure Windows is - # supported we use normpath - path = os.path.normpath(path) - - self.file_path.setText(path) - - def add_playblast(self, item): - """ - Add an item to the previous playblast menu - - :param item: full path to a playblast file - :type item: str - - :return: None - """ - - # If item already in the recent playblasts remove it so we are - # sure to add it as the new first most-recent - try: - self.recent_playblasts.remove(item) - except ValueError: - pass - - # Add as first in the recent playblasts - self.recent_playblasts.insert(0, item) - - # Ensure the playblast list is never longer than maximum amount - # by removing the older entries that are at the end of the list - if len(self.recent_playblasts) > self.max_recent_playblasts: - del self.recent_playblasts[self.max_recent_playblasts:] - - # Rebuild the actions menu - self.recent_menu.clear() - for playblast in self.recent_playblasts: - action = IoAction(parent=self, filepath=playblast) - self.recent_menu.addAction(action) - - def on_playblast_finished(self, options): - """Take action after the play blast is done""" - playblast_file = options['filename'] - if not playblast_file: - return - self.add_playblast(playblast_file) - - def get_outputs(self): - """Get the plugin outputs that matches `capture.capture` arguments - - Returns: - dict: Plugin outputs - - """ - - output = {"filename": None, - "raw_frame_numbers": self.raw_frame_numbers.isChecked(), - "viewer": self.open_viewer.isChecked()} - - save = self.save_file.isChecked() - if not save: - return output - - # get path, if nothing is set fall back to default - # project/images/playblast - path = self.file_path.text() - if not path: - path = lib.default_output() - - output["filename"] = path - - return output - - def get_inputs(self, as_preset): - inputs = {"name": self.file_path.text(), - "save_file": self.save_file.isChecked(), - "open_finished": self.open_viewer.isChecked(), - "recent_playblasts": self.recent_playblasts, - "raw_frame_numbers": self.raw_frame_numbers.isChecked()} - - if as_preset: - inputs["recent_playblasts"] = [] - - return inputs - - def apply_inputs(self, settings): - - directory = settings.get("name", None) - save_file = settings.get("save_file", True) - open_finished = settings.get("open_finished", True) - raw_frame_numbers = settings.get("raw_frame_numbers", False) - previous_playblasts = settings.get("recent_playblasts", []) - - self.save_file.setChecked(save_file) - self.open_viewer.setChecked(open_finished) - self.raw_frame_numbers.setChecked(raw_frame_numbers) - - for playblast in reversed(previous_playblasts): - self.add_playblast(playblast) - - self.file_path.setText(directory) - - def token_menu(self): - """ - Build the token menu based on the registered tokens - - :returns: Menu - :rtype: QtWidgets.QMenu - """ - menu = QtWidgets.QMenu(self) - registered_tokens = tokens.list_tokens() - - for token, value in registered_tokens.items(): - label = "{} \t{}".format(token, value['label']) - action = QtWidgets.QAction(label, menu) - fn = partial(self.file_path.insert, token) - action.triggered.connect(fn) - menu.addAction(action) - - return menu - - def show_token_menu(self, pos): - """Show custom manu on position of widget""" - menu = self.token_menu() - globalpos = QtCore.QPoint(self.file_path.mapToGlobal(pos)) - menu.exec_(globalpos) diff --git a/openpype/vendor/python/common/capture_gui/plugins/panzoomplugin.py b/openpype/vendor/python/common/capture_gui/plugins/panzoomplugin.py deleted file mode 100644 index 5bf818ff2d..0000000000 --- a/openpype/vendor/python/common/capture_gui/plugins/panzoomplugin.py +++ /dev/null @@ -1,48 +0,0 @@ -from capture_gui.vendor.Qt import QtCore, QtWidgets -import capture_gui.plugin - - -class PanZoomPlugin(capture_gui.plugin.Plugin): - """Pan/Zoom widget. - - Allows to toggle whether you want to playblast with the camera's pan/zoom - state or disable it during the playblast. When "Use pan/zoom from camera" - is *not* checked it will force disable pan/zoom. - - """ - id = "PanZoom" - label = "Pan/Zoom" - section = "config" - order = 110 - - def __init__(self, parent=None): - super(PanZoomPlugin, self).__init__(parent=parent) - - self._layout = QtWidgets.QHBoxLayout() - self._layout.setContentsMargins(5, 0, 5, 0) - self.setLayout(self._layout) - - self.pan_zoom = QtWidgets.QCheckBox("Use pan/zoom from camera") - self.pan_zoom.setChecked(True) - - self._layout.addWidget(self.pan_zoom) - - self.pan_zoom.stateChanged.connect(self.options_changed) - - def get_outputs(self): - - if not self.pan_zoom.isChecked(): - return {"camera_options": { - "panZoomEnabled": 1, - "horizontalPan": 0.0, - "verticalPan": 0.0, - "zoom": 1.0} - } - else: - return {} - - def apply_inputs(self, settings): - self.pan_zoom.setChecked(settings.get("pan_zoom", True)) - - def get_inputs(self, as_preset): - return {"pan_zoom": self.pan_zoom.isChecked()} diff --git a/openpype/vendor/python/common/capture_gui/plugins/rendererplugin.py b/openpype/vendor/python/common/capture_gui/plugins/rendererplugin.py deleted file mode 100644 index 17932d69d9..0000000000 --- a/openpype/vendor/python/common/capture_gui/plugins/rendererplugin.py +++ /dev/null @@ -1,104 +0,0 @@ -import maya.cmds as cmds - -from capture_gui.vendor.Qt import QtCore, QtWidgets -import capture_gui.lib as lib -import capture_gui.plugin - - -class RendererPlugin(capture_gui.plugin.Plugin): - """Renderer plugin to control the used playblast renderer for viewport""" - - id = "Renderer" - label = "Renderer" - section = "config" - order = 60 - - def __init__(self, parent=None): - super(RendererPlugin, self).__init__(parent=parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(layout) - - # Get active renderers for viewport - self._renderers = self.get_renderers() - - # Create list of renderers - self.renderers = QtWidgets.QComboBox() - self.renderers.addItems(self._renderers.keys()) - - layout.addWidget(self.renderers) - - self.apply_inputs(self.get_defaults()) - - # Signals - self.renderers.currentIndexChanged.connect(self.options_changed) - - def get_current_renderer(self): - """Get current renderer by internal name (non-UI) - - Returns: - str: Name of renderer. - - """ - renderer_ui = self.renderers.currentText() - renderer = self._renderers.get(renderer_ui, None) - if renderer is None: - raise RuntimeError("No valid renderer: {0}".format(renderer_ui)) - - return renderer - - def get_renderers(self): - """Collect all available renderers for playblast""" - active_editor = lib.get_active_editor() - renderers_ui = cmds.modelEditor(active_editor, - query=True, - rendererListUI=True) - renderers_id = cmds.modelEditor(active_editor, - query=True, - rendererList=True) - - renderers = dict(zip(renderers_ui, renderers_id)) - renderers.pop("Stub Renderer") - - return renderers - - def get_defaults(self): - return {"rendererName": "vp2Renderer"} - - def get_inputs(self, as_preset): - return {"rendererName": self.get_current_renderer()} - - def get_outputs(self): - """Get the plugin outputs that matches `capture.capture` arguments - - Returns: - dict: Plugin outputs - - """ - return { - "viewport_options": { - "rendererName": self.get_current_renderer() - } - } - - def apply_inputs(self, inputs): - """Apply previous settings or settings from a preset - - Args: - inputs (dict): Plugin input settings - - Returns: - None - - """ - - reverse_lookup = {value: key for key, value in self._renderers.items()} - renderer = inputs.get("rendererName", "vp2Renderer") - renderer_ui = reverse_lookup.get(renderer) - - if renderer_ui: - index = self.renderers.findText(renderer_ui) - self.renderers.setCurrentIndex(index) - else: - self.renderers.setCurrentIndex(1) diff --git a/openpype/vendor/python/common/capture_gui/plugins/resolutionplugin.py b/openpype/vendor/python/common/capture_gui/plugins/resolutionplugin.py deleted file mode 100644 index 193a95b8ba..0000000000 --- a/openpype/vendor/python/common/capture_gui/plugins/resolutionplugin.py +++ /dev/null @@ -1,199 +0,0 @@ -import math -from functools import partial - -import maya.cmds as cmds -from capture_gui.vendor.Qt import QtCore, QtWidgets - -import capture_gui.lib as lib -import capture_gui.plugin - - -class ResolutionPlugin(capture_gui.plugin.Plugin): - """Resolution widget. - - Allows to set scale based on set of options. - - """ - id = "Resolution" - section = "app" - order = 20 - - resolution_changed = QtCore.Signal() - - ScaleWindow = "From Window" - ScaleRenderSettings = "From Render Settings" - ScaleCustom = "Custom" - - def __init__(self, parent=None): - super(ResolutionPlugin, self).__init__(parent=parent) - - self._layout = QtWidgets.QVBoxLayout() - self._layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self._layout) - - # Scale - self.mode = QtWidgets.QComboBox() - self.mode.addItems([self.ScaleWindow, - self.ScaleRenderSettings, - self.ScaleCustom]) - self.mode.setCurrentIndex(1) # Default: From render settings - - # Custom width/height - self.resolution = QtWidgets.QWidget() - self.resolution.setContentsMargins(0, 0, 0, 0) - resolution_layout = QtWidgets.QHBoxLayout() - resolution_layout.setContentsMargins(0, 0, 0, 0) - resolution_layout.setSpacing(6) - - self.resolution.setLayout(resolution_layout) - width_label = QtWidgets.QLabel("Width") - width_label.setFixedWidth(40) - self.width = QtWidgets.QSpinBox() - self.width.setMinimum(0) - self.width.setMaximum(99999) - self.width.setValue(1920) - heigth_label = QtWidgets.QLabel("Height") - heigth_label.setFixedWidth(40) - self.height = QtWidgets.QSpinBox() - self.height.setMinimum(0) - self.height.setMaximum(99999) - self.height.setValue(1080) - - resolution_layout.addWidget(width_label) - resolution_layout.addWidget(self.width) - resolution_layout.addWidget(heigth_label) - resolution_layout.addWidget(self.height) - - self.scale_result = QtWidgets.QLineEdit() - self.scale_result.setReadOnly(True) - - # Percentage - self.percent_label = QtWidgets.QLabel("Scale") - self.percent = QtWidgets.QDoubleSpinBox() - self.percent.setMinimum(0.01) - self.percent.setValue(1.0) # default value - self.percent.setSingleStep(0.05) - - self.percent_presets = QtWidgets.QHBoxLayout() - self.percent_presets.setSpacing(4) - for value in [0.25, 0.5, 0.75, 1.0, 2.0]: - btn = QtWidgets.QPushButton(str(value)) - self.percent_presets.addWidget(btn) - btn.setFixedWidth(35) - btn.clicked.connect(partial(self.percent.setValue, value)) - - self.percent_layout = QtWidgets.QHBoxLayout() - self.percent_layout.addWidget(self.percent_label) - self.percent_layout.addWidget(self.percent) - self.percent_layout.addLayout(self.percent_presets) - - # Resulting scale display - self._layout.addWidget(self.mode) - self._layout.addWidget(self.resolution) - self._layout.addLayout(self.percent_layout) - self._layout.addWidget(self.scale_result) - - # refresh states - self.on_mode_changed() - self.on_resolution_changed() - - # connect signals - self.mode.currentIndexChanged.connect(self.on_mode_changed) - self.mode.currentIndexChanged.connect(self.on_resolution_changed) - self.percent.valueChanged.connect(self.on_resolution_changed) - self.width.valueChanged.connect(self.on_resolution_changed) - self.height.valueChanged.connect(self.on_resolution_changed) - - # Connect options changed - self.mode.currentIndexChanged.connect(self.options_changed) - self.percent.valueChanged.connect(self.options_changed) - self.width.valueChanged.connect(self.options_changed) - self.height.valueChanged.connect(self.options_changed) - - def on_mode_changed(self): - """Update the width/height enabled state when mode changes""" - - if self.mode.currentText() != self.ScaleCustom: - self.width.setEnabled(False) - self.height.setEnabled(False) - self.resolution.hide() - else: - self.width.setEnabled(True) - self.height.setEnabled(True) - self.resolution.show() - - def _get_output_resolution(self): - - options = self.get_outputs() - return int(options["width"]), int(options["height"]) - - def on_resolution_changed(self): - """Update the resulting resolution label""" - - width, height = self._get_output_resolution() - label = "Result: {0}x{1}".format(width, height) - - self.scale_result.setText(label) - - # Update label - self.label = "Resolution ({0}x{1})".format(width, height) - self.label_changed.emit(self.label) - - def get_outputs(self): - """Return width x height defined by the combination of settings - - Returns: - dict: width and height key values - - """ - mode = self.mode.currentText() - panel = lib.get_active_editor() - - if mode == self.ScaleCustom: - width = self.width.value() - height = self.height.value() - - elif mode == self.ScaleRenderSettings: - # width height from render resolution - width = cmds.getAttr("defaultResolution.width") - height = cmds.getAttr("defaultResolution.height") - - elif mode == self.ScaleWindow: - # width height from active view panel size - if not panel: - # No panel would be passed when updating in the UI as such - # the resulting resolution can't be previewed. But this should - # never happen when starting the capture. - width = 0 - height = 0 - else: - width = cmds.control(panel, query=True, width=True) - height = cmds.control(panel, query=True, height=True) - else: - raise NotImplementedError("Unsupported scale mode: " - "{0}".format(mode)) - - scale = [width, height] - percentage = self.percent.value() - scale = [math.floor(x * percentage) for x in scale] - - return {"width": scale[0], "height": scale[1]} - - def get_inputs(self, as_preset): - return {"mode": self.mode.currentText(), - "width": self.width.value(), - "height": self.height.value(), - "percent": self.percent.value()} - - def apply_inputs(self, settings): - # get value else fall back to default values - mode = settings.get("mode", self.ScaleRenderSettings) - width = int(settings.get("width", 1920)) - height = int(settings.get("height", 1080)) - percent = float(settings.get("percent", 1.0)) - - # set values - self.mode.setCurrentIndex(self.mode.findText(mode)) - self.width.setValue(width) - self.height.setValue(height) - self.percent.setValue(percent) diff --git a/openpype/vendor/python/common/capture_gui/plugins/timeplugin.py b/openpype/vendor/python/common/capture_gui/plugins/timeplugin.py deleted file mode 100644 index b4901f9cb4..0000000000 --- a/openpype/vendor/python/common/capture_gui/plugins/timeplugin.py +++ /dev/null @@ -1,292 +0,0 @@ -import sys -import logging -import re - -import maya.OpenMaya as om -from capture_gui.vendor.Qt import QtCore, QtWidgets - -import capture_gui.lib -import capture_gui.plugin - -log = logging.getLogger("Time Range") - - -def parse_frames(string): - """Parse the resulting frames list from a frame list string. - - Examples - >>> parse_frames("0-3;30") - [0, 1, 2, 3, 30] - >>> parse_frames("0,2,4,-10") - [0, 2, 4, -10] - >>> parse_frames("-10--5,-2") - [-10, -9, -8, -7, -6, -5, -2] - - Args: - string (str): The string to parse for frames. - - Returns: - list: A list of frames - - """ - - result = list() - if not string.strip(): - raise ValueError("Can't parse an empty frame string.") - - if not re.match("^[-0-9,; ]*$", string): - raise ValueError("Invalid symbols in frame string: {}".format(string)) - - for raw in re.split(";|,", string): - - # Skip empty elements - value = raw.strip().replace(" ", "") - if not value: - continue - - # Check for sequences (1-20) including negatives (-10--8) - sequence = re.search("(-?[0-9]+)-(-?[0-9]+)", value) - - # Sequence - if sequence: - start, end = sequence.groups() - frames = range(int(start), int(end) + 1) - result.extend(frames) - - # Single frame - else: - try: - frame = int(value) - except ValueError: - raise ValueError("Invalid frame description: " - "'{0}'".format(value)) - - result.append(frame) - - if not result: - # This happens when only spaces are entered with a separator like `,` or `;` - raise ValueError("Unable to parse any frames from string: {}".format(string)) - - return result - - -class TimePlugin(capture_gui.plugin.Plugin): - """Widget for time based options""" - - id = "Time Range" - section = "app" - order = 30 - - RangeTimeSlider = "Time Slider" - RangeStartEnd = "Start/End" - CurrentFrame = "Current Frame" - CustomFrames = "Custom Frames" - - def __init__(self, parent=None): - super(TimePlugin, self).__init__(parent=parent) - - self._event_callbacks = list() - - self._layout = QtWidgets.QHBoxLayout() - self._layout.setContentsMargins(5, 0, 5, 0) - self.setLayout(self._layout) - - self.mode = QtWidgets.QComboBox() - self.mode.addItems([self.RangeTimeSlider, - self.RangeStartEnd, - self.CurrentFrame, - self.CustomFrames]) - - frame_input_height = 20 - self.start = QtWidgets.QSpinBox() - self.start.setRange(-sys.maxint, sys.maxint) - self.start.setFixedHeight(frame_input_height) - self.end = QtWidgets.QSpinBox() - self.end.setRange(-sys.maxint, sys.maxint) - self.end.setFixedHeight(frame_input_height) - - # unique frames field - self.custom_frames = QtWidgets.QLineEdit() - self.custom_frames.setFixedHeight(frame_input_height) - self.custom_frames.setPlaceholderText("Example: 1-20,25;50;75,100-150") - self.custom_frames.setVisible(False) - - self._layout.addWidget(self.mode) - self._layout.addWidget(self.start) - self._layout.addWidget(self.end) - self._layout.addWidget(self.custom_frames) - - # Connect callbacks to ensure start is never higher then end - # and the end is never lower than start - self.end.valueChanged.connect(self._ensure_start) - self.start.valueChanged.connect(self._ensure_end) - - self.on_mode_changed() # force enabled state refresh - - self.mode.currentIndexChanged.connect(self.on_mode_changed) - self.start.valueChanged.connect(self.on_mode_changed) - self.end.valueChanged.connect(self.on_mode_changed) - self.custom_frames.textChanged.connect(self.on_mode_changed) - - def _ensure_start(self, value): - self.start.setValue(min(self.start.value(), value)) - - def _ensure_end(self, value): - self.end.setValue(max(self.end.value(), value)) - - def on_mode_changed(self, emit=True): - """Update the GUI when the user updated the time range or settings. - - Arguments: - emit (bool): Whether to emit the options changed signal - - Returns: - None - - """ - - mode = self.mode.currentText() - if mode == self.RangeTimeSlider: - start, end = capture_gui.lib.get_time_slider_range() - self.start.setEnabled(False) - self.end.setEnabled(False) - self.start.setVisible(True) - self.end.setVisible(True) - self.custom_frames.setVisible(False) - mode_values = int(start), int(end) - elif mode == self.RangeStartEnd: - self.start.setEnabled(True) - self.end.setEnabled(True) - self.start.setVisible(True) - self.end.setVisible(True) - self.custom_frames.setVisible(False) - mode_values = self.start.value(), self.end.value() - elif mode == self.CustomFrames: - self.start.setVisible(False) - self.end.setVisible(False) - self.custom_frames.setVisible(True) - mode_values = "({})".format(self.custom_frames.text()) - - # ensure validation state for custom frames - self.validate() - - else: - self.start.setEnabled(False) - self.end.setEnabled(False) - self.start.setVisible(True) - self.end.setVisible(True) - self.custom_frames.setVisible(False) - currentframe = int(capture_gui.lib.get_current_frame()) - mode_values = "({})".format(currentframe) - - # Update label - self.label = "Time Range {}".format(mode_values) - self.label_changed.emit(self.label) - - if emit: - self.options_changed.emit() - - def validate(self): - errors = [] - - if self.mode.currentText() == self.CustomFrames: - - # Reset - self.custom_frames.setStyleSheet("") - - try: - parse_frames(self.custom_frames.text()) - except ValueError as exc: - errors.append("{} : Invalid frame description: " - "{}".format(self.id, exc)) - self.custom_frames.setStyleSheet(self.highlight) - - return errors - - def get_outputs(self, panel=""): - """Get the plugin outputs that matches `capture.capture` arguments - - Returns: - dict: Plugin outputs - - """ - - mode = self.mode.currentText() - frames = None - - if mode == self.RangeTimeSlider: - start, end = capture_gui.lib.get_time_slider_range() - - elif mode == self.RangeStartEnd: - start = self.start.value() - end = self.end.value() - - elif mode == self.CurrentFrame: - frame = capture_gui.lib.get_current_frame() - start = frame - end = frame - - elif mode == self.CustomFrames: - frames = parse_frames(self.custom_frames.text()) - start = None - end = None - else: - raise NotImplementedError("Unsupported time range mode: " - "{0}".format(mode)) - - return {"start_frame": start, - "end_frame": end, - "frame": frames} - - def get_inputs(self, as_preset): - return {"time": self.mode.currentText(), - "start_frame": self.start.value(), - "end_frame": self.end.value(), - "frame": self.custom_frames.text()} - - def apply_inputs(self, settings): - # get values - mode = self.mode.findText(settings.get("time", self.RangeTimeSlider)) - startframe = settings.get("start_frame", 1) - endframe = settings.get("end_frame", 120) - custom_frames = settings.get("frame", None) - - # set values - self.mode.setCurrentIndex(mode) - self.start.setValue(int(startframe)) - self.end.setValue(int(endframe)) - if custom_frames is not None: - self.custom_frames.setText(custom_frames) - - def initialize(self): - self._register_callbacks() - - def uninitialize(self): - self._remove_callbacks() - - def _register_callbacks(self): - """Register maya time and playback range change callbacks. - - Register callbacks to ensure Capture GUI reacts to changes in - the Maya GUI in regards to time slider and current frame - - """ - - callback = lambda x: self.on_mode_changed(emit=False) - - # this avoid overriding the ids on re-run - currentframe = om.MEventMessage.addEventCallback("timeChanged", - callback) - timerange = om.MEventMessage.addEventCallback("playbackRangeChanged", - callback) - - self._event_callbacks.append(currentframe) - self._event_callbacks.append(timerange) - - def _remove_callbacks(self): - """Remove callbacks when closing widget""" - for callback in self._event_callbacks: - try: - om.MEventMessage.removeCallback(callback) - except RuntimeError, error: - log.error("Encounter error : {}".format(error)) diff --git a/openpype/vendor/python/common/capture_gui/plugins/viewportplugin.py b/openpype/vendor/python/common/capture_gui/plugins/viewportplugin.py deleted file mode 100644 index 96f311fdcf..0000000000 --- a/openpype/vendor/python/common/capture_gui/plugins/viewportplugin.py +++ /dev/null @@ -1,292 +0,0 @@ -from capture_gui.vendor.Qt import QtCore, QtWidgets -import capture_gui.plugin -import capture_gui.lib as lib -import capture - - -class ViewportPlugin(capture_gui.plugin.Plugin): - """Plugin to apply viewport visibilities and settings""" - - id = "Viewport Options" - label = "Viewport Options" - section = "config" - order = 70 - - def __init__(self, parent=None): - super(ViewportPlugin, self).__init__(parent=parent) - - # set inherited attributes - self.setObjectName(self.label) - - # custom atttributes - self.show_type_actions = list() - - # get information - self.show_types = lib.get_show_object_types() - - # set main layout for widget - self._layout = QtWidgets.QVBoxLayout() - self._layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self._layout) - - # build - # region Menus - menus_vlayout = QtWidgets.QHBoxLayout() - - # Display Lights - self.display_light_menu = self._build_light_menu() - self.display_light_menu.setFixedHeight(20) - - # Show - self.show_types_button = QtWidgets.QPushButton("Show") - self.show_types_button.setFixedHeight(20) - self.show_types_menu = self._build_show_menu() - self.show_types_button.setMenu(self.show_types_menu) - - # fill layout - menus_vlayout.addWidget(self.display_light_menu) - menus_vlayout.addWidget(self.show_types_button) - - # endregion Menus - - # region Checkboxes - checkbox_layout = QtWidgets.QGridLayout() - self.high_quality = QtWidgets.QCheckBox() - self.high_quality.setText("Force Viewport 2.0 + AA") - self.override_viewport = QtWidgets.QCheckBox("Override viewport " - "settings") - self.override_viewport.setChecked(True) - - # two sided lighting - self.two_sided_ligthing = QtWidgets.QCheckBox("Two Sided Ligthing") - self.two_sided_ligthing.setChecked(False) - - # show - self.shadows = QtWidgets.QCheckBox("Shadows") - self.shadows.setChecked(False) - - checkbox_layout.addWidget(self.override_viewport, 0, 0) - checkbox_layout.addWidget(self.high_quality, 0, 1) - checkbox_layout.addWidget(self.two_sided_ligthing, 1, 0) - checkbox_layout.addWidget(self.shadows, 1, 1) - # endregion Checkboxes - - self._layout.addLayout(checkbox_layout) - self._layout.addLayout(menus_vlayout) - - self.connections() - - def connections(self): - - self.high_quality.stateChanged.connect(self.options_changed) - self.override_viewport.stateChanged.connect(self.options_changed) - self.override_viewport.stateChanged.connect(self.on_toggle_override) - - self.two_sided_ligthing.stateChanged.connect(self.options_changed) - self.shadows.stateChanged.connect(self.options_changed) - - self.display_light_menu.currentIndexChanged.connect( - self.options_changed - ) - - def _build_show_menu(self): - """Build the menu to select which object types are shown in the output. - - Returns: - QtGui.QMenu: The visibilities "show" menu. - - """ - - menu = QtWidgets.QMenu(self) - menu.setObjectName("ShowShapesMenu") - menu.setWindowTitle("Show") - menu.setFixedWidth(180) - menu.setTearOffEnabled(True) - - # Show all check - toggle_all = QtWidgets.QAction(menu, text="All") - toggle_none = QtWidgets.QAction(menu, text="None") - menu.addAction(toggle_all) - menu.addAction(toggle_none) - menu.addSeparator() - - # add plugin shapes if any - for shape in self.show_types: - action = QtWidgets.QAction(menu, text=shape) - action.setCheckable(True) - # emit signal when the state is changed of the checkbox - action.toggled.connect(self.options_changed) - menu.addAction(action) - self.show_type_actions.append(action) - - # connect signals - toggle_all.triggered.connect(self.toggle_all_visbile) - toggle_none.triggered.connect(self.toggle_all_hide) - - return menu - - def _build_light_menu(self): - """Build lighting menu. - - Create the menu items for the different types of lighting for - in the viewport - - Returns: - None - - """ - - menu = QtWidgets.QComboBox(self) - - # names cane be found in - display_lights = (("Use Default Lighting", "default"), - ("Use All Lights", "all"), - ("Use Selected Lights", "active"), - ("Use Flat Lighting", "flat"), - ("Use No Lights", "none")) - - for label, name in display_lights: - menu.addItem(label, userData=name) - - return menu - - def on_toggle_override(self): - """Enable or disable show menu when override is checked""" - state = self.override_viewport.isChecked() - self.show_types_button.setEnabled(state) - self.high_quality.setEnabled(state) - self.display_light_menu.setEnabled(state) - self.shadows.setEnabled(state) - self.two_sided_ligthing.setEnabled(state) - - def toggle_all_visbile(self): - """Set all object types off or on depending on the state""" - for action in self.show_type_actions: - action.setChecked(True) - - def toggle_all_hide(self): - """Set all object types off or on depending on the state""" - for action in self.show_type_actions: - action.setChecked(False) - - def get_show_inputs(self): - """Return checked state of show menu items - - Returns: - dict: The checked show states in the widget. - - """ - - show_inputs = {} - # get all checked objects - for action in self.show_type_actions: - label = action.text() - name = self.show_types.get(label, None) - if name is None: - continue - show_inputs[name] = action.isChecked() - - return show_inputs - - def get_displaylights(self): - """Get and parse the currently selected displayLights options. - - Returns: - dict: The display light options - - """ - indx = self.display_light_menu.currentIndex() - return {"displayLights": self.display_light_menu.itemData(indx), - "shadows": self.shadows.isChecked(), - "twoSidedLighting": self.two_sided_ligthing.isChecked()} - - def get_inputs(self, as_preset): - """Return the widget options - - Returns: - dict: The input settings of the widgets. - - """ - inputs = {"high_quality": self.high_quality.isChecked(), - "override_viewport_options": self.override_viewport.isChecked(), - "displayLights": self.display_light_menu.currentIndex(), - "shadows": self.shadows.isChecked(), - "twoSidedLighting": self.two_sided_ligthing.isChecked()} - - inputs.update(self.get_show_inputs()) - - return inputs - - def apply_inputs(self, inputs): - """Apply the saved inputs from the inputs configuration - - Arguments: - settings (dict): The input settings to apply. - - """ - - # get input values directly from input given - override_viewport = inputs.get("override_viewport_options", True) - high_quality = inputs.get("high_quality", True) - displaylight = inputs.get("displayLights", 0) # default lighting - two_sided_ligthing = inputs.get("twoSidedLighting", False) - shadows = inputs.get("shadows", False) - - self.high_quality.setChecked(high_quality) - self.override_viewport.setChecked(override_viewport) - self.show_types_button.setEnabled(override_viewport) - - # display light menu - self.display_light_menu.setCurrentIndex(displaylight) - self.shadows.setChecked(shadows) - self.two_sided_ligthing.setChecked(two_sided_ligthing) - - for action in self.show_type_actions: - system_name = self.show_types[action.text()] - state = inputs.get(system_name, True) - action.setChecked(state) - - def get_outputs(self): - """Get the plugin outputs that matches `capture.capture` arguments - - Returns: - dict: Plugin outputs - - """ - outputs = dict() - - high_quality = self.high_quality.isChecked() - override_viewport_options = self.override_viewport.isChecked() - - if override_viewport_options: - outputs['viewport2_options'] = dict() - outputs['viewport_options'] = dict() - - if high_quality: - # force viewport 2.0 and AA - outputs['viewport_options']['rendererName'] = 'vp2Renderer' - outputs['viewport2_options']['multiSampleEnable'] = True - outputs['viewport2_options']['multiSampleCount'] = 8 - - show_per_type = self.get_show_inputs() - display_lights = self.get_displaylights() - outputs['viewport_options'].update(show_per_type) - outputs['viewport_options'].update(display_lights) - else: - # TODO: When this fails we should give the user a warning - # Use settings from the active viewport - outputs = capture.parse_active_view() - - # Remove the display options and camera attributes - outputs.pop("display_options", None) - outputs.pop("camera", None) - - # Remove the current renderer because there's already - # renderer plug-in handling that - outputs["viewport_options"].pop("rendererName", None) - - # Remove all camera options except depth of field - dof = outputs["camera_options"]["depthOfField"] - outputs["camera_options"] = {"depthOfField": dof} - - return outputs diff --git a/openpype/vendor/python/common/capture_gui/presets.py b/openpype/vendor/python/common/capture_gui/presets.py deleted file mode 100644 index 634e8264ec..0000000000 --- a/openpype/vendor/python/common/capture_gui/presets.py +++ /dev/null @@ -1,105 +0,0 @@ -import glob -import os -import logging - -_registered_paths = [] -log = logging.getLogger("Presets") - - -def discover(paths=None): - """Get the full list of files found in the registered folders - - Args: - paths (list, Optional): directories which host preset files or None. - When None (default) it will list from the registered preset paths. - - Returns: - list: valid .json preset file paths. - - """ - - presets = [] - for path in paths or preset_paths(): - path = os.path.normpath(path) - if not os.path.isdir(path): - continue - - # check for json files - glob_query = os.path.abspath(os.path.join(path, "*.json")) - filenames = glob.glob(glob_query) - for filename in filenames: - # skip private files - if filename.startswith("_"): - continue - - # check for file size - if not check_file_size(filename): - log.warning("Filesize is smaller than 1 byte for file '%s'", - filename) - continue - - if filename not in presets: - presets.append(filename) - - return presets - - -def check_file_size(filepath): - """Check if filesize of the given file is bigger than 1.0 byte - - Args: - filepath (str): full filepath of the file to check - - Returns: - bool: Whether bigger than 1 byte. - - """ - - file_stats = os.stat(filepath) - if file_stats.st_size < 1: - return False - return True - - -def preset_paths(): - """Return existing registered preset paths - - Returns: - list: List of full paths. - - """ - - paths = list() - for path in _registered_paths: - # filter duplicates - if path in paths: - continue - - if not os.path.exists(path): - continue - - paths.append(path) - - return paths - - -def register_preset_path(path): - """Add filepath to registered presets - - :param path: the directory of the preset file(s) - :type path: str - - :return: - """ - if path in _registered_paths: - return log.warning("Path already registered: %s", path) - - _registered_paths.append(path) - - return path - - -# Register default user folder -user_folder = os.path.expanduser("~") -capture_gui_presets = os.path.join(user_folder, "CaptureGUI", "presets") -register_preset_path(capture_gui_presets) diff --git a/openpype/vendor/python/common/capture_gui/resources/config.png b/openpype/vendor/python/common/capture_gui/resources/config.png deleted file mode 100644 index 634a1da65a..0000000000 Binary files a/openpype/vendor/python/common/capture_gui/resources/config.png and /dev/null differ diff --git a/openpype/vendor/python/common/capture_gui/resources/import.png b/openpype/vendor/python/common/capture_gui/resources/import.png deleted file mode 100644 index 785747191a..0000000000 Binary files a/openpype/vendor/python/common/capture_gui/resources/import.png and /dev/null differ diff --git a/openpype/vendor/python/common/capture_gui/resources/reset.png b/openpype/vendor/python/common/capture_gui/resources/reset.png deleted file mode 100644 index 629822cd44..0000000000 Binary files a/openpype/vendor/python/common/capture_gui/resources/reset.png and /dev/null differ diff --git a/openpype/vendor/python/common/capture_gui/resources/save.png b/openpype/vendor/python/common/capture_gui/resources/save.png deleted file mode 100644 index 817af19e9f..0000000000 Binary files a/openpype/vendor/python/common/capture_gui/resources/save.png and /dev/null differ diff --git a/openpype/vendor/python/common/capture_gui/tokens.py b/openpype/vendor/python/common/capture_gui/tokens.py deleted file mode 100644 index d34167b53d..0000000000 --- a/openpype/vendor/python/common/capture_gui/tokens.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Token system - -The capture gui application will format tokens in the filename. -The tokens can be registered using `register_token` - -""" -from . import lib - -_registered_tokens = dict() - - -def format_tokens(string, options): - """Replace the tokens with the correlated strings - - Arguments: - string (str): filename of the playblast with tokens. - options (dict): The parsed capture options. - - Returns: - str: The formatted filename with all tokens resolved - - """ - - if not string: - return string - - for token, value in _registered_tokens.items(): - if token in string: - func = value['func'] - string = string.replace(token, func(options)) - - return string - - -def register_token(token, func, label=""): - assert token.startswith("<") and token.endswith(">") - assert callable(func) - _registered_tokens[token] = {"func": func, "label": label} - - -def list_tokens(): - return _registered_tokens.copy() - - -# register default tokens -# scene based tokens -def _camera_token(options): - """Return short name of camera from capture options""" - camera = options['camera'] - camera = camera.rsplit("|", 1)[-1] # use short name - camera = camera.replace(":", "_") # namespace `:` to `_` - return camera - - -register_token("", _camera_token, - label="Insert camera name") -register_token("", lambda options: lib.get_current_scenename() or "playblast", - label="Insert current scene name") -register_token("", lambda options: lib.get_current_renderlayer(), - label="Insert active render layer name") - -# project based tokens -register_token("", - lambda options: lib.get_project_rule("images"), - label="Insert image directory of set project") -register_token("", - lambda options: lib.get_project_rule("movie"), - label="Insert movies directory of set project") diff --git a/openpype/vendor/python/common/capture_gui/vendor/Qt.py b/openpype/vendor/python/common/capture_gui/vendor/Qt.py deleted file mode 100644 index 3a97da872d..0000000000 --- a/openpype/vendor/python/common/capture_gui/vendor/Qt.py +++ /dev/null @@ -1,1030 +0,0 @@ -"""The MIT License (MIT) - -Copyright (c) 2016-2017 Marcus Ottosson - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -Documentation - - Map all bindings to PySide2 - - Project goals: - Qt.py was born in the film and visual effects industry to address - the growing need for the development of software capable of running - with more than one flavour of the Qt bindings for Python - PySide, - PySide2, PyQt4 and PyQt5. - - 1. Build for one, run with all - 2. Explicit is better than implicit - 3. Support co-existence - - Default resolution order: - - PySide2 - - PyQt5 - - PySide - - PyQt4 - - Usage: - >> import sys - >> from Qt import QtWidgets - >> app = QtWidgets.QApplication(sys.argv) - >> button = QtWidgets.QPushButton("Hello World") - >> button.show() - >> app.exec_() - - All members of PySide2 are mapped from other bindings, should they exist. - If no equivalent member exist, it is excluded from Qt.py and inaccessible. - The idea is to highlight members that exist across all supported binding, - and guarantee that code that runs on one binding runs on all others. - - For more details, visit https://github.com/mottosso/Qt.py - -""" - -import os -import sys -import types -import shutil -import importlib - -__version__ = "1.0.0.b3" - -# Enable support for `from Qt import *` -__all__ = [] - -# Flags from environment variables -QT_VERBOSE = bool(os.getenv("QT_VERBOSE")) -QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING", "") -QT_SIP_API_HINT = os.getenv("QT_SIP_API_HINT") - -# Reference to Qt.py -Qt = sys.modules[__name__] -Qt.QtCompat = types.ModuleType("QtCompat") - -"""Common members of all bindings - -This is where each member of Qt.py is explicitly defined. -It is based on a "lowest commond denominator" of all bindings; -including members found in each of the 4 bindings. - -Find or add excluded members in build_membership.py - -""" - -_common_members = { - "QtGui": [ - "QAbstractTextDocumentLayout", - "QActionEvent", - "QBitmap", - "QBrush", - "QClipboard", - "QCloseEvent", - "QColor", - "QConicalGradient", - "QContextMenuEvent", - "QCursor", - "QDoubleValidator", - "QDrag", - "QDragEnterEvent", - "QDragLeaveEvent", - "QDragMoveEvent", - "QDropEvent", - "QFileOpenEvent", - "QFocusEvent", - "QFont", - "QFontDatabase", - "QFontInfo", - "QFontMetrics", - "QFontMetricsF", - "QGradient", - "QHelpEvent", - "QHideEvent", - "QHoverEvent", - "QIcon", - "QIconDragEvent", - "QIconEngine", - "QImage", - "QImageIOHandler", - "QImageReader", - "QImageWriter", - "QInputEvent", - "QInputMethodEvent", - "QIntValidator", - "QKeyEvent", - "QKeySequence", - "QLinearGradient", - "QMatrix2x2", - "QMatrix2x3", - "QMatrix2x4", - "QMatrix3x2", - "QMatrix3x3", - "QMatrix3x4", - "QMatrix4x2", - "QMatrix4x3", - "QMatrix4x4", - "QMouseEvent", - "QMoveEvent", - "QMovie", - "QPaintDevice", - "QPaintEngine", - "QPaintEngineState", - "QPaintEvent", - "QPainter", - "QPainterPath", - "QPainterPathStroker", - "QPalette", - "QPen", - "QPicture", - "QPictureIO", - "QPixmap", - "QPixmapCache", - "QPolygon", - "QPolygonF", - "QQuaternion", - "QRadialGradient", - "QRegExpValidator", - "QRegion", - "QResizeEvent", - "QSessionManager", - "QShortcutEvent", - "QShowEvent", - "QStandardItem", - "QStandardItemModel", - "QStatusTipEvent", - "QSyntaxHighlighter", - "QTabletEvent", - "QTextBlock", - "QTextBlockFormat", - "QTextBlockGroup", - "QTextBlockUserData", - "QTextCharFormat", - "QTextCursor", - "QTextDocument", - "QTextDocumentFragment", - "QTextFormat", - "QTextFragment", - "QTextFrame", - "QTextFrameFormat", - "QTextImageFormat", - "QTextInlineObject", - "QTextItem", - "QTextLayout", - "QTextLength", - "QTextLine", - "QTextList", - "QTextListFormat", - "QTextObject", - "QTextObjectInterface", - "QTextOption", - "QTextTable", - "QTextTableCell", - "QTextTableCellFormat", - "QTextTableFormat", - "QTransform", - "QValidator", - "QVector2D", - "QVector3D", - "QVector4D", - "QWhatsThisClickedEvent", - "QWheelEvent", - "QWindowStateChangeEvent", - "qAlpha", - "qBlue", - "qGray", - "qGreen", - "qIsGray", - "qRed", - "qRgb", - "qRgb", - ], - "QtWidgets": [ - "QAbstractButton", - "QAbstractGraphicsShapeItem", - "QAbstractItemDelegate", - "QAbstractItemView", - "QAbstractScrollArea", - "QAbstractSlider", - "QAbstractSpinBox", - "QAction", - "QActionGroup", - "QApplication", - "QBoxLayout", - "QButtonGroup", - "QCalendarWidget", - "QCheckBox", - "QColorDialog", - "QColumnView", - "QComboBox", - "QCommandLinkButton", - "QCommonStyle", - "QCompleter", - "QDataWidgetMapper", - "QDateEdit", - "QDateTimeEdit", - "QDesktopWidget", - "QDial", - "QDialog", - "QDialogButtonBox", - "QDirModel", - "QDockWidget", - "QDoubleSpinBox", - "QErrorMessage", - "QFileDialog", - "QFileIconProvider", - "QFileSystemModel", - "QFocusFrame", - "QFontComboBox", - "QFontDialog", - "QFormLayout", - "QFrame", - "QGesture", - "QGestureEvent", - "QGestureRecognizer", - "QGraphicsAnchor", - "QGraphicsAnchorLayout", - "QGraphicsBlurEffect", - "QGraphicsColorizeEffect", - "QGraphicsDropShadowEffect", - "QGraphicsEffect", - "QGraphicsEllipseItem", - "QGraphicsGridLayout", - "QGraphicsItem", - "QGraphicsItemGroup", - "QGraphicsLayout", - "QGraphicsLayoutItem", - "QGraphicsLineItem", - "QGraphicsLinearLayout", - "QGraphicsObject", - "QGraphicsOpacityEffect", - "QGraphicsPathItem", - "QGraphicsPixmapItem", - "QGraphicsPolygonItem", - "QGraphicsProxyWidget", - "QGraphicsRectItem", - "QGraphicsRotation", - "QGraphicsScale", - "QGraphicsScene", - "QGraphicsSceneContextMenuEvent", - "QGraphicsSceneDragDropEvent", - "QGraphicsSceneEvent", - "QGraphicsSceneHelpEvent", - "QGraphicsSceneHoverEvent", - "QGraphicsSceneMouseEvent", - "QGraphicsSceneMoveEvent", - "QGraphicsSceneResizeEvent", - "QGraphicsSceneWheelEvent", - "QGraphicsSimpleTextItem", - "QGraphicsTextItem", - "QGraphicsTransform", - "QGraphicsView", - "QGraphicsWidget", - "QGridLayout", - "QGroupBox", - "QHBoxLayout", - "QHeaderView", - "QInputDialog", - "QItemDelegate", - "QItemEditorCreatorBase", - "QItemEditorFactory", - "QKeyEventTransition", - "QLCDNumber", - "QLabel", - "QLayout", - "QLayoutItem", - "QLineEdit", - "QListView", - "QListWidget", - "QListWidgetItem", - "QMainWindow", - "QMdiArea", - "QMdiSubWindow", - "QMenu", - "QMenuBar", - "QMessageBox", - "QMouseEventTransition", - "QPanGesture", - "QPinchGesture", - "QPlainTextDocumentLayout", - "QPlainTextEdit", - "QProgressBar", - "QProgressDialog", - "QPushButton", - "QRadioButton", - "QRubberBand", - "QScrollArea", - "QScrollBar", - "QShortcut", - "QSizeGrip", - "QSizePolicy", - "QSlider", - "QSpacerItem", - "QSpinBox", - "QSplashScreen", - "QSplitter", - "QSplitterHandle", - "QStackedLayout", - "QStackedWidget", - "QStatusBar", - "QStyle", - "QStyleFactory", - "QStyleHintReturn", - "QStyleHintReturnMask", - "QStyleHintReturnVariant", - "QStyleOption", - "QStyleOptionButton", - "QStyleOptionComboBox", - "QStyleOptionComplex", - "QStyleOptionDockWidget", - "QStyleOptionFocusRect", - "QStyleOptionFrame", - "QStyleOptionGraphicsItem", - "QStyleOptionGroupBox", - "QStyleOptionHeader", - "QStyleOptionMenuItem", - "QStyleOptionProgressBar", - "QStyleOptionRubberBand", - "QStyleOptionSizeGrip", - "QStyleOptionSlider", - "QStyleOptionSpinBox", - "QStyleOptionTab", - "QStyleOptionTabBarBase", - "QStyleOptionTabWidgetFrame", - "QStyleOptionTitleBar", - "QStyleOptionToolBar", - "QStyleOptionToolBox", - "QStyleOptionToolButton", - "QStyleOptionViewItem", - "QStylePainter", - "QStyledItemDelegate", - "QSwipeGesture", - "QSystemTrayIcon", - "QTabBar", - "QTabWidget", - "QTableView", - "QTableWidget", - "QTableWidgetItem", - "QTableWidgetSelectionRange", - "QTapAndHoldGesture", - "QTapGesture", - "QTextBrowser", - "QTextEdit", - "QTimeEdit", - "QToolBar", - "QToolBox", - "QToolButton", - "QToolTip", - "QTreeView", - "QTreeWidget", - "QTreeWidgetItem", - "QTreeWidgetItemIterator", - "QUndoCommand", - "QUndoGroup", - "QUndoStack", - "QUndoView", - "QVBoxLayout", - "QWhatsThis", - "QWidget", - "QWidgetAction", - "QWidgetItem", - "QWizard", - "QWizardPage", - ], - "QtCore": [ - "QAbstractAnimation", - "QAbstractEventDispatcher", - "QAbstractItemModel", - "QAbstractListModel", - "QAbstractState", - "QAbstractTableModel", - "QAbstractTransition", - "QAnimationGroup", - "QBasicTimer", - "QBitArray", - "QBuffer", - "QByteArray", - "QByteArrayMatcher", - "QChildEvent", - "QCoreApplication", - "QCryptographicHash", - "QDataStream", - "QDate", - "QDateTime", - "QDir", - "QDirIterator", - "QDynamicPropertyChangeEvent", - "QEasingCurve", - "QElapsedTimer", - "QEvent", - "QEventLoop", - "QEventTransition", - "QFile", - "QFileInfo", - "QFileSystemWatcher", - "QFinalState", - "QGenericArgument", - "QGenericReturnArgument", - "QHistoryState", - "QIODevice", - "QLibraryInfo", - "QLine", - "QLineF", - "QLocale", - "QMargins", - "QMetaClassInfo", - "QMetaEnum", - "QMetaMethod", - "QMetaObject", - "QMetaProperty", - "QMimeData", - "QModelIndex", - "QMutex", - "QMutexLocker", - "QObject", - "QParallelAnimationGroup", - "QPauseAnimation", - "QPersistentModelIndex", - "QPluginLoader", - "QPoint", - "QPointF", - "QProcess", - "QProcessEnvironment", - "QPropertyAnimation", - "QReadLocker", - "QReadWriteLock", - "QRect", - "QRectF", - "QRegExp", - "QResource", - "QRunnable", - "QSemaphore", - "QSequentialAnimationGroup", - "QSettings", - "QSignalMapper", - "QSignalTransition", - "QSize", - "QSizeF", - "QSocketNotifier", - "QState", - "QStateMachine", - "QSysInfo", - "QSystemSemaphore", - "QTemporaryFile", - "QTextBoundaryFinder", - "QTextCodec", - "QTextDecoder", - "QTextEncoder", - "QTextStream", - "QTextStreamManipulator", - "QThread", - "QThreadPool", - "QTime", - "QTimeLine", - "QTimer", - "QTimerEvent", - "QTranslator", - "QUrl", - "QVariantAnimation", - "QWaitCondition", - "QWriteLocker", - "QXmlStreamAttribute", - "QXmlStreamAttributes", - "QXmlStreamEntityDeclaration", - "QXmlStreamEntityResolver", - "QXmlStreamNamespaceDeclaration", - "QXmlStreamNotationDeclaration", - "QXmlStreamReader", - "QXmlStreamWriter", - "Qt", - "QtCriticalMsg", - "QtDebugMsg", - "QtFatalMsg", - "QtMsgType", - "QtSystemMsg", - "QtWarningMsg", - "qAbs", - "qAddPostRoutine", - "qChecksum", - "qCritical", - "qDebug", - "qFatal", - "qFuzzyCompare", - "qIsFinite", - "qIsInf", - "qIsNaN", - "qIsNull", - "qRegisterResourceData", - "qUnregisterResourceData", - "qVersion", - "qWarning", - "qrand", - "qsrand", - ], - "QtXml": [ - "QDomAttr", - "QDomCDATASection", - "QDomCharacterData", - "QDomComment", - "QDomDocument", - "QDomDocumentFragment", - "QDomDocumentType", - "QDomElement", - "QDomEntity", - "QDomEntityReference", - "QDomImplementation", - "QDomNamedNodeMap", - "QDomNode", - "QDomNodeList", - "QDomNotation", - "QDomProcessingInstruction", - "QDomText", - "QXmlAttributes", - "QXmlContentHandler", - "QXmlDTDHandler", - "QXmlDeclHandler", - "QXmlDefaultHandler", - "QXmlEntityResolver", - "QXmlErrorHandler", - "QXmlInputSource", - "QXmlLexicalHandler", - "QXmlLocator", - "QXmlNamespaceSupport", - "QXmlParseException", - "QXmlReader", - "QXmlSimpleReader" - ], - "QtHelp": [ - "QHelpContentItem", - "QHelpContentModel", - "QHelpContentWidget", - "QHelpEngine", - "QHelpEngineCore", - "QHelpIndexModel", - "QHelpIndexWidget", - "QHelpSearchEngine", - "QHelpSearchQuery", - "QHelpSearchQueryWidget", - "QHelpSearchResultWidget" - ], - "QtNetwork": [ - "QAbstractNetworkCache", - "QAbstractSocket", - "QAuthenticator", - "QHostAddress", - "QHostInfo", - "QLocalServer", - "QLocalSocket", - "QNetworkAccessManager", - "QNetworkAddressEntry", - "QNetworkCacheMetaData", - "QNetworkConfiguration", - "QNetworkConfigurationManager", - "QNetworkCookie", - "QNetworkCookieJar", - "QNetworkDiskCache", - "QNetworkInterface", - "QNetworkProxy", - "QNetworkProxyFactory", - "QNetworkProxyQuery", - "QNetworkReply", - "QNetworkRequest", - "QNetworkSession", - "QSsl", - "QTcpServer", - "QTcpSocket", - "QUdpSocket" - ], - "QtOpenGL": [ - "QGL", - "QGLContext", - "QGLFormat", - "QGLWidget" - ] -} - - -def _new_module(name): - return types.ModuleType(__name__ + "." + name) - - -def _setup(module, extras): - """Install common submodules""" - - Qt.__binding__ = module.__name__ - - for name in list(_common_members) + extras: - try: - # print("Trying %s" % name) - submodule = importlib.import_module( - module.__name__ + "." + name) - except ImportError: - # print("Failed %s" % name) - continue - - setattr(Qt, "_" + name, submodule) - - if name not in extras: - # Store reference to original binding, - # but don't store speciality modules - # such as uic or QtUiTools - setattr(Qt, name, _new_module(name)) - - -def _pyside2(): - """Initialise PySide2 - - These functions serve to test the existence of a binding - along with set it up in such a way that it aligns with - the final step; adding members from the original binding - to Qt.py - - """ - - import PySide2 as module - _setup(module, ["QtUiTools"]) - - Qt.__binding_version__ = module.__version__ - - if hasattr(Qt, "_QtUiTools"): - Qt.QtCompat.loadUi = lambda fname: \ - Qt._QtUiTools.QUiLoader().load(fname) - - if hasattr(Qt, "_QtGui") and hasattr(Qt, "_QtCore"): - Qt.QtCore.QStringListModel = Qt._QtGui.QStringListModel - - if hasattr(Qt, "_QtWidgets"): - Qt.QtCompat.setSectionResizeMode = \ - Qt._QtWidgets.QHeaderView.setSectionResizeMode - - if hasattr(Qt, "_QtCore"): - Qt.__qt_version__ = Qt._QtCore.qVersion() - Qt.QtCompat.translate = Qt._QtCore.QCoreApplication.translate - - Qt.QtCore.Property = Qt._QtCore.Property - Qt.QtCore.Signal = Qt._QtCore.Signal - Qt.QtCore.Slot = Qt._QtCore.Slot - - Qt.QtCore.QAbstractProxyModel = Qt._QtCore.QAbstractProxyModel - Qt.QtCore.QSortFilterProxyModel = Qt._QtCore.QSortFilterProxyModel - Qt.QtCore.QItemSelection = Qt._QtCore.QItemSelection - Qt.QtCore.QItemSelectionRange = Qt._QtCore.QItemSelectionRange - Qt.QtCore.QItemSelectionModel = Qt._QtCore.QItemSelectionModel - - -def _pyside(): - """Initialise PySide""" - - import PySide as module - _setup(module, ["QtUiTools"]) - - Qt.__binding_version__ = module.__version__ - - if hasattr(Qt, "_QtUiTools"): - Qt.QtCompat.loadUi = lambda fname: \ - Qt._QtUiTools.QUiLoader().load(fname) - - if hasattr(Qt, "_QtGui"): - setattr(Qt, "QtWidgets", _new_module("QtWidgets")) - setattr(Qt, "_QtWidgets", Qt._QtGui) - - Qt.QtCompat.setSectionResizeMode = Qt._QtGui.QHeaderView.setResizeMode - - if hasattr(Qt, "_QtCore"): - Qt.QtCore.QAbstractProxyModel = Qt._QtGui.QAbstractProxyModel - Qt.QtCore.QSortFilterProxyModel = Qt._QtGui.QSortFilterProxyModel - Qt.QtCore.QStringListModel = Qt._QtGui.QStringListModel - Qt.QtCore.QItemSelection = Qt._QtGui.QItemSelection - Qt.QtCore.QItemSelectionRange = Qt._QtGui.QItemSelectionRange - Qt.QtCore.QItemSelectionModel = Qt._QtGui.QItemSelectionModel - - if hasattr(Qt, "_QtCore"): - Qt.__qt_version__ = Qt._QtCore.qVersion() - - Qt.QtCore.Property = Qt._QtCore.Property - Qt.QtCore.Signal = Qt._QtCore.Signal - Qt.QtCore.Slot = Qt._QtCore.Slot - - QCoreApplication = Qt._QtCore.QCoreApplication - Qt.QtCompat.translate = ( - lambda context, sourceText, disambiguation, n: - QCoreApplication.translate( - context, - sourceText, - disambiguation, - QCoreApplication.CodecForTr, - n - ) - ) - - -def _pyqt5(): - """Initialise PyQt5""" - - import PyQt5 as module - _setup(module, ["uic"]) - - if hasattr(Qt, "_uic"): - Qt.QtCompat.loadUi = lambda fname: Qt._uic.loadUi(fname) - - if hasattr(Qt, "_QtWidgets"): - Qt.QtCompat.setSectionResizeMode = \ - Qt._QtWidgets.QHeaderView.setSectionResizeMode - - if hasattr(Qt, "_QtCore"): - Qt.QtCompat.translate = Qt._QtCore.QCoreApplication.translate - - Qt.QtCore.Property = Qt._QtCore.pyqtProperty - Qt.QtCore.Signal = Qt._QtCore.pyqtSignal - Qt.QtCore.Slot = Qt._QtCore.pyqtSlot - - Qt.QtCore.QAbstractProxyModel = Qt._QtCore.QAbstractProxyModel - Qt.QtCore.QSortFilterProxyModel = Qt._QtCore.QSortFilterProxyModel - Qt.QtCore.QStringListModel = Qt._QtCore.QStringListModel - Qt.QtCore.QItemSelection = Qt._QtCore.QItemSelection - Qt.QtCore.QItemSelectionModel = Qt._QtCore.QItemSelectionModel - Qt.QtCore.QItemSelectionRange = Qt._QtCore.QItemSelectionRange - - Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR - Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR - - -def _pyqt4(): - """Initialise PyQt4""" - - import sip - - # Validation of envivornment variable. Prevents an error if - # the variable is invalid since it's just a hint. - try: - hint = int(QT_SIP_API_HINT) - except TypeError: - hint = None # Variable was None, i.e. not set. - except ValueError: - raise ImportError("QT_SIP_API_HINT=%s must be a 1 or 2") - - for api in ("QString", - "QVariant", - "QDate", - "QDateTime", - "QTextStream", - "QTime", - "QUrl"): - try: - sip.setapi(api, hint or 2) - except AttributeError: - raise ImportError("PyQt4 < 4.6 isn't supported by Qt.py") - except ValueError: - actual = sip.getapi(api) - if not hint: - raise ImportError("API version already set to %d" % actual) - else: - # Having provided a hint indicates a soft constraint, one - # that doesn't throw an exception. - sys.stderr.write( - "Warning: API '%s' has already been set to %d.\n" - % (api, actual) - ) - - import PyQt4 as module - _setup(module, ["uic"]) - - if hasattr(Qt, "_uic"): - Qt.QtCompat.loadUi = lambda fname: Qt._uic.loadUi(fname) - - if hasattr(Qt, "_QtGui"): - setattr(Qt, "QtWidgets", _new_module("QtWidgets")) - setattr(Qt, "_QtWidgets", Qt._QtGui) - - Qt.QtCompat.setSectionResizeMode = \ - Qt._QtGui.QHeaderView.setResizeMode - - if hasattr(Qt, "_QtCore"): - Qt.QtCore.QAbstractProxyModel = Qt._QtGui.QAbstractProxyModel - Qt.QtCore.QSortFilterProxyModel = Qt._QtGui.QSortFilterProxyModel - Qt.QtCore.QItemSelection = Qt._QtGui.QItemSelection - Qt.QtCore.QStringListModel = Qt._QtGui.QStringListModel - Qt.QtCore.QItemSelectionModel = Qt._QtGui.QItemSelectionModel - Qt.QtCore.QItemSelectionRange = Qt._QtGui.QItemSelectionRange - - if hasattr(Qt, "_QtCore"): - Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR - Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR - - Qt.QtCore.Property = Qt._QtCore.pyqtProperty - Qt.QtCore.Signal = Qt._QtCore.pyqtSignal - Qt.QtCore.Slot = Qt._QtCore.pyqtSlot - - QCoreApplication = Qt._QtCore.QCoreApplication - Qt.QtCompat.translate = ( - lambda context, sourceText, disambiguation, n: - QCoreApplication.translate( - context, - sourceText, - disambiguation, - QCoreApplication.CodecForTr, - n) - ) - - -def _none(): - """Internal option (used in installer)""" - - Mock = type("Mock", (), {"__getattr__": lambda Qt, attr: None}) - - Qt.__binding__ = "None" - Qt.__qt_version__ = "0.0.0" - Qt.__binding_version__ = "0.0.0" - Qt.QtCompat.loadUi = lambda fname: None - Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None - - for submodule in _common_members.keys(): - setattr(Qt, submodule, Mock()) - setattr(Qt, "_" + submodule, Mock()) - - -def _log(text): - if QT_VERBOSE: - sys.stdout.write(text + "\n") - - -def _convert(lines): - """Convert compiled .ui file from PySide2 to Qt.py - - Arguments: - lines (list): Each line of of .ui file - - Usage: - >> with open("myui.py") as f: - .. lines = _convert(f.readlines()) - - """ - - def parse(line): - line = line.replace("from PySide2 import", "from Qt import") - line = line.replace("QtWidgets.QApplication.translate", - "Qt.QtCompat.translate") - return line - - parsed = list() - for line in lines: - line = parse(line) - parsed.append(line) - - return parsed - - -def _cli(args): - """Qt.py command-line interface""" - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument("--convert", - help="Path to compiled Python module, e.g. my_ui.py") - parser.add_argument("--compile", - help="Accept raw .ui file and compile with native " - "PySide2 compiler.") - parser.add_argument("--stdout", - help="Write to stdout instead of file", - action="store_true") - parser.add_argument("--stdin", - help="Read from stdin instead of file", - action="store_true") - - args = parser.parse_args(args) - - if args.stdout: - raise NotImplementedError("--stdout") - - if args.stdin: - raise NotImplementedError("--stdin") - - if args.compile: - raise NotImplementedError("--compile") - - if args.convert: - sys.stdout.write("#\n" - "# WARNING: --convert is an ALPHA feature.\n#\n" - "# See https://github.com/mottosso/Qt.py/pull/132\n" - "# for details.\n" - "#\n") - - # - # ------> Read - # - with open(args.convert) as f: - lines = _convert(f.readlines()) - - backup = "%s_backup%s" % os.path.splitext(args.convert) - sys.stdout.write("Creating \"%s\"..\n" % backup) - shutil.copy(args.convert, backup) - - # - # <------ Write - # - with open(args.convert, "w") as f: - f.write("".join(lines)) - - sys.stdout.write("Successfully converted \"%s\"\n" % args.convert) - - -def _install(): - # Default order (customise order and content via QT_PREFERRED_BINDING) - default_order = ("PySide2", "PyQt5", "PySide", "PyQt4") - preferred_order = list( - b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b - ) - - order = preferred_order or default_order - - available = { - "PySide2": _pyside2, - "PyQt5": _pyqt5, - "PySide": _pyside, - "PyQt4": _pyqt4, - "None": _none - } - - _log("Order: '%s'" % "', '".join(order)) - - found_binding = False - for name in order: - _log("Trying %s" % name) - - try: - available[name]() - found_binding = True - break - - except ImportError as e: - _log("ImportError: %s" % e) - - except KeyError: - _log("ImportError: Preferred binding '%s' not found." % name) - - if not found_binding: - # If not binding were found, throw this error - raise ImportError("No Qt binding were found.") - - # Install individual members - for name, members in _common_members.items(): - try: - their_submodule = getattr(Qt, "_%s" % name) - except AttributeError: - continue - - our_submodule = getattr(Qt, name) - - # Enable import * - __all__.append(name) - - # Enable direct import of submodule, - # e.g. import Qt.QtCore - sys.modules[__name__ + "." + name] = our_submodule - - for member in members: - # Accept that a submodule may miss certain members. - try: - their_member = getattr(their_submodule, member) - except AttributeError: - _log("'%s.%s' was missing." % (name, member)) - continue - - setattr(our_submodule, member, their_member) - - # Backwards compatibility - Qt.QtCompat.load_ui = Qt.QtCompat.loadUi - - -_install() - - -"""Augment QtCompat - -QtCompat contains wrappers and added functionality -to the original bindings, such as the CLI interface -and otherwise incompatible members between bindings, -such as `QHeaderView.setSectionResizeMode`. - -""" - -Qt.QtCompat._cli = _cli -Qt.QtCompat._convert = _convert - -# Enable command-line interface -if __name__ == "__main__": - _cli(sys.argv[1:]) diff --git a/openpype/vendor/python/common/capture_gui/version.py b/openpype/vendor/python/common/capture_gui/version.py deleted file mode 100644 index badefb1659..0000000000 --- a/openpype/vendor/python/common/capture_gui/version.py +++ /dev/null @@ -1,9 +0,0 @@ -VERSION_MAJOR = 1 -VERSION_MINOR = 5 -VERSION_PATCH = 0 - - -version = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) -__version__ = version - -__all__ = ['version', 'version_info', '__version__'] diff --git a/openpype/version.py b/openpype/version.py index 202cb9348e..6bcc8dcfb8 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.0.0-rc.6" +__version__ = "3.0.0" diff --git a/openpype/widgets/__init__.py b/openpype/widgets/__init__.py index e69de29bb2..b0552c7a0d 100644 --- a/openpype/widgets/__init__.py +++ b/openpype/widgets/__init__.py @@ -0,0 +1,6 @@ +from .password_dialog import PasswordDialog + + +__all__ = ( + "PasswordDialog", +) diff --git a/openpype/tools/settings/widgets.py b/openpype/widgets/password_dialog.py similarity index 93% rename from openpype/tools/settings/widgets.py rename to openpype/widgets/password_dialog.py index e2662f350f..9990642ca1 100644 --- a/openpype/tools/settings/widgets.py +++ b/openpype/widgets/password_dialog.py @@ -1,6 +1,7 @@ from Qt import QtWidgets, QtCore, QtGui -from .resources import get_resource +from openpype import style +from openpype.resources import get_resource from openpype.api import get_system_settings from openpype.settings.lib import ( @@ -43,7 +44,7 @@ class PasswordDialog(QtWidgets.QDialog): def __init__(self, parent=None, allow_remember=True): super(PasswordDialog, self).__init__(parent) - self.setWindowTitle("Settings Password") + self.setWindowTitle("Admin Password") self.resize(300, 120) system_settings = get_system_settings() @@ -62,13 +63,11 @@ class PasswordDialog(QtWidgets.QDialog): password_input = QtWidgets.QLineEdit(password_widget) password_input.setEchoMode(QtWidgets.QLineEdit.Password) - show_password_icon_path = get_resource("images", "eye.png") + show_password_icon_path = get_resource("icons", "eye.png") show_password_icon = QtGui.QIcon(show_password_icon_path) show_password_btn = PressHoverButton(password_widget) + show_password_btn.setObjectName("PasswordBtn") show_password_btn.setIcon(show_password_icon) - show_password_btn.setStyleSheet(( - "border: none;padding:0.1em;" - )) show_password_btn.setFocusPolicy(QtCore.Qt.ClickFocus) password_layout = QtWidgets.QHBoxLayout(password_widget) @@ -83,10 +82,8 @@ class PasswordDialog(QtWidgets.QDialog): buttons_widget = QtWidgets.QWidget(self) remember_checkbox = QtWidgets.QCheckBox("Remember", buttons_widget) + remember_checkbox.setObjectName("RememberCheckbox") remember_checkbox.setVisible(allow_remember) - remember_checkbox.setStyleSheet(( - "spacing: 0.5em;" - )) ok_btn = QtWidgets.QPushButton("Ok", buttons_widget) cancel_btn = QtWidgets.QPushButton("Cancel", buttons_widget) @@ -114,6 +111,8 @@ class PasswordDialog(QtWidgets.QDialog): self.remember_checkbox = remember_checkbox self.message_label = message_label + self.setStyleSheet(style.load_stylesheet()) + def remember_password(self): if not self._allow_remember: return False diff --git a/pyproject.toml b/pyproject.toml index 7ba869e50e..2b52c6f83e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.0.0-rc.6" +version = "3.0.0" description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" @@ -9,6 +9,23 @@ documentation = "https://openpype.io/docs/artist_getting_started" repository = "https://github.com/pypeclub/openpype" readme = "README.md" keywords = ["Pipeline", "Avalon", "VFX", "animation", "automation", "tracking", "asset management"] +packages = [ + {include = "igniter"}, + {include = "repos"}, + {include = "tools"}, + {include = "tests"}, + {include = "docs"}, + {include = "openpype"}, + {include = "start.py"}, + {include = "LICENSE"}, + {include = "README.md"}, + {include = "setup.py"}, + {include = "pyproject.toml"}, + {include = "poetry.lock"} +] + +[tool.poetry.scripts] +openpype = 'start:boot' [tool.poetry.dependencies] python = "3.7.*" @@ -51,6 +68,7 @@ flake8 = "^3.7" autopep8 = "^1.4" coverage = "*" cx_freeze = "^6.6" +GitPython = "*" jedi = "^0.13" Jinja2 = "^2.11" pycodestyle = "^2.5.0" diff --git a/repos/avalon-core b/repos/avalon-core index 0d9a228fdb..e871108c8e 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 0d9a228fdb2eb08fe6caa30f25fe2a34fead1a03 +Subproject commit e871108c8e33a7ae3741df33027f9af52667df34 diff --git a/setup.py b/setup.py index 5fb0b33f2a..927dd28afd 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,9 @@ install_requires = [ ] includes = [] -excludes = [] +excludes = [ + "openpype" +] bin_includes = [] include_files = [ "igniter", diff --git a/start.py b/start.py index 8ee9775ce8..8e7c195e95 100644 --- a/start.py +++ b/start.py @@ -100,7 +100,6 @@ import traceback import subprocess import site from pathlib import Path -import platform # OPENPYPE_ROOT is variable pointing to build (or code) directory @@ -113,17 +112,6 @@ if not getattr(sys, 'frozen', False): else: OPENPYPE_ROOT = os.path.dirname(sys.executable) - # FIX #1469: Certificates from certifi are not available in some - # macos builds, so connection to ftrack/mongo will fail with - # unable to verify certificate issuer error. This will add certifi - # certificates so ssl can see them. - # WARNING: this can break stuff if custom certificates are used. In that - # case they need to be merged to certificate bundle and SSL_CERT_FILE - # should point to them. - if not os.getenv("SSL_CERT_FILE") and platform.system().lower() == "darwin": # noqa: E501 - ssl_cert_file = Path(OPENPYPE_ROOT) / "dependencies" / "certifi" / "cacert.pem" # noqa: E501 - os.environ["SSL_CERT_FILE"] = ssl_cert_file.as_posix() - # add dependencies folder to sys.pat for frozen code frozen_libs = os.path.normpath( os.path.join(OPENPYPE_ROOT, "dependencies") @@ -136,6 +124,44 @@ else: paths.append(frozen_libs) os.environ["PYTHONPATH"] = os.pathsep.join(paths) + +import blessed # noqa: E402 +import certifi # noqa: E402 + + +if sys.__stdout__: + term = blessed.Terminal() + + def _print(message: str): + if message.startswith("!!! "): + print("{}{}".format(term.orangered2("!!! "), message[4:])) + if message.startswith(">>> "): + print("{}{}".format(term.aquamarine3(">>> "), message[4:])) + if message.startswith("--- "): + print("{}{}".format(term.darkolivegreen3("--- "), message[4:])) + if message.startswith(" "): + print("{}{}".format(term.darkseagreen3(" "), message[4:])) + if message.startswith("*** "): + print("{}{}".format(term.gold("*** "), message[4:])) + if message.startswith(" - "): + print("{}{}".format(term.wheat(" - "), message[4:])) + if message.startswith(" . "): + print("{}{}".format(term.tan(" . "), message[4:])) +else: + def _print(message: str): + print(message) + + +# if SSL_CERT_FILE is not set prior to OpenPype launch, we set it to point +# to certifi bundle to make sure we have reasonably new CA certificates. +if os.getenv("SSL_CERT_FILE") and \ + os.getenv("SSL_CERT_FILE") != certifi.where(): + _print("--- your system is set to use custom CA certificate bundle.") +else: + ssl_cert_file = certifi.where() + os.environ["SSL_CERT_FILE"] = ssl_cert_file + + import igniter # noqa: E402 from igniter import BootstrapRepos # noqa: E402 from igniter.tools import ( @@ -198,7 +224,7 @@ def run(arguments: list, env: dict = None) -> int: p = subprocess.Popen(interpreter, env=env) p.wait() - print(f">>> done [{p.returncode}]") + _print(f">>> done [{p.returncode}]") return p.returncode @@ -279,8 +305,8 @@ def _process_arguments() -> tuple: use_staging = False for arg in sys.argv: if arg == "--use-version": - print("!!! Please use option --use-version like:") - print(" --use-version=3.0.0") + _print("!!! Please use option --use-version like:") + _print(" --use-version=3.0.0") sys.exit(1) m = re.search( @@ -338,12 +364,12 @@ def _determine_mongodb() -> str: if openpype_mongo: result, msg = validate_mongo_connection(openpype_mongo) if not result: - print(msg) + _print(msg) openpype_mongo = None if not openpype_mongo: - print("*** No DB connection string specified.") - print("--- launching setup UI ...") + _print("*** No DB connection string specified.") + _print("--- launching setup UI ...") result = igniter.open_dialog() if result == 0: @@ -368,7 +394,7 @@ def _initialize_environment(openpype_version: OpenPypeVersion) -> None: version_path.as_posix() ) # inject version to Python environment (sys.path, ...) - print(">>> Injecting OpenPype version to running environment ...") + _print(">>> Injecting OpenPype version to running environment ...") bootstrap.add_paths_from_directory(version_path) # Additional sys paths related to OPENPYPE_REPOS_ROOT directory @@ -437,7 +463,7 @@ def _find_frozen_openpype(use_version: str = None, os.environ["OPENPYPE_TRYOUT"] = "1" openpype_versions = [] else: - print("!!! Warning: cannot determine current running version.") + _print("!!! Warning: cannot determine current running version.") if not os.getenv("OPENPYPE_TRYOUT"): try: @@ -445,8 +471,8 @@ def _find_frozen_openpype(use_version: str = None, openpype_version = openpype_versions[-1] except IndexError: # no OpenPype version found, run Igniter and ask for them. - print('*** No OpenPype versions found.') - print("--- launching setup UI ...") + _print('*** No OpenPype versions found.') + _print("--- launching setup UI ...") import igniter return_code = igniter.open_dialog() if return_code == 2: @@ -454,25 +480,25 @@ def _find_frozen_openpype(use_version: str = None, if return_code == 3: # run OpenPype after installation - print('>>> Finding OpenPype again ...') + _print('>>> Finding OpenPype again ...') openpype_versions = bootstrap.find_openpype( staging=use_staging) try: openpype_version = openpype_versions[-1] except IndexError: - print(("!!! Something is wrong and we didn't " + _print(("!!! Something is wrong and we didn't " "found it again.")) sys.exit(1) elif return_code != 2: - print(f" . finished ({return_code})") + _print(f" . finished ({return_code})") sys.exit(return_code) if not openpype_versions: # no openpype versions found anyway, lets use then the one # shipped with frozen OpenPype if not os.getenv("OPENPYPE_TRYOUT"): - print("*** Still no luck finding OpenPype.") - print(("*** We'll try to use the one coming " + _print("*** Still no luck finding OpenPype.") + _print(("*** We'll try to use the one coming " "with OpenPype installation.")) version_path = _bootstrap_from_code(use_version) openpype_version = OpenPypeVersion( @@ -493,13 +519,13 @@ def _find_frozen_openpype(use_version: str = None, if found: openpype_version = sorted(found)[-1] if not openpype_version: - print(f"!!! requested version {use_version} was not found.") + _print(f"!!! requested version {use_version} was not found.") if openpype_versions: - print(" - found: ") + _print(" - found: ") for v in sorted(openpype_versions): - print(f" - {v}: {v.path}") + _print(f" - {v}: {v.path}") - print(f" - local version {local_version}") + _print(f" - local version {local_version}") sys.exit(1) # test if latest detected is installed (in user data dir) @@ -518,11 +544,11 @@ def _find_frozen_openpype(use_version: str = None, openpype_version, force=True) if openpype_version.path.is_file(): - print(">>> Extracting zip file ...") + _print(">>> Extracting zip file ...") try: version_path = bootstrap.extract_openpype(openpype_version) except OSError as e: - print("!!! failed: {}".format(str(e))) + _print("!!! failed: {}".format(str(e))) sys.exit(1) else: # cleanup zip after extraction @@ -549,7 +575,7 @@ def _bootstrap_from_code(use_version): _openpype_root = OPENPYPE_ROOT if getattr(sys, 'frozen', False): local_version = bootstrap.get_version(Path(_openpype_root)) - print(f" - running version: {local_version}") + _print(f" - running version: {local_version}") assert local_version else: # get current version of OpenPype @@ -574,13 +600,13 @@ def _bootstrap_from_code(use_version): os.environ["OPENPYPE_REPOS_ROOT"] = (version_path / "openpype").as_posix() # noqa: E501 _openpype_root = version_to_use.path.as_posix() else: - print(f"!!! requested version {use_version} was not found.") + _print(f"!!! requested version {use_version} was not found.") if openpype_versions: - print(" - found: ") + _print(" - found: ") for v in sorted(openpype_versions): - print(f" - {v}: {v.path}") + _print(f" - {v}: {v.path}") - print(f" - local version {local_version}") + _print(f" - local version {local_version}") sys.exit(1) else: os.environ["OPENPYPE_VERSION"] = local_version @@ -663,7 +689,7 @@ def boot(): openpype_mongo = _determine_mongodb() except RuntimeError as e: # without mongodb url we are done for. - print(f"!!! {e}") + _print(f"!!! {e}") sys.exit(1) os.environ["OPENPYPE_MONGO"] = openpype_mongo @@ -673,7 +699,7 @@ def boot(): # find its versions there and bootstrap them. openpype_path = get_openpype_path_from_db(openpype_mongo) if not openpype_path: - print("*** Cannot get OpenPype path from database.") + _print("*** Cannot get OpenPype path from database.") if not os.getenv("OPENPYPE_PATH") and openpype_path: os.environ["OPENPYPE_PATH"] = openpype_path @@ -689,7 +715,7 @@ def boot(): version_path = _find_frozen_openpype(use_version, use_staging) except RuntimeError as e: # no version to run - print(f"!!! {e}") + _print(f"!!! {e}") sys.exit(1) else: version_path = _bootstrap_from_code(use_version) @@ -714,13 +740,13 @@ def boot(): except KeyError: pass - print(">>> loading environments ...") + _print(">>> loading environments ...") # Avalon environments must be set before avalon module is imported - print(" - for Avalon ...") + _print(" - for Avalon ...") set_avalon_environments() - print(" - global OpenPype ...") + _print(" - global OpenPype ...") set_openpype_global_environments() - print(" - for modules ...") + _print(" - for modules ...") set_modules_environments() from openpype import cli @@ -750,7 +776,7 @@ def boot(): cli.main(obj={}, prog_name="openpype") except Exception: # noqa exc_info = sys.exc_info() - print("!!! OpenPype crashed:") + _print("!!! OpenPype crashed:") traceback.print_exception(*exc_info) sys.exit(1) diff --git a/tools/build_dependencies.py b/tools/build_dependencies.py index de3b6da021..3898450471 100644 --- a/tools/build_dependencies.py +++ b/tools/build_dependencies.py @@ -120,7 +120,7 @@ _print("Copying dependencies ...") total_files = count_folders(site_pkg) progress_bar = enlighten.Counter( total=total_files, desc="Processing Dependencies", - units="%", color="green") + units="%", color=(53, 178, 202)) def _progress(_base, _names): @@ -140,7 +140,8 @@ to_delete = [] deps_items = list(deps_dir.iterdir()) item_count = len(list(libs_dir.iterdir())) find_progress_bar = enlighten.Counter( - total=item_count, desc="Finding duplicates", units="%", color="yellow") + total=item_count, desc="Finding duplicates", units="%", + color=(56, 211, 159)) for d in libs_dir.iterdir(): if (deps_dir / d.name) in deps_items: @@ -152,16 +153,23 @@ find_progress_bar.close() # add openpype and igniter in libs too to_delete.append(libs_dir / "openpype") to_delete.append(libs_dir / "igniter") +to_delete.append(libs_dir / "openpype.pth") +to_delete.append(deps_dir / "openpype.pth") # delete duplicates # _print(f"Deleting {len(to_delete)} duplicates ...") delete_progress_bar = enlighten.Counter( - total=len(to_delete), desc="Deleting duplicates", units="%", color="red") + total=len(to_delete), desc="Deleting duplicates", units="%", + color=(251, 192, 32)) for d in to_delete: if d.is_dir(): shutil.rmtree(d) else: - d.unlink() + try: + d.unlink() + except FileNotFoundError: + # skip non-existent silently + pass delete_progress_bar.update() delete_progress_bar.close() diff --git a/tools/ci_tools.py b/tools/ci_tools.py new file mode 100644 index 0000000000..28a735222b --- /dev/null +++ b/tools/ci_tools.py @@ -0,0 +1,140 @@ +import re +import sys +from semver import VersionInfo +from git import Repo +from optparse import OptionParser + + +def remove_prefix(text, prefix): + return text[text.startswith(prefix) and len(prefix):] + + +def get_last_version(match): + repo = Repo() + assert not repo.bare + version_types = { + "CI": "CI/[0-9]*", + "release": "[0-9]*" + } + tag = repo.git.describe( + '--tags', + f'--match={version_types[match]}', + '--abbrev=0' + ) + + if match == "CI": + return remove_prefix(tag, "CI/"), tag + else: + return tag, tag + + +def get_log_since_tag(version): + repo = Repo() + assert not repo.bare + return repo.git.log(f'{version}..HEAD', '--merges', '--oneline') + + +def release_type(log): + regex_minor = ["feature/", "(feat)"] + regex_patch = ["bugfix/", "fix/", "(fix)"] + for reg in regex_minor: + if re.search(reg, log): + return "minor" + for reg in regex_patch: + if re.search(reg, log): + return "patch" + return None + + +def file_regex_replace(filename, regex, version): + with open(filename, 'r+') as f: + text = f.read() + text = re.sub(regex, version, text) + # pp.pprint(f"NEW VERSION {version} INSERTED into {filename}") + f.seek(0) + f.write(text) + f.truncate() + + +def bump_file_versions(version): + + filename = "./openpypeCItest/version.py" + regex = "(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-((0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?" + file_regex_replace(filename, regex, version) + + # bump pyproject.toml + filename = "pyproject.toml" + regex = "version = \"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-((0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?\" # OpenPype" + pyproject_version = f"version = \"{version}\" # OpenPype" + file_regex_replace(filename, regex, pyproject_version) + + +def calculate_next_nightly(token="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) + + last_release, last_release_tag = get_last_version("release") + + last_release_v = VersionInfo.parse(last_release) + bump_type = release_type(get_log_since_tag(last_release)) + if not bump_type: + return None + + next_release_v = last_release_v.next_version(part=bump_type) + # print(next_release_v) + + if next_release_v > last_pre_v_finalized: + next_tag = next_release_v.bump_prerelease(token=token).__str__() + return next_tag + elif next_release_v == last_pre_v_finalized: + next_tag = last_pre_v.bump_prerelease(token=token).__str__() + return next_tag + + +def main(): + usage = "usage: %prog [options] arg" + parser = OptionParser(usage) + parser.add_option("-n", "--nightly", + dest="nightly", action="store_true", + help="Bump nightly version and return it") + 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("-p", "--prerelease", + dest="prerelease", action="store", + help="define prerelease token") + + (options, args) = parser.parse_args() + + if options.bump: + last_CI, last_CI_tag = get_last_version("CI") + last_release, last_release_tag = get_last_version("release") + bump_type_CI = release_type(get_log_since_tag(last_CI_tag)) + bump_type_release = release_type(get_log_since_tag(last_release_tag)) + if bump_type_CI is None or bump_type_release is None: + print("skip") + + if options.nightly: + next_tag_v = calculate_next_nightly() + print(next_tag_v) + bump_file_versions(next_tag_v) + + if options.prerelease: + current_prerelease = VersionInfo.parse(options.prerelease) + new_prerelease = current_prerelease.bump_prerelease().__str__() + print(new_prerelease) + bump_file_versions(new_prerelease) + + if options.version: + bump_file_versions(options.version) + print(f"Injected version {options.version} into the release") + + + +if __name__ == "__main__": + main() diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index 0c4a6c863d..80154356af 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -7,15 +7,24 @@ sidebar_label: System settings import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -## Global +## General Settings applicable to the full studio. -`Studio Name` +**`Studio Name`** - Full name of the studio (can be used as variable on some places) -`Studio Code` +**`Studio Code`** - Studio acronym or a short code (can be used as variable on some places) -`Environment` +**`Admin Password`** - After setting admin password, normal user won't have access to OpenPype settings +and Project Manager GUI. Please keep in mind that this is a studio wide password and it is meant purely +as a naive barier to prevent artists from accidental setting changes. + +**`Environment`** - Globally applied environment variables that will be appended to any OpenPype process in the studio. + +**`Versions Repository`** - Location where automatic update mechanism searches for zip files with +OpenPype update packages. To read more about preparing OpenPype for automatic updates go to [Admin Distribute docs](admin_distribute#2-openpype-codebase) + +![general_settings](assets/settings/settings_system_general.png) ## Modules @@ -24,25 +33,25 @@ their own attributes that need to be set, before they become fully functional. ### Avalon -`Avalon Mongo Timeout` - You might need to change this if your mongo connection is a bit slow. Making the +**`Avalon Mongo Timeout`** - You might need to change this if your mongo connection is a bit slow. Making the timeout longer will give Avalon better chance to connect. -`Thumbnail Storage Location` - simple disk storage path, where all thumbnails will be stored. +**`Thumbnail Storage Location`** - simple disk storage path, where all thumbnails will be stored. ### Ftrack -`Server` - URL of your ftrack server. +**`Server`** - URL of your ftrack server. Additional Action paths -`Action paths` - Directories containing your custom ftrack actions. +**`Action paths`** - Directories containing your custom ftrack actions. -`Event paths` - Directories containing your custom ftrack event plugins. +**`Event paths`** - Directories containing your custom ftrack event plugins. -`Intent` - Special ftrack attribute that mark the intention of individual publishes. This setting will be reflected +**`Intent`** - Special ftrack attribute that mark the intention of individual publishes. This setting will be reflected in publisher as well as ftrack custom attributes -`Custom Attributes` - Write and Read permissions for all OpenPype required ftrack custom attributes. The values should be +**`Custom Attributes`** - Write and Read permissions for all OpenPype required ftrack custom attributes. The values should be ftrack roles names. ### Sync Server @@ -55,25 +64,25 @@ Disable/Enable Standalone Publisher option ### Deadline -`Deadline Rest URL` - URL to deadline webservice that. This URL must be reachable from every +**`Deadline Rest URL`** - URL to deadline webservice that. This URL must be reachable from every workstation that should be submitting render jobs to deadline via OpenPype. ### Muster -`Muster Rest URL` - URL to Muster webservice that. This URL must be reachable from every +**`Muster Rest URL`** - URL to Muster webservice that. This URL must be reachable from every workstation that should be submitting render jobs to muster via OpenPype. -`templates mapping` - you can customize Muster templates to match your existing setup here. +**`templates mapping`** - you can customize Muster templates to match your existing setup here. ### Clockify -`Workspace Name` - name of the clockify workspace where you would like to be sending all the timelogs. +**`Workspace Name`** - name of the clockify workspace where you would like to be sending all the timelogs. ### Timers Manager -`Max Idle Time` - Duration (minutes) of inactivity, after which currently running timer will be stopped. +**`Max Idle Time`** - Duration (minutes) of inactivity, after which currently running timer will be stopped. -`Dialog popup time` - Time in minutes, before the end of Max Idle ti, when a notification will alert +**`Dialog popup time`** - Time in minutes, before the end of Max Idle ti, when a notification will alert the user that their timer is about to be stopped. ### Idle Manager @@ -87,17 +96,20 @@ Module that allows storing all logging into the database for easier retrieval an ## Applications In this section you can manage what Applications are available to your studio, locations of their -executables and their additional environments. +executables and their additional environments. In OpenPype context each application that is integrated is +also called a `Host` and these two terms might be used interchangeably in the documentation. -Each DCC is made of two levels. +Each Host is made of two levels. 1. **Application group** - This is the main name of the application and you can define extra environments -that are applicable to all version of the give application. For example any extra Maya scripts that are not +that are applicable to all versions of the given application. For example any extra Maya scripts that are not version dependant, can be added to `Maya` environment here. 2. **Application versions** - Here you can define executables (per platform) for each supported version of the DCC and any default arguments (`--nukex` for instance). You can also further extend it's environment. ![settings_applications](assets/settings/applications_01.png) +### Environments + Please keep in mind that the environments are not additive by default, so if you are extending variables like `PYTHONPATH`, or `PATH` make sure that you add themselves to the end of the list. @@ -112,8 +124,15 @@ For instance: } ``` +### Adding versions +It is possible to add new version for any supported application. There are two ways of doing it. +1. **Add new executable** to an existing application version. This is a good way if you have multiple fully compatible versions of your DCC across the studio. Nuke is a typical example where multiple artists might have different `v#` releases of the same minor Nuke release. For example `12.2v3` and `12.3v6`. When you add both to `12.2` Nuke executables they will be treated the same in OpenPype and the system will automatically pick the first that it finds on an artist machine when launching. Their order is also the order of their priority when choosing which version to run if multiple are present. +![settings_applications](assets/settings/settings_addapplication.gif) + +2. **Add version** in case you want this version to be selectable individually. This is usually used for bigger releases that might not be fully compatible with previous versions. Keep in mind that if you add the latest version of an Application that is not yet part of the official OpenPype release, you might run into problems with integration. We test all the new software versions for compatibility and most often, smaller or bigger updates to OpenPype code are necessary to keep everything running. +![settings_applications](assets/settings/settings_addappversion.gif) ## Tools diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index fc94f20f02..6fbd59ae1e 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -531,7 +531,10 @@ OpenPype supports creating review video for almost any type of data you want to What we call review video is actually _playblast_ or _capture_ (depending on terminology you are familiar with) made from pre-defined camera in scene. This is very useful in cases where you want to add turntable preview of your model for example. But it can -be used to generate preview for animation, simulations, and so on. +be used to generate preview for animation, simulations, and so on. You can either +publish review as separate subset version, or you can attach generated video to subset you +are publishing - for example attach video of turntable rotation to published model as in +following example. ### Setting scene for review extraction @@ -570,10 +573,14 @@ on this set to control review video generation: * `Step` - number of steps * `Fps` - framerate +Next step is to move your model set to review set so it will be connected to each other. + This is my scene: ![Maya - Review model setup](assets/maya-model_review_setup.jpg) +You see that `modelMain` in under `reviewMain` with `reviewCamera`. + _* note that I had to fix UVs and normals on Stanford dragon model as it wouldn't pass model validators_ @@ -588,6 +595,8 @@ version. All parts of this process - like what burnins, what type of video file, settings for Maya playblast - can be customized by your TDs. For more information about customizing review process refer to [admin section](admin_presets_plugins). +If you don't move `modelMain` into `reviewMain`, review will be generated but it will +be published as separate entity. ## Working with Yeti in OpenPype diff --git a/website/docs/assets/maya-model_review_setup.jpg b/website/docs/assets/maya-model_review_setup.jpg index 6c43807596..16576894b1 100644 Binary files a/website/docs/assets/maya-model_review_setup.jpg and b/website/docs/assets/maya-model_review_setup.jpg differ diff --git a/website/docs/assets/settings/applications_01.png b/website/docs/assets/settings/applications_01.png index 52c31f6649..c82a9e0d0f 100644 Binary files a/website/docs/assets/settings/applications_01.png and b/website/docs/assets/settings/applications_01.png differ diff --git a/website/docs/assets/settings/settings_addapplication.gif b/website/docs/assets/settings/settings_addapplication.gif new file mode 100644 index 0000000000..fa617c7c27 Binary files /dev/null and b/website/docs/assets/settings/settings_addapplication.gif differ diff --git a/website/docs/assets/settings/settings_addappversion.gif b/website/docs/assets/settings/settings_addappversion.gif new file mode 100644 index 0000000000..573dd407b3 Binary files /dev/null and b/website/docs/assets/settings/settings_addappversion.gif differ diff --git a/website/docs/assets/settings/settings_system_general.png b/website/docs/assets/settings/settings_system_general.png new file mode 100644 index 0000000000..4c1452d0d4 Binary files /dev/null and b/website/docs/assets/settings/settings_system_general.png differ diff --git a/website/docs/project_settings/assets/global_extract_review_letter_box.png b/website/docs/project_settings/assets/global_extract_review_letter_box.png index 7cd9ecbdd6..45c1942f24 100644 Binary files a/website/docs/project_settings/assets/global_extract_review_letter_box.png and b/website/docs/project_settings/assets/global_extract_review_letter_box.png differ diff --git a/website/docs/project_settings/assets/global_extract_review_letter_box_settings.png b/website/docs/project_settings/assets/global_extract_review_letter_box_settings.png index 9ad9c05f43..80e00702e6 100644 Binary files a/website/docs/project_settings/assets/global_extract_review_letter_box_settings.png and b/website/docs/project_settings/assets/global_extract_review_letter_box_settings.png differ diff --git a/website/docs/project_settings/assets/global_extract_review_output_defs.png b/website/docs/project_settings/assets/global_extract_review_output_defs.png index 0dc8329324..ce3c00ca40 100644 Binary files a/website/docs/project_settings/assets/global_extract_review_output_defs.png and b/website/docs/project_settings/assets/global_extract_review_output_defs.png differ diff --git a/website/docs/project_settings/assets/global_tools_workfile_open_last_version.png b/website/docs/project_settings/assets/global_tools_workfile_open_last_version.png new file mode 100644 index 0000000000..dfcac072c5 Binary files /dev/null and b/website/docs/project_settings/assets/global_tools_workfile_open_last_version.png differ diff --git a/website/docs/project_settings/assets/nuke_workfile_builder_create_first_workfile.png b/website/docs/project_settings/assets/nuke_workfile_builder_create_first_workfile.png new file mode 100644 index 0000000000..f138709a7f Binary files /dev/null and b/website/docs/project_settings/assets/nuke_workfile_builder_create_first_workfile.png differ diff --git a/website/docs/project_settings/assets/nuke_workfile_builder_location.png b/website/docs/project_settings/assets/nuke_workfile_builder_location.png new file mode 100644 index 0000000000..916b79755d Binary files /dev/null and b/website/docs/project_settings/assets/nuke_workfile_builder_location.png differ diff --git a/website/docs/project_settings/assets/nuke_workfile_builder_profiles.png b/website/docs/project_settings/assets/nuke_workfile_builder_profiles.png new file mode 100644 index 0000000000..e4105767ef Binary files /dev/null and b/website/docs/project_settings/assets/nuke_workfile_builder_profiles.png differ diff --git a/website/docs/project_settings/assets/nuke_workfile_builder_template_anatomy.png b/website/docs/project_settings/assets/nuke_workfile_builder_template_anatomy.png new file mode 100644 index 0000000000..195e884bfb Binary files /dev/null and b/website/docs/project_settings/assets/nuke_workfile_builder_template_anatomy.png differ diff --git a/website/docs/project_settings/assets/nuke_workfile_builder_template_task_type.png b/website/docs/project_settings/assets/nuke_workfile_builder_template_task_type.png new file mode 100644 index 0000000000..4c84f37c0e Binary files /dev/null and b/website/docs/project_settings/assets/nuke_workfile_builder_template_task_type.png differ diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 4fee57d575..5d23dd75e6 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -17,10 +17,10 @@ Projects always use default project values unless they have [project override](. Many of the settings are using a concept of **Profile filters** -You can define multiple profiles to choose from for different contexts. Each filter is evaluated and a -profile with filters matching the current context the most, is used. +You can define multiple profiles to choose from for different contexts. Each filter is evaluated and a +profile with filters matching the current context the most, is used. -You can define profile without any filters and use it as **default**. +You can define profile without any filters and use it as **default**. Only **one or none** profile will be returned per context. @@ -69,6 +69,49 @@ Profile may generate multiple outputs from a single input. Each output must defi - it is possible to rescale output to specified resolution and keep aspect ratio. - If value is set to 0, source resolution will be used. +- **`Overscan crop`** + - Crop input resolution before rescaling. + + - Value is text may have a few variants. Each variant define output size for input size. + + - All values that cause output resolution smaller than 1 pixel are invalid. + + - Value without sign (+/-) in is always explicit and value with sign is + relative. Output size for values "200px" and "+200px" are not the same "+200px" will add 200 pixels to source and "200px" will keep only 200px from source. Value of "0", "0px" or "0%" are automatically converted to "+0px" as 0px is invalid ouput. + + - Cropped value is related to center. It is better to avoid odd numbers if + possible. + + **Example outputs for input size: 2200px** + + | String | Output | Description | + |---|---|---| + | ` ` | 2200px | Empty string keep resolution unchanged. | + | `50%` | 1100px | Crop 25% of input width on left and right side. | + | `300px` | 300px | Keep 300px in center of input and crop rest on left adn right. | + | `300` | 300px | Values without units are used as pixels (`px`). | + | `+0px` | 2200px | Keep resolution unchanged. | + | `0px` | 2200px | Same as `+0px`. | + | `+300px` | 2500px | Add black pillars of 150px width on left and right side. | + | `-300px` | 1900px | Crop 150px on left and right side | + | `+10%` | 2420px | Add black pillars of 5% size of input on left and right side. | + | `-10%` | 1980px | Crop 5% of input size by on left and right side. | + | `-10%+` | 2000px | Input width is 110% of output width. | + + **Value "-10%+" is a special case which says that input's resolution is + bigger by 10% than expected output.** + + - It is possible to enter single value for both width and height or + combination of two variants for width and height separated with space. + + **Example for resolution: 2000px 1000px** + + | String | Output | + |---------------|---------------| + | "100px 120px" | 2100px 1120px | + | "-10% -200px" | 1800px 800px | + | "-10% -0px" | 1800px 1000px | + - **`Letter Box`** - **Enabled** - Enable letter boxes - **Ratio** - Ratio of letter boxes @@ -86,7 +129,7 @@ Profile may generate multiple outputs from a single input. Each output must defi Saves information for all published subsets into DB, published assets are available for other hosts, tools and tasks after. #### Template name profiles -Allows to select [anatomy template](admin_settings_project_anatomy.md#templates) based on context of subset being published. +Allows to select [anatomy template](admin_settings_project_anatomy.md#templates) based on context of subset being published. For example for `render` profile you might want to publish and store assets in different location (based on anatomy setting) then for `publish` profile. [Profile filtering](#profile-filters) is used to select between appropriate template for each context of published subsets. @@ -96,7 +139,7 @@ Applicable context filters: - **`tasks`** - Current task. `["modeling", "animation"]` ![global_integrate_new_template_name_profile](assets/global_integrate_new_template_name_profile.png) - + (This image shows use case where `render` anatomy template is used for subsets of families ['review, 'render', 'prerender'], `publish` template is chosen for all other.) #### Subset grouping profiles @@ -111,5 +154,16 @@ Applicable context filters: - **`tasks`** - Current task. `["modeling", "animation"]` ![global_integrate_new_template_name_profile](assets/global_integrate_new_subset_group.png) - -(This image shows use case where only assets published from 'photoshop', for all families for all tasks should be marked as grouped with a capitalized name of Task where they are published from.) \ No newline at end of file + +(This image shows use case where only assets published from 'photoshop', for all families for all tasks should be marked as grouped with a capitalized name of Task where they are published from.) + +## Tools +Settings for OpenPype tools. + +## Workfiles +All settings related to Workfile tool. + +### Open last workfile at launch +This feature allows you to define a rule for each task/host or toggle the feature globally to all tasks as they are visible in the picture. + +![global_tools_workfile_open_last_version](assets/global_tools_workfile_open_last_version.png) \ No newline at end of file diff --git a/website/docs/project_settings/settings_project_nuke.md b/website/docs/project_settings/settings_project_nuke.md new file mode 100644 index 0000000000..561311317f --- /dev/null +++ b/website/docs/project_settings/settings_project_nuke.md @@ -0,0 +1,63 @@ +--- +id: settings_project_nuke +title: Project Nuke Setting +sidebar_label: Nuke +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Project settings can have project specific values. Each new project is using studio values defined in **default** project but these values can be modified or overriden per project. + +:::warning Default studio values +Projects always use default project values unless they have [project override](../admin_settings#project-overrides) (orage colour). Any changes in default project may affect all existing projects. +::: + +## Workfile Builder + +All Workfile Builder related settings can be found here. This is a list of available features: +- Create first workfile +- Custom Template path (task type filtered) +- Run Builder profiles at first run +- Define Builder Profiles With Filters + +![nuke_workfile_options_location](assets/nuke_workfile_builder_location.png) + +:::important Auto Load Last Version +In case you want to set the auto load of the latest available version of workfiles, you can do it from [here](settings_project_global#open-last-workfile-at-launch). +::: + +### Create first workfile + +By switchintg this feature on, OpenPype will generate initial workfile version. Following attributes are possible to configure: + +![nuke_workfile_options_create_first_version](assets/nuke_workfile_builder_create_first_workfile.png) + +#### Custom templates +Custom templates are added into nuke's node graph as nodes. List of task types can be defined for templates filtering. + +- Task types are sourced from project related Anatomy/Task Types + +![nuke_workfile_builder_template_task_type](assets/nuke_workfile_builder_template_task_type.png) + + - multi platform path can be used in a variety of ways. Along the absolut path to a template file also an python formating could be used. At the moment these keys are supported (image example bellow): + - `root[key]`: definitions from anatomy roots + - `project[name, code]`: project in context + - `asset`: name of asset/shot in context + - `task[type, name, short_name]`: as they are defined on asset/shot and in **Anatomy/Task Type** on project context + +![nuke_workfile_builder_template_anatomy](assets/nuke_workfile_builder_template_anatomy.png) + +#### Run Builder profiles on first launch +Enabling this feature will look into available Builder's Prorfiles (look bellow for more informations about this feature) and load available versions into node graph. + +### Profiles (Builder) +Builder profiles are set of rules allowing artist Load any available versions for the context of the asset, which it is run from. Preset is having following attributes: + +- **Filter**: Each profile could be defined with task filter. In case no filter is defined, a profile will be working for all. + +- **Context section**: filtres for subset name (regex accepted), families, representation names and available Loader plugin. + +- **Linked Assets/Shots**: filters for asset builds to be added + +![nuke_workfile_builder_profiles](assets/nuke_workfile_builder_profiles.png) diff --git a/website/sidebars.js b/website/sidebars.js index 7a5379c36b..0b831bccb3 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -64,7 +64,8 @@ module.exports = { type: "category", label: "Project Settings", items: [ - "project_settings/settings_project_global" + "project_settings/settings_project_global", + "project_settings/settings_project_nuke" ], }, ], diff --git a/website/src/pages/index.js b/website/src/pages/index.js index 81b8d77bd3..8eb6e84c24 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -50,7 +50,7 @@ const collab = [ infoLink: 'http://kredenc.studio' }, { title: 'Colorbleed', - image: '/img/colorbleed_logo.png', + image: '/img/colorbleed_logo_black.png', infoLink: 'http://colorbleed.nl' }, { title: 'Bumpybox', @@ -67,7 +67,7 @@ const collab = [ } ]; -const clients = [ +const studios = [ { title: 'Imagine Studio', image: '/img/imagine_logo.png', @@ -82,11 +82,11 @@ const clients = [ infoLink: 'https://www.3de.com.pl/' }, { title: 'Incognito', - image: '/img/client_incognito.png', + image: '/img/incognito.png', infoLink: 'https://incognito.studio/' }, { title: 'Fourth Wall Animation', - image: '/img/client_fourthwall_logo.png', + image: '/img/fourthwall_logo.png', infoLink: 'https://fourthwallanimation.com/' }, { title: 'The Scope Studio', @@ -99,22 +99,27 @@ const clients = [ }, { title: 'Filmmore', image: '/img/filmmore_logotype_bw.png', - infoLink: 'https://filmmore.nl/' + infoLink: 'https://filmmore.eu/' }, { title: 'Yowza Animation', - image: '/img/client_yowza_logo.png', + image: '/img/yowza_logo.png', infoLink: 'https://yowzaanimation.com/' }, { title: "Red Knuckles", - image: "/img/redknuckles_logotype.png", + image: "/img/redknuckles_logo.png", infoLink: "https://www.redknuckles.co.uk/", }, { title: "Orca Studios", image: "/img/orcastudios_logo.png", infoLink: "https://orcastudios.es/", + }, + { + title: "Bad Clay", + image: "/img/badClay_logo.png", + infoLink: "https://www.bad-clay.com/", } ]; @@ -343,7 +348,7 @@ function Home() { - DaVinci Resolve (Alpha) + DaVinci Resolve (Beta) @@ -387,12 +392,12 @@ function Home() { )} - {clients && clients.length && ( + {studios && studios.length && (

Studios using openPYPE

- {clients.map((props, idx) => ( + {studios.map((props, idx) => ( ))}
diff --git a/website/static/img/3bohemians-logo.png b/website/static/img/3bohemians-logo.png deleted file mode 100644 index 8910e7a4e3..0000000000 Binary files a/website/static/img/3bohemians-logo.png and /dev/null differ diff --git a/website/static/img/3bohemians-logo.svg b/website/static/img/3bohemians-logo.svg deleted file mode 100644 index e15a2668e7..0000000000 --- a/website/static/img/3bohemians-logo.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/website/static/img/BionautAnimation.png b/website/static/img/BionautAnimation.png deleted file mode 100644 index 1200444d9e..0000000000 Binary files a/website/static/img/BionautAnimation.png and /dev/null differ diff --git a/website/static/img/badClay_logo.png b/website/static/img/badClay_logo.png new file mode 100644 index 0000000000..d3385a6e33 Binary files /dev/null and b/website/static/img/badClay_logo.png differ diff --git a/website/static/img/bionaut_logo.png b/website/static/img/bionaut_logo.png deleted file mode 100644 index 1603a10468..0000000000 Binary files a/website/static/img/bionaut_logo.png and /dev/null differ diff --git a/website/static/img/bumpybox.png b/website/static/img/bumpybox.png deleted file mode 100644 index e07c605a59..0000000000 Binary files a/website/static/img/bumpybox.png and /dev/null differ diff --git a/website/static/img/client_krutart_logo.png b/website/static/img/client_krutart_logo.png deleted file mode 100644 index 77b0b580d5..0000000000 Binary files a/website/static/img/client_krutart_logo.png and /dev/null differ diff --git a/website/static/img/colorbleed_logo.png b/website/static/img/colorbleed_logo.png deleted file mode 100644 index bab83b7e8f..0000000000 Binary files a/website/static/img/colorbleed_logo.png and /dev/null differ diff --git a/website/static/img/cubicmotion.png b/website/static/img/cubicmotion.png deleted file mode 100644 index 0c496867ac..0000000000 Binary files a/website/static/img/cubicmotion.png and /dev/null differ diff --git a/website/static/img/filmmore.png b/website/static/img/filmmore.png deleted file mode 100644 index 4d05c6782b..0000000000 Binary files a/website/static/img/filmmore.png and /dev/null differ diff --git a/website/static/img/client_fourthwall_logo.png b/website/static/img/fourthwall_logo.png similarity index 100% rename from website/static/img/client_fourthwall_logo.png rename to website/static/img/fourthwall_logo.png diff --git a/website/static/img/client_incognito.png b/website/static/img/incognito.png similarity index 100% rename from website/static/img/client_incognito.png rename to website/static/img/incognito.png diff --git a/website/static/img/redknuckles_logo.png b/website/static/img/redknuckles_logo.png new file mode 100644 index 0000000000..9f852128f5 Binary files /dev/null and b/website/static/img/redknuckles_logo.png differ diff --git a/website/static/img/redknuckles_logotype.png b/website/static/img/redknuckles_logotype.png deleted file mode 100644 index 13c49c8504..0000000000 Binary files a/website/static/img/redknuckles_logotype.png and /dev/null differ diff --git a/website/static/img/client_yowza_logo.png b/website/static/img/yowza_logo.png similarity index 100% rename from website/static/img/client_yowza_logo.png rename to website/static/img/yowza_logo.png diff --git a/website/yarn.lock b/website/yarn.lock index e5dadd4e80..f1527f5b76 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -3366,9 +3366,9 @@ dns-equal@^1.0.0: integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= dns-packet@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" - integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== + version "1.3.4" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f" + integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== dependencies: ip "^1.1.0" safe-buffer "^5.0.1" @@ -8754,9 +8754,9 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== dependencies: async-limiter "~1.0.0"