mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into feature/webserver_without_interface
This commit is contained in:
commit
dc8cd3ef35
133 changed files with 7602 additions and 539 deletions
10
.github/workflows/prerelease.yml
vendored
10
.github/workflows/prerelease.yml
vendored
|
|
@ -20,12 +20,12 @@ jobs:
|
|||
python-version: 3.7
|
||||
|
||||
- name: Install Python requirements
|
||||
run: pip install gitpython semver
|
||||
run: pip install gitpython semver PyGithub
|
||||
|
||||
- name: 🔎 Determine next version type
|
||||
id: version_type
|
||||
run: |
|
||||
TYPE=$(python ./tools/ci_tools.py --bump)
|
||||
TYPE=$(python ./tools/ci_tools.py --bump --github_token ${{ secrets.GITHUB_TOKEN }})
|
||||
|
||||
echo ::set-output name=type::$TYPE
|
||||
|
||||
|
|
@ -43,11 +43,7 @@ jobs:
|
|||
uses: heinrichreimer/github-changelog-generator-action@v2.2
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
breakingLabel: '**💥 Breaking**'
|
||||
enhancementLabel: '**🚀 Enhancements**'
|
||||
bugsLabel: '**🐛 Bug fixes**'
|
||||
deprecatedLabel: '**⚠️ Deprecations**'
|
||||
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"### 🆕 New features","labels":["feature"]},}'
|
||||
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}'
|
||||
issues: false
|
||||
issuesWoLabels: false
|
||||
sinceTag: "3.0.0"
|
||||
|
|
|
|||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
|
|
@ -39,11 +39,7 @@ jobs:
|
|||
uses: heinrichreimer/github-changelog-generator-action@v2.2
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
breakingLabel: '**💥 Breaking**'
|
||||
enhancementLabel: '**🚀 Enhancements**'
|
||||
bugsLabel: '**🐛 Bug fixes**'
|
||||
deprecatedLabel: '**⚠️ Deprecations**'
|
||||
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
|
||||
addSections: '{"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]},"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]}}'
|
||||
issues: false
|
||||
issuesWoLabels: false
|
||||
sinceTag: "3.0.0"
|
||||
|
|
@ -85,11 +81,7 @@ jobs:
|
|||
uses: heinrichreimer/github-changelog-generator-action@v2.2
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
breakingLabel: '**💥 Breaking**'
|
||||
enhancementLabel: '**🚀 Enhancements**'
|
||||
bugsLabel: '**🐛 Bug fixes**'
|
||||
deprecatedLabel: '**⚠️ Deprecations**'
|
||||
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
|
||||
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}'
|
||||
issues: false
|
||||
issuesWoLabels: false
|
||||
sinceTag: ${{ steps.version.outputs.last_release }}
|
||||
|
|
|
|||
144
CHANGELOG.md
144
CHANGELOG.md
|
|
@ -1,35 +1,80 @@
|
|||
# Changelog
|
||||
|
||||
## [3.4.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.5.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...HEAD)
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.0...HEAD)
|
||||
|
||||
**Merged pull requests:**
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972)
|
||||
- Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967)
|
||||
- Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038)
|
||||
|
||||
## [3.4.0](https://github.com/pypeclub/OpenPype/tree/3.4.0) (2021-09-17)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.0-nightly.6...3.4.0)
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014)
|
||||
- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952)
|
||||
|
||||
**🆕 New features**
|
||||
|
||||
- Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041)
|
||||
- General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036)
|
||||
- Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022)
|
||||
- Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019)
|
||||
- General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017)
|
||||
- Ftrack: Sync to avalon actions have jobs [\#2015](https://github.com/pypeclub/OpenPype/pull/2015)
|
||||
- Modules: Connect method is not required [\#2009](https://github.com/pypeclub/OpenPype/pull/2009)
|
||||
- Settings UI: Number with configurable steps [\#2001](https://github.com/pypeclub/OpenPype/pull/2001)
|
||||
- Moving project folder structure creation out of ftrack module \#1989 [\#1996](https://github.com/pypeclub/OpenPype/pull/1996)
|
||||
- Configurable items for providers without Settings [\#1987](https://github.com/pypeclub/OpenPype/pull/1987)
|
||||
- Global: Example addons [\#1986](https://github.com/pypeclub/OpenPype/pull/1986)
|
||||
- Standalone Publisher: Extract harmony zip handle workfile template [\#1982](https://github.com/pypeclub/OpenPype/pull/1982)
|
||||
- Settings UI: Number sliders [\#1978](https://github.com/pypeclub/OpenPype/pull/1978)
|
||||
- Workfiles: Support more workfile templates [\#1966](https://github.com/pypeclub/OpenPype/pull/1966)
|
||||
- Launcher: Fix crashes on action click [\#1964](https://github.com/pypeclub/OpenPype/pull/1964)
|
||||
- Settings: Minor fixes in UI and missing default values [\#1963](https://github.com/pypeclub/OpenPype/pull/1963)
|
||||
- Blender: Toggle system console works on windows [\#1962](https://github.com/pypeclub/OpenPype/pull/1962)
|
||||
- Resolve path when adding to zip [\#1960](https://github.com/pypeclub/OpenPype/pull/1960)
|
||||
- Bump url-parse from 1.5.1 to 1.5.3 in /website [\#1958](https://github.com/pypeclub/OpenPype/pull/1958)
|
||||
- Global: Settings defined by Addons/Modules [\#1959](https://github.com/pypeclub/OpenPype/pull/1959)
|
||||
- CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954)
|
||||
- Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949)
|
||||
- Global: Define hosts in CollectSceneVersion [\#1948](https://github.com/pypeclub/OpenPype/pull/1948)
|
||||
- Maya: Add Xgen family support [\#1947](https://github.com/pypeclub/OpenPype/pull/1947)
|
||||
- Add face sets to exported alembics [\#1942](https://github.com/pypeclub/OpenPype/pull/1942)
|
||||
- Bump path-parse from 1.0.6 to 1.0.7 in /website [\#1933](https://github.com/pypeclub/OpenPype/pull/1933)
|
||||
- \#1894 - adds host to template\_name\_profiles for filtering [\#1915](https://github.com/pypeclub/OpenPype/pull/1915)
|
||||
- Environments: Tool environments in alphabetical order [\#1910](https://github.com/pypeclub/OpenPype/pull/1910)
|
||||
- Disregard publishing time. [\#1888](https://github.com/pypeclub/OpenPype/pull/1888)
|
||||
- Feature/webpublisher backend [\#1876](https://github.com/pypeclub/OpenPype/pull/1876)
|
||||
- Dynamic modules [\#1872](https://github.com/pypeclub/OpenPype/pull/1872)
|
||||
- Houdini: add Camera, Point Cache, Composite, Redshift ROP and VDB Cache support [\#1821](https://github.com/pypeclub/OpenPype/pull/1821)
|
||||
- OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\#1939](https://github.com/pypeclub/OpenPype/pull/1939)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Workfiles tool: Task selection [\#2040](https://github.com/pypeclub/OpenPype/pull/2040)
|
||||
- Ftrack: Delete old versions missing settings key [\#2037](https://github.com/pypeclub/OpenPype/pull/2037)
|
||||
- Nuke: typo on a button [\#2034](https://github.com/pypeclub/OpenPype/pull/2034)
|
||||
- Hiero: Fix "none" named tags [\#2033](https://github.com/pypeclub/OpenPype/pull/2033)
|
||||
- FFmpeg: Subprocess arguments as list [\#2032](https://github.com/pypeclub/OpenPype/pull/2032)
|
||||
- General: Fix Python 2 breaking line [\#2016](https://github.com/pypeclub/OpenPype/pull/2016)
|
||||
- Bugfix/webpublisher task type [\#2006](https://github.com/pypeclub/OpenPype/pull/2006)
|
||||
- Nuke thumbnails generated from middle of the sequence [\#1992](https://github.com/pypeclub/OpenPype/pull/1992)
|
||||
- Nuke: last version from path gets correct version [\#1990](https://github.com/pypeclub/OpenPype/pull/1990)
|
||||
- nuke, resolve, hiero: precollector order lest then 0.5 [\#1984](https://github.com/pypeclub/OpenPype/pull/1984)
|
||||
- Last workfile with multiple work templates [\#1981](https://github.com/pypeclub/OpenPype/pull/1981)
|
||||
- Collectors order [\#1977](https://github.com/pypeclub/OpenPype/pull/1977)
|
||||
- Stop timer was within validator order range. [\#1975](https://github.com/pypeclub/OpenPype/pull/1975)
|
||||
- Ftrack: arrow submodule has https url source [\#1974](https://github.com/pypeclub/OpenPype/pull/1974)
|
||||
- Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972)
|
||||
- Deadline: Houdini plugins in different hierarchy [\#1970](https://github.com/pypeclub/OpenPype/pull/1970)
|
||||
- Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967)
|
||||
- Global: ExtractJpeg can handle filepaths with spaces [\#1961](https://github.com/pypeclub/OpenPype/pull/1961)
|
||||
|
||||
## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1)
|
||||
|
||||
**Merged pull requests:**
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946)
|
||||
- Maya: Menu actions fix [\#1945](https://github.com/pypeclub/OpenPype/pull/1945)
|
||||
|
|
@ -40,52 +85,41 @@
|
|||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.0-nightly.11...3.3.0)
|
||||
|
||||
**Merged pull requests:**
|
||||
**🆕 New features**
|
||||
|
||||
- Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932)
|
||||
- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923)
|
||||
- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Python console interpreter [\#1940](https://github.com/pypeclub/OpenPype/pull/1940)
|
||||
- Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937)
|
||||
- Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935)
|
||||
- Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932)
|
||||
- Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930)
|
||||
- Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929)
|
||||
- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927)
|
||||
- Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926)
|
||||
- Check for missing ✨ Python when using `pyenv` [\#1925](https://github.com/pypeclub/OpenPype/pull/1925)
|
||||
- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923)
|
||||
- Fix - validate takes repre\["files"\] as list all the time [\#1922](https://github.com/pypeclub/OpenPype/pull/1922)
|
||||
- Settings: Default values for enum [\#1920](https://github.com/pypeclub/OpenPype/pull/1920)
|
||||
- Settings UI: Modifiable dict view enhance [\#1919](https://github.com/pypeclub/OpenPype/pull/1919)
|
||||
- standalone: validator asset parents [\#1917](https://github.com/pypeclub/OpenPype/pull/1917)
|
||||
- Nuke: update video file crassing [\#1916](https://github.com/pypeclub/OpenPype/pull/1916)
|
||||
- Fix - texture validators for workfiles triggers only for textures workfiles [\#1914](https://github.com/pypeclub/OpenPype/pull/1914)
|
||||
- submodules: avalon-core update [\#1911](https://github.com/pypeclub/OpenPype/pull/1911)
|
||||
- Settings UI: List order works as expected [\#1906](https://github.com/pypeclub/OpenPype/pull/1906)
|
||||
- Add support for multiple Deadline ☠️➖ servers [\#1905](https://github.com/pypeclub/OpenPype/pull/1905)
|
||||
- Hiero: loaded clip was not set colorspace from version data [\#1904](https://github.com/pypeclub/OpenPype/pull/1904)
|
||||
- Pyblish UI: Fix collecting stage processing [\#1903](https://github.com/pypeclub/OpenPype/pull/1903)
|
||||
- Burnins: Use input's bitrate in h624 [\#1902](https://github.com/pypeclub/OpenPype/pull/1902)
|
||||
- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901)
|
||||
- Ftrack: Where I run action enhancement [\#1900](https://github.com/pypeclub/OpenPype/pull/1900)
|
||||
- Ftrack: Private project server actions [\#1899](https://github.com/pypeclub/OpenPype/pull/1899)
|
||||
- Support nested studio plugins paths. [\#1898](https://github.com/pypeclub/OpenPype/pull/1898)
|
||||
- Bug: fixed python detection [\#1893](https://github.com/pypeclub/OpenPype/pull/1893)
|
||||
- Settings: global validators with options [\#1892](https://github.com/pypeclub/OpenPype/pull/1892)
|
||||
- Settings: Conditional dict enum positioning [\#1891](https://github.com/pypeclub/OpenPype/pull/1891)
|
||||
- global: integrate name missing default template [\#1890](https://github.com/pypeclub/OpenPype/pull/1890)
|
||||
- publisher: editorial plugins fixes [\#1889](https://github.com/pypeclub/OpenPype/pull/1889)
|
||||
- Expose stop timer through rest api. [\#1886](https://github.com/pypeclub/OpenPype/pull/1886)
|
||||
- TVPaint: Increment workfile [\#1885](https://github.com/pypeclub/OpenPype/pull/1885)
|
||||
- Allow Multiple Notes to run on tasks. [\#1882](https://github.com/pypeclub/OpenPype/pull/1882)
|
||||
- Normalize path returned from Workfiles. [\#1880](https://github.com/pypeclub/OpenPype/pull/1880)
|
||||
- Prepare for pyside2 [\#1869](https://github.com/pypeclub/OpenPype/pull/1869)
|
||||
- Filter hosts in settings host-enum [\#1868](https://github.com/pypeclub/OpenPype/pull/1868)
|
||||
- Local actions with process identifier [\#1867](https://github.com/pypeclub/OpenPype/pull/1867)
|
||||
- Workfile tool start at host launch support [\#1865](https://github.com/pypeclub/OpenPype/pull/1865)
|
||||
- Maya: add support for `RedshiftNormalMap` node, fix `tx` linear space 🚀 [\#1863](https://github.com/pypeclub/OpenPype/pull/1863)
|
||||
- Workfiles tool event arguments fix [\#1862](https://github.com/pypeclub/OpenPype/pull/1862)
|
||||
- Maya: support for configurable `dirmap` 🗺️ [\#1859](https://github.com/pypeclub/OpenPype/pull/1859)
|
||||
- Maya: don't add reference members as connections to the container set 📦 [\#1855](https://github.com/pypeclub/OpenPype/pull/1855)
|
||||
- Settings list can use template or schema as object type [\#1815](https://github.com/pypeclub/OpenPype/pull/1815)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935)
|
||||
- Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929)
|
||||
- Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926)
|
||||
- Fix - validate takes repre\["files"\] as list all the time [\#1922](https://github.com/pypeclub/OpenPype/pull/1922)
|
||||
- standalone: validator asset parents [\#1917](https://github.com/pypeclub/OpenPype/pull/1917)
|
||||
- Nuke: update video file crassing [\#1916](https://github.com/pypeclub/OpenPype/pull/1916)
|
||||
- Fix - texture validators for workfiles triggers only for textures workfiles [\#1914](https://github.com/pypeclub/OpenPype/pull/1914)
|
||||
- Settings UI: List order works as expected [\#1906](https://github.com/pypeclub/OpenPype/pull/1906)
|
||||
- Hiero: loaded clip was not set colorspace from version data [\#1904](https://github.com/pypeclub/OpenPype/pull/1904)
|
||||
- Pyblish UI: Fix collecting stage processing [\#1903](https://github.com/pypeclub/OpenPype/pull/1903)
|
||||
- Burnins: Use input's bitrate in h624 [\#1902](https://github.com/pypeclub/OpenPype/pull/1902)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937)
|
||||
- Add support for multiple Deadline ☠️➖ servers [\#1905](https://github.com/pypeclub/OpenPype/pull/1905)
|
||||
|
||||
## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ from .pipeline import (
|
|||
|
||||
from avalon.tools import (
|
||||
creator,
|
||||
loader,
|
||||
sceneinventory,
|
||||
)
|
||||
from openpype.tools import (
|
||||
loader,
|
||||
libraryloader
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Basic avalon integration
|
|||
"""
|
||||
import os
|
||||
|
||||
from avalon.tools import workfiles
|
||||
from openpype.tools import workfiles
|
||||
from avalon import api as avalon
|
||||
from pyblish import api as pyblish
|
||||
from openpype.api import Logger
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ class ExtractRender(pyblish.api.InstancePlugin):
|
|||
thumbnail_path = os.path.join(path, "thumbnail.png")
|
||||
ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg")
|
||||
args = [
|
||||
"{}".format(ffmpeg_path), "-y",
|
||||
ffmpeg_path,
|
||||
"-y",
|
||||
"-i", os.path.join(path, list(collections[0])[0]),
|
||||
"-vf", "scale=300:-1",
|
||||
"-vframes", "1",
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ def menu_install():
|
|||
apply_colorspace_project, apply_colorspace_clips
|
||||
)
|
||||
# here is the best place to add menu
|
||||
from avalon.tools import cbloader, creator, sceneinventory
|
||||
from avalon.tools import creator, sceneinventory
|
||||
from openpype.tools import loader
|
||||
from avalon.vendor.Qt import QtGui
|
||||
|
||||
menu_name = os.environ['AVALON_LABEL']
|
||||
|
|
@ -90,7 +91,7 @@ def menu_install():
|
|||
|
||||
loader_action = menu.addAction("Load ...")
|
||||
loader_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
|
||||
loader_action.triggered.connect(cbloader.show)
|
||||
loader_action.triggered.connect(loader.show)
|
||||
|
||||
sceneinventory_action = menu.addAction("Manage ...")
|
||||
sceneinventory_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ Basic avalon integration
|
|||
import os
|
||||
import contextlib
|
||||
from collections import OrderedDict
|
||||
from avalon.tools import (
|
||||
workfiles,
|
||||
publish as _publish
|
||||
)
|
||||
from avalon.tools import publish as _publish
|
||||
from openpype.tools import workfiles
|
||||
from avalon.pipeline import AVALON_CONTAINER_ID
|
||||
from avalon import api as avalon
|
||||
from avalon import schema
|
||||
|
|
|
|||
|
|
@ -10,16 +10,16 @@ log = Logger().get_logger(__name__)
|
|||
|
||||
def tag_data():
|
||||
return {
|
||||
"Retiming": {
|
||||
"editable": "1",
|
||||
"note": "Clip has retime or TimeWarp effects (or multiple effects stacked on the clip)", # noqa
|
||||
"icon": "retiming.png",
|
||||
"metadata": {
|
||||
"family": "retiming",
|
||||
"marginIn": 1,
|
||||
"marginOut": 1
|
||||
}
|
||||
},
|
||||
# "Retiming": {
|
||||
# "editable": "1",
|
||||
# "note": "Clip has retime or TimeWarp effects (or multiple effects stacked on the clip)", # noqa
|
||||
# "icon": "retiming.png",
|
||||
# "metadata": {
|
||||
# "family": "retiming",
|
||||
# "marginIn": 1,
|
||||
# "marginOut": 1
|
||||
# }
|
||||
# },
|
||||
"[Lenses]": {
|
||||
"Set lense here": {
|
||||
"editable": "1",
|
||||
|
|
@ -31,15 +31,15 @@ def tag_data():
|
|||
}
|
||||
}
|
||||
},
|
||||
"NukeScript": {
|
||||
"editable": "1",
|
||||
"note": "Collecting track items to Nuke scripts.",
|
||||
"icon": "icons:TagNuke.png",
|
||||
"metadata": {
|
||||
"family": "nukescript",
|
||||
"subset": "main"
|
||||
}
|
||||
},
|
||||
# "NukeScript": {
|
||||
# "editable": "1",
|
||||
# "note": "Collecting track items to Nuke scripts.",
|
||||
# "icon": "icons:TagNuke.png",
|
||||
# "metadata": {
|
||||
# "family": "nukescript",
|
||||
# "subset": "main"
|
||||
# }
|
||||
# },
|
||||
"Comment": {
|
||||
"editable": "1",
|
||||
"note": "Comment on a shot.",
|
||||
|
|
@ -78,8 +78,7 @@ def update_tag(tag, data):
|
|||
# set icon if any available in input data
|
||||
if data.get("icon"):
|
||||
tag.setIcon(str(data["icon"]))
|
||||
# set note description of tag
|
||||
tag.setNote(data["note"])
|
||||
|
||||
# get metadata of tag
|
||||
mtd = tag.metadata()
|
||||
# get metadata key from data
|
||||
|
|
@ -97,6 +96,9 @@ def update_tag(tag, data):
|
|||
"tag.{}".format(str(k)),
|
||||
str(v)
|
||||
)
|
||||
|
||||
# set note description of tag
|
||||
tag.setNote(str(data["note"]))
|
||||
return tag
|
||||
|
||||
|
||||
|
|
@ -106,6 +108,26 @@ def add_tags_to_workfile():
|
|||
"""
|
||||
from .lib import get_current_project
|
||||
|
||||
def add_tag_to_bin(root_bin, name, data):
|
||||
# for Tags to be created in root level Bin
|
||||
# at first check if any of input data tag is not already created
|
||||
done_tag = next((t for t in root_bin.items()
|
||||
if str(name) in t.name()), None)
|
||||
|
||||
if not done_tag:
|
||||
# create Tag
|
||||
tag = create_tag(name, data)
|
||||
tag.setName(str(name))
|
||||
|
||||
log.debug("__ creating tag: {}".format(tag))
|
||||
# adding Tag to Root Bin
|
||||
root_bin.addItem(tag)
|
||||
else:
|
||||
# update only non hierarchy tags
|
||||
update_tag(done_tag, data)
|
||||
done_tag.setName(str(name))
|
||||
log.debug("__ updating tag: {}".format(done_tag))
|
||||
|
||||
# get project and root bin object
|
||||
project = get_current_project()
|
||||
root_bin = project.tagsBin()
|
||||
|
|
@ -125,10 +147,8 @@ def add_tags_to_workfile():
|
|||
for task_type in tasks.keys():
|
||||
nks_pres_tags["[Tasks]"][task_type.lower()] = {
|
||||
"editable": "1",
|
||||
"note": "",
|
||||
"icon": {
|
||||
"path": "icons:TagGood.png"
|
||||
},
|
||||
"note": task_type,
|
||||
"icon": "icons:TagGood.png",
|
||||
"metadata": {
|
||||
"family": "task",
|
||||
"type": task_type
|
||||
|
|
@ -157,10 +177,10 @@ def add_tags_to_workfile():
|
|||
# check if key is not decorated with [] so it is defined as bin
|
||||
bin_find = None
|
||||
pattern = re.compile(r"\[(.*)\]")
|
||||
bin_finds = pattern.findall(_k)
|
||||
_bin_finds = pattern.findall(_k)
|
||||
# if there is available any then pop it to string
|
||||
if bin_finds:
|
||||
bin_find = bin_finds.pop()
|
||||
if _bin_finds:
|
||||
bin_find = _bin_finds.pop()
|
||||
|
||||
# if bin was found then create or update
|
||||
if bin_find:
|
||||
|
|
@ -168,7 +188,6 @@ def add_tags_to_workfile():
|
|||
# first check if in root lever is not already created bins
|
||||
bins = [b for b in root_bin.items()
|
||||
if b.name() in str(bin_find)]
|
||||
log.debug(">>> bins: {}".format(bins))
|
||||
|
||||
if bins:
|
||||
bin = bins.pop()
|
||||
|
|
@ -178,49 +197,14 @@ def add_tags_to_workfile():
|
|||
bin = hiero.core.Bin(str(bin_find))
|
||||
|
||||
# update or create tags in the bin
|
||||
for k, v in _val.items():
|
||||
tags = [t for t in bin.items()
|
||||
if str(k) in t.name()
|
||||
if len(str(k)) == len(t.name())]
|
||||
if not tags:
|
||||
# create Tag obj
|
||||
tag = create_tag(k, v)
|
||||
|
||||
# adding Tag to Bin
|
||||
bin.addItem(tag)
|
||||
else:
|
||||
update_tag(tags.pop(), v)
|
||||
for __k, __v in _val.items():
|
||||
add_tag_to_bin(bin, __k, __v)
|
||||
|
||||
# finally add the Bin object to the root level Bin
|
||||
if root_add:
|
||||
# adding Tag to Root Bin
|
||||
root_bin.addItem(bin)
|
||||
else:
|
||||
# for Tags to be created in root level Bin
|
||||
# at first check if any of input data tag is not already created
|
||||
tags = None
|
||||
tags = [t for t in root_bin.items()
|
||||
if str(_k) in t.name()]
|
||||
|
||||
if not tags:
|
||||
# create Tag
|
||||
tag = create_tag(_k, _val)
|
||||
|
||||
# adding Tag to Root Bin
|
||||
root_bin.addItem(tag)
|
||||
else:
|
||||
# update Tags if they already exists
|
||||
for _t in tags:
|
||||
# skip bin objects
|
||||
if isinstance(_t, hiero.core.Bin):
|
||||
continue
|
||||
|
||||
# check if Hierarchy in name and skip it
|
||||
# because hierarchy could be edited
|
||||
if "hierarchy" in _t.name().lower():
|
||||
continue
|
||||
|
||||
# update only non hierarchy tags
|
||||
update_tag(_t, _val)
|
||||
add_tag_to_bin(root_bin, _k, _val)
|
||||
|
||||
log.info("Default Tags were set...")
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ creator.show()
|
|||
<scriptItem id="avalon_load">
|
||||
<label>Load ...</label>
|
||||
<scriptCode><![CDATA[
|
||||
from avalon.tools import cbloader
|
||||
cbloader.show(use_context=True)
|
||||
from openpype.tools import loader
|
||||
loader.show(use_context=True)
|
||||
]]></scriptCode>
|
||||
</scriptItem>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from avalon import api as avalon
|
|||
from avalon import pipeline
|
||||
from avalon.maya import suspended_refresh
|
||||
from avalon.maya.pipeline import IS_HEADLESS
|
||||
from avalon.tools import workfiles
|
||||
from openpype.tools import workfiles
|
||||
from pyblish import api as pyblish
|
||||
from openpype.lib import any_outdated
|
||||
import openpype.hosts.maya
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ def override_toolbox_ui():
|
|||
log.warning("Could not import SceneInventory tool")
|
||||
|
||||
try:
|
||||
import avalon.tools.loader as loader
|
||||
import openpype.tools.loader as loader
|
||||
except Exception:
|
||||
log.warning("Could not import Loader tool")
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class ShaderDefinitionsEditor(QtWidgets.QWidget):
|
|||
|
||||
self.setObjectName("shaderDefinitionEditor")
|
||||
self.setWindowTitle("OpenPype shader name definition editor")
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setParent(parent)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from collections import OrderedDict
|
|||
|
||||
|
||||
from avalon import api, io, lib
|
||||
from avalon.tools import workfiles
|
||||
from openpype.tools import workfiles
|
||||
import avalon.nuke
|
||||
from avalon.nuke import lib as anlib
|
||||
from avalon.nuke import (
|
||||
|
|
@ -287,7 +287,7 @@ def script_name():
|
|||
|
||||
def add_button_write_to_read(node):
|
||||
name = "createReadNode"
|
||||
label = "Cread Read From Rendered"
|
||||
label = "Create Read From Rendered"
|
||||
value = "import write_to_read;write_to_read.write_to_read(nuke.thisNode())"
|
||||
knob = nuke.PyScript_Knob(name, label, value)
|
||||
knob.clearFlag(nuke.STARTLINE)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin):
|
|||
hosts = ["nuke", "nukeassist"]
|
||||
|
||||
# presets
|
||||
sync_workfile_version = False
|
||||
sync_workfile_version_on_families = []
|
||||
|
||||
def process(self, context):
|
||||
asset_data = io.find_one({
|
||||
|
|
@ -120,11 +120,12 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin):
|
|||
# sync workfile version
|
||||
_families_test = [family] + families
|
||||
self.log.debug("__ _families_test: `{}`".format(_families_test))
|
||||
if not next((f for f in _families_test
|
||||
if "prerender" in f),
|
||||
None) and self.sync_workfile_version:
|
||||
# get version to instance for integration
|
||||
instance.data['version'] = instance.context.data['version']
|
||||
for family_test in _families_test:
|
||||
if family_test in self.sync_workfile_version_on_families:
|
||||
self.log.debug("Syncing version with workfile for '{}'"
|
||||
.format(family_test))
|
||||
# get version to instance for integration
|
||||
instance.data['version'] = instance.context.data['version']
|
||||
|
||||
instance.data.update({
|
||||
"subset": subset,
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ class ExtractReview(openpype.api.Extractor):
|
|||
# Generate thumbnail.
|
||||
thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg")
|
||||
args = [
|
||||
"{}".format(ffmpeg_path), "-y",
|
||||
ffmpeg_path,
|
||||
"-y",
|
||||
"-i", output_image_path,
|
||||
"-vf", "scale=300:-1",
|
||||
"-vframes", "1",
|
||||
|
|
@ -78,7 +79,8 @@ class ExtractReview(openpype.api.Extractor):
|
|||
# Generate mov.
|
||||
mov_path = os.path.join(staging_dir, "review.mov")
|
||||
args = [
|
||||
ffmpeg_path, "-y",
|
||||
ffmpeg_path,
|
||||
"-y",
|
||||
"-i", output_image_path,
|
||||
"-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
|
||||
"-vframes", "1",
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ from .pipeline import (
|
|||
|
||||
from avalon.tools import (
|
||||
creator,
|
||||
loader,
|
||||
sceneinventory,
|
||||
libraryloader,
|
||||
subsetmanager
|
||||
)
|
||||
from openpype.tools import (
|
||||
loader,
|
||||
libraryloader,
|
||||
)
|
||||
|
||||
|
||||
def load_stylesheet():
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Basic avalon integration
|
|||
import os
|
||||
import contextlib
|
||||
from collections import OrderedDict
|
||||
from avalon.tools import workfiles
|
||||
from openpype.tools import workfiles
|
||||
from avalon import api as avalon
|
||||
from avalon import schema
|
||||
from avalon.pipeline import AVALON_CONTAINER_ID
|
||||
|
|
|
|||
|
|
@ -101,11 +101,14 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
|
|||
jpeg_items.append("\"{}\"".format(full_thumbnail_path))
|
||||
|
||||
subprocess_jpeg = " ".join(jpeg_items)
|
||||
subprocess_args = openpype.lib.split_command_to_list(
|
||||
subprocess_jpeg
|
||||
)
|
||||
|
||||
# run subprocess
|
||||
self.log.debug("Executing: {}".format(subprocess_jpeg))
|
||||
self.log.debug("Executing: {}".format(" ".join(subprocess_args)))
|
||||
openpype.api.run_subprocess(
|
||||
subprocess_jpeg, shell=True, logger=self.log
|
||||
subprocess_args, shell=True, logger=self.log
|
||||
)
|
||||
|
||||
# remove thumbnail key from origin repre
|
||||
|
|
|
|||
|
|
@ -59,32 +59,35 @@ class ExtractTrimVideoAudio(openpype.api.Extractor):
|
|||
if "trimming" not in fml
|
||||
]
|
||||
|
||||
args = [
|
||||
f"\"{ffmpeg_path}\"",
|
||||
ffmpeg_args = [
|
||||
ffmpeg_path,
|
||||
"-ss", str(start / fps),
|
||||
"-i", f"\"{video_file_path}\"",
|
||||
"-i", video_file_path,
|
||||
"-t", str(dur / fps)
|
||||
]
|
||||
if ext in [".mov", ".mp4"]:
|
||||
args.extend([
|
||||
ffmpeg_args.extend([
|
||||
"-crf", "18",
|
||||
"-pix_fmt", "yuv420p"])
|
||||
"-pix_fmt", "yuv420p"
|
||||
])
|
||||
elif ext in ".wav":
|
||||
args.extend([
|
||||
"-vn -acodec pcm_s16le",
|
||||
"-ar 48000 -ac 2"
|
||||
ffmpeg_args.extend([
|
||||
"-vn",
|
||||
"-acodec", "pcm_s16le",
|
||||
"-ar", "48000",
|
||||
"-ac", "2"
|
||||
])
|
||||
|
||||
# add output path
|
||||
args.append(f"\"{clip_trimed_path}\"")
|
||||
ffmpeg_args.append(clip_trimed_path)
|
||||
|
||||
self.log.info(f"Processing: {args}")
|
||||
ffmpeg_args = " ".join(args)
|
||||
joined_args = " ".join(ffmpeg_args)
|
||||
self.log.info(f"Processing: {joined_args}")
|
||||
openpype.api.run_subprocess(
|
||||
ffmpeg_args, shell=True, logger=self.log
|
||||
)
|
||||
|
||||
repr = {
|
||||
repre = {
|
||||
"name": ext[1:],
|
||||
"ext": ext[1:],
|
||||
"files": os.path.basename(clip_trimed_path),
|
||||
|
|
@ -97,10 +100,10 @@ class ExtractTrimVideoAudio(openpype.api.Extractor):
|
|||
}
|
||||
|
||||
if ext in [".mov", ".mp4"]:
|
||||
repr.update({
|
||||
repre.update({
|
||||
"thumbnail": True,
|
||||
"tags": ["review", "ftrackreview", "delete"]})
|
||||
|
||||
instance.data["representations"].append(repr)
|
||||
instance.data["representations"].append(repre)
|
||||
|
||||
self.log.debug(f"Instance data: {pformat(instance.data)}")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ Provides:
|
|||
import os
|
||||
import json
|
||||
import clique
|
||||
import tempfile
|
||||
|
||||
import pyblish.api
|
||||
from avalon import io
|
||||
|
|
@ -94,7 +95,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
|
|||
instance.data["families"] = families
|
||||
instance.data["version"] = \
|
||||
self._get_last_version(asset, subset) + 1
|
||||
instance.data["stagingDir"] = task_dir
|
||||
instance.data["stagingDir"] = tempfile.mkdtemp()
|
||||
instance.data["source"] = "webpublisher"
|
||||
|
||||
# to store logging info into DB openpype.webpublishes
|
||||
|
|
@ -113,6 +114,8 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
|
|||
instance.data["frameEnd"] = \
|
||||
instance.data["representations"][0]["frameEnd"]
|
||||
else:
|
||||
instance.data["frameStart"] = 0
|
||||
instance.data["frameEnd"] = 1
|
||||
instance.data["representations"] = self._get_single_repre(
|
||||
task_dir, task_data["files"], tags
|
||||
)
|
||||
|
|
@ -174,7 +177,11 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
|
|||
(family, [families], subset_template_name, tags) tuple
|
||||
AssertionError if not matching family found
|
||||
"""
|
||||
task_obj = settings.get(task_type)
|
||||
task_type = task_type.lower()
|
||||
lower_cased_task_types = {}
|
||||
for t_type, task in settings.items():
|
||||
lower_cased_task_types[t_type.lower()] = task
|
||||
task_obj = lower_cased_task_types.get(task_type)
|
||||
assert task_obj, "No family configuration for '{}'".format(task_type)
|
||||
|
||||
found_family = None
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ from .execute import (
|
|||
get_pype_execute_args,
|
||||
execute,
|
||||
run_subprocess,
|
||||
split_command_to_list,
|
||||
path_to_subprocess_arg,
|
||||
CREATE_NO_WINDOW
|
||||
)
|
||||
from .log import PypeLogger, timeit
|
||||
|
|
@ -59,6 +61,11 @@ from .python_module_tools import (
|
|||
import_module_from_dirpath
|
||||
)
|
||||
|
||||
from .profiles_filtering import (
|
||||
compile_list_of_regexes,
|
||||
filter_profiles
|
||||
)
|
||||
|
||||
from .avalon_context import (
|
||||
CURRENT_DOC_SCHEMAS,
|
||||
PROJECT_NAME_ALLOWED_SYMBOLS,
|
||||
|
|
@ -118,13 +125,9 @@ from .applications import (
|
|||
prepare_host_environments,
|
||||
prepare_context_environments,
|
||||
get_app_environments_for_context,
|
||||
apply_project_environments_value,
|
||||
|
||||
compile_list_of_regexes
|
||||
apply_project_environments_value
|
||||
)
|
||||
|
||||
from .profiles_filtering import filter_profiles
|
||||
|
||||
from .plugin_tools import (
|
||||
TaskNotSetError,
|
||||
get_subset_name,
|
||||
|
|
@ -171,6 +174,9 @@ __all__ = [
|
|||
"get_pype_execute_args",
|
||||
"execute",
|
||||
"run_subprocess",
|
||||
"split_command_to_list",
|
||||
"path_to_subprocess_arg",
|
||||
"CREATE_NO_WINDOW",
|
||||
|
||||
"env_value_to_bool",
|
||||
"get_paths_from_environ",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from . import (
|
|||
PypeLogger,
|
||||
Anatomy
|
||||
)
|
||||
from .profiles_filtering import filter_profiles
|
||||
from .local_settings import get_openpype_username
|
||||
from .avalon_context import (
|
||||
get_workdir_data,
|
||||
|
|
@ -1244,6 +1245,9 @@ def prepare_context_environments(data):
|
|||
asset_tasks = asset_doc.get("data", {}).get("tasks") or {}
|
||||
task_info = asset_tasks.get(task_name) or {}
|
||||
task_type = task_info.get("type")
|
||||
# Temp solution how to pass task type to `_prepare_last_workfile`
|
||||
data["task_type"] = task_type
|
||||
|
||||
workfile_template_key = get_workfile_template_key(
|
||||
task_type,
|
||||
app.host_name,
|
||||
|
|
@ -1320,13 +1324,14 @@ def _prepare_last_workfile(data, workdir, workfile_template_key):
|
|||
workdir_data = copy.deepcopy(_workdir_data)
|
||||
project_name = data["project_name"]
|
||||
task_name = data["task_name"]
|
||||
task_type = data["task_type"]
|
||||
start_last_workfile = should_start_last_workfile(
|
||||
project_name, app.host_name, task_name
|
||||
project_name, app.host_name, task_name, task_type
|
||||
)
|
||||
data["start_last_workfile"] = start_last_workfile
|
||||
|
||||
workfile_startup = should_workfile_tool_start(
|
||||
project_name, app.host_name, task_name
|
||||
project_name, app.host_name, task_name, task_type
|
||||
)
|
||||
data["workfile_startup"] = workfile_startup
|
||||
|
||||
|
|
@ -1375,54 +1380,8 @@ def _prepare_last_workfile(data, workdir, workfile_template_key):
|
|||
data["last_workfile_path"] = last_workfile_path
|
||||
|
||||
|
||||
def get_option_from_settings(
|
||||
startup_presets, host_name, task_name, default_output
|
||||
):
|
||||
host_name_lowered = host_name.lower()
|
||||
task_name_lowered = task_name.lower()
|
||||
|
||||
max_points = 2
|
||||
matching_points = -1
|
||||
matching_item = None
|
||||
for item in startup_presets:
|
||||
hosts = item.get("hosts") or tuple()
|
||||
tasks = item.get("tasks") or tuple()
|
||||
|
||||
hosts_lowered = tuple(_host_name.lower() for _host_name in hosts)
|
||||
# Skip item if has set hosts and current host is not in
|
||||
if hosts_lowered and host_name_lowered not in hosts_lowered:
|
||||
continue
|
||||
|
||||
tasks_lowered = tuple(_task_name.lower() for _task_name in tasks)
|
||||
# Skip item if has set tasks and current task is not in
|
||||
if tasks_lowered:
|
||||
task_match = False
|
||||
for task_regex in compile_list_of_regexes(tasks_lowered):
|
||||
if re.match(task_regex, task_name_lowered):
|
||||
task_match = True
|
||||
break
|
||||
|
||||
if not task_match:
|
||||
continue
|
||||
|
||||
points = int(bool(hosts_lowered)) + int(bool(tasks_lowered))
|
||||
if points > matching_points:
|
||||
matching_item = item
|
||||
matching_points = points
|
||||
|
||||
if matching_points == max_points:
|
||||
break
|
||||
|
||||
if matching_item is not None:
|
||||
output = matching_item.get("enabled")
|
||||
if output is None:
|
||||
output = default_output
|
||||
return output
|
||||
return default_output
|
||||
|
||||
|
||||
def should_start_last_workfile(
|
||||
project_name, host_name, task_name, default_output=False
|
||||
project_name, host_name, task_name, task_type, default_output=False
|
||||
):
|
||||
"""Define if host should start last version workfile if possible.
|
||||
|
||||
|
|
@ -1444,7 +1403,7 @@ def should_start_last_workfile(
|
|||
"""
|
||||
|
||||
project_settings = get_project_settings(project_name)
|
||||
startup_presets = (
|
||||
profiles = (
|
||||
project_settings
|
||||
["global"]
|
||||
["tools"]
|
||||
|
|
@ -1452,15 +1411,27 @@ def should_start_last_workfile(
|
|||
["last_workfile_on_startup"]
|
||||
)
|
||||
|
||||
if not startup_presets:
|
||||
if not profiles:
|
||||
return default_output
|
||||
|
||||
return get_option_from_settings(
|
||||
startup_presets, host_name, task_name, default_output)
|
||||
filter_data = {
|
||||
"tasks": task_name,
|
||||
"task_types": task_type,
|
||||
"hosts": host_name
|
||||
}
|
||||
matching_item = filter_profiles(profiles, filter_data)
|
||||
|
||||
output = None
|
||||
if matching_item:
|
||||
output = matching_item.get("enabled")
|
||||
|
||||
if output is None:
|
||||
return default_output
|
||||
return output
|
||||
|
||||
|
||||
def should_workfile_tool_start(
|
||||
project_name, host_name, task_name, default_output=False
|
||||
project_name, host_name, task_name, task_type, default_output=False
|
||||
):
|
||||
"""Define if host should start workfile tool at host launch.
|
||||
|
||||
|
|
@ -1482,7 +1453,7 @@ def should_workfile_tool_start(
|
|||
"""
|
||||
|
||||
project_settings = get_project_settings(project_name)
|
||||
startup_presets = (
|
||||
profiles = (
|
||||
project_settings
|
||||
["global"]
|
||||
["tools"]
|
||||
|
|
@ -1490,27 +1461,20 @@ def should_workfile_tool_start(
|
|||
["open_workfile_tool_on_startup"]
|
||||
)
|
||||
|
||||
if not startup_presets:
|
||||
if not profiles:
|
||||
return default_output
|
||||
|
||||
return get_option_from_settings(
|
||||
startup_presets, host_name, task_name, default_output)
|
||||
filter_data = {
|
||||
"tasks": task_name,
|
||||
"task_types": task_type,
|
||||
"hosts": host_name
|
||||
}
|
||||
matching_item = filter_profiles(profiles, filter_data)
|
||||
|
||||
output = None
|
||||
if matching_item:
|
||||
output = matching_item.get("enabled")
|
||||
|
||||
def compile_list_of_regexes(in_list):
|
||||
"""Convert strings in entered list to compiled regex objects."""
|
||||
regexes = list()
|
||||
if not in_list:
|
||||
return regexes
|
||||
|
||||
for item in in_list:
|
||||
if not item:
|
||||
continue
|
||||
try:
|
||||
regexes.append(re.compile(item))
|
||||
except TypeError:
|
||||
print((
|
||||
"Invalid type \"{}\" value \"{}\"."
|
||||
" Expected string based object. Skipping."
|
||||
).format(str(type(item)), str(item)))
|
||||
return regexes
|
||||
if output is None:
|
||||
return default_output
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import functools
|
|||
|
||||
from openpype.settings import get_project_settings
|
||||
from .anatomy import Anatomy
|
||||
from .profiles_filtering import filter_profiles
|
||||
|
||||
# avalon module is not imported at the top
|
||||
# - may not be in path at the time of pype.lib initialization
|
||||
|
|
@ -453,8 +454,6 @@ def get_workfile_template_key(
|
|||
if not profiles:
|
||||
return default
|
||||
|
||||
from .profiles_filtering import filter_profiles
|
||||
|
||||
profile_filter = {
|
||||
"task_types": task_type,
|
||||
"hosts": host_name
|
||||
|
|
@ -791,7 +790,9 @@ class BuildWorkfile:
|
|||
current_task_name = avalon.io.Session["AVALON_TASK"]
|
||||
|
||||
# Load workfile presets for task
|
||||
self.build_presets = self.get_build_presets(current_task_name)
|
||||
self.build_presets = self.get_build_presets(
|
||||
current_task_name, current_asset_entity
|
||||
)
|
||||
|
||||
# Skip if there are any presets for task
|
||||
if not self.build_presets:
|
||||
|
|
@ -875,7 +876,7 @@ class BuildWorkfile:
|
|||
return loaded_containers
|
||||
|
||||
@with_avalon
|
||||
def get_build_presets(self, task_name):
|
||||
def get_build_presets(self, task_name, asset_doc):
|
||||
""" Returns presets to build workfile for task name.
|
||||
|
||||
Presets are loaded for current project set in
|
||||
|
|
@ -889,30 +890,33 @@ class BuildWorkfile:
|
|||
(dict): preset per entered task name
|
||||
"""
|
||||
host_name = avalon.api.registered_host().__name__.rsplit(".", 1)[-1]
|
||||
presets = get_project_settings(avalon.io.Session["AVALON_PROJECT"])
|
||||
project_settings = get_project_settings(
|
||||
avalon.io.Session["AVALON_PROJECT"]
|
||||
)
|
||||
|
||||
host_settings = project_settings.get(host_name) or {}
|
||||
# Get presets for host
|
||||
wb_settings = presets.get(host_name, {}).get("workfile_builder")
|
||||
|
||||
wb_settings = host_settings.get("workfile_builder")
|
||||
if not wb_settings:
|
||||
# backward compatibility
|
||||
wb_settings = presets.get(host_name, {}).get("workfile_build")
|
||||
wb_settings = host_settings.get("workfile_build") or {}
|
||||
|
||||
builder_presets = wb_settings.get("profiles")
|
||||
builder_profiles = wb_settings.get("profiles")
|
||||
if not builder_profiles:
|
||||
return None
|
||||
|
||||
if not builder_presets:
|
||||
return
|
||||
|
||||
task_name_low = task_name.lower()
|
||||
per_task_preset = None
|
||||
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:
|
||||
per_task_preset = preset
|
||||
break
|
||||
|
||||
return per_task_preset
|
||||
task_type = (
|
||||
asset_doc
|
||||
.get("data", {})
|
||||
.get("tasks", {})
|
||||
.get(task_name, {})
|
||||
.get("type")
|
||||
)
|
||||
filter_data = {
|
||||
"task_types": task_type,
|
||||
"tasks": task_name
|
||||
}
|
||||
return filter_profiles(builder_profiles, filter_data)
|
||||
|
||||
def _filter_build_profiles(self, build_profiles, loaders_by_name):
|
||||
""" Filter build profiles by loaders and prepare process data.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import logging
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import platform
|
||||
|
||||
from .log import PypeLogger as Logger
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# MSDN process creation flag (Windows only)
|
||||
CREATE_NO_WINDOW = 0x08000000
|
||||
|
||||
|
|
@ -100,7 +99,9 @@ def run_subprocess(*args, **kwargs):
|
|||
filtered_env = {str(k): str(v) for k, v in env.items()}
|
||||
|
||||
# Use lib's logger if was not passed with kwargs.
|
||||
logger = kwargs.pop("logger", log)
|
||||
logger = kwargs.pop("logger", None)
|
||||
if logger is None:
|
||||
logger = Logger.get_logger("run_subprocess")
|
||||
|
||||
# set overrides
|
||||
kwargs['stdout'] = kwargs.get('stdout', subprocess.PIPE)
|
||||
|
|
@ -138,6 +139,44 @@ def run_subprocess(*args, **kwargs):
|
|||
return full_output
|
||||
|
||||
|
||||
def path_to_subprocess_arg(path):
|
||||
"""Prepare path for subprocess arguments.
|
||||
|
||||
Returned path can be wrapped with quotes or kept as is.
|
||||
"""
|
||||
return subprocess.list2cmdline([path])
|
||||
|
||||
|
||||
def split_command_to_list(string_command):
|
||||
"""Split string subprocess command to list.
|
||||
|
||||
Should be able to split complex subprocess command to separated arguments:
|
||||
`"C:\\ffmpeg folder\\ffmpeg.exe" -i \"D:\\input.mp4\\" \"D:\\output.mp4\"`
|
||||
|
||||
Should result into list:
|
||||
`["C:\ffmpeg folder\ffmpeg.exe", "-i", "D:\input.mp4", "D:\output.mp4"]`
|
||||
|
||||
This may be required on few versions of python where subprocess can handle
|
||||
only list of arguments.
|
||||
|
||||
To be able do that is using `shlex` python module.
|
||||
|
||||
Args:
|
||||
string_command(str): Full subprocess command.
|
||||
|
||||
Returns:
|
||||
list: Command separated into individual arguments.
|
||||
"""
|
||||
if not string_command:
|
||||
return []
|
||||
|
||||
kwargs = {}
|
||||
# Use 'posix' argument only on windows
|
||||
if platform.system().lower() == "windows":
|
||||
kwargs["posix"] = False
|
||||
return shlex.split(string_command, **kwargs)
|
||||
|
||||
|
||||
def get_pype_execute_args(*args):
|
||||
"""Arguments to run pype command.
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ def get_subset_name(
|
|||
project_name=None,
|
||||
host_name=None,
|
||||
default_template=None,
|
||||
dynamic_data=None
|
||||
dynamic_data=None,
|
||||
dbcon=None
|
||||
):
|
||||
if not family:
|
||||
return ""
|
||||
|
|
@ -46,13 +47,42 @@ def get_subset_name(
|
|||
# Use only last part of class family value split by dot (`.`)
|
||||
family = family.rsplit(".", 1)[-1]
|
||||
|
||||
if project_name is None:
|
||||
import avalon.api
|
||||
|
||||
project_name = avalon.api.Session["AVALON_PROJECT"]
|
||||
|
||||
# Function should expect asset document instead of asset id
|
||||
# - that way `dbcon` is not needed
|
||||
if dbcon is None:
|
||||
from avalon.api import AvalonMongoDB
|
||||
|
||||
dbcon = AvalonMongoDB()
|
||||
dbcon.Session["AVALON_PROJECT"] = project_name
|
||||
|
||||
dbcon.install()
|
||||
|
||||
asset_doc = dbcon.find_one(
|
||||
{
|
||||
"type": "asset",
|
||||
"_id": asset_id
|
||||
},
|
||||
{
|
||||
"data.tasks": True
|
||||
}
|
||||
)
|
||||
asset_tasks = asset_doc.get("data", {}).get("tasks") or {}
|
||||
task_info = asset_tasks.get(task_name) or {}
|
||||
task_type = task_info.get("type")
|
||||
|
||||
# Get settings
|
||||
tools_settings = get_project_settings(project_name)["global"]["tools"]
|
||||
profiles = tools_settings["creator"]["subset_name_profiles"]
|
||||
filtering_criteria = {
|
||||
"families": family,
|
||||
"hosts": host_name,
|
||||
"tasks": task_name
|
||||
"tasks": task_name,
|
||||
"task_types": task_type
|
||||
}
|
||||
|
||||
matching_profile = filter_profiles(profiles, filtering_criteria)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,28 @@
|
|||
import re
|
||||
import logging
|
||||
from .applications import compile_list_of_regexes
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def compile_list_of_regexes(in_list):
|
||||
"""Convert strings in entered list to compiled regex objects."""
|
||||
regexes = list()
|
||||
if not in_list:
|
||||
return regexes
|
||||
|
||||
for item in in_list:
|
||||
if not item:
|
||||
continue
|
||||
try:
|
||||
regexes.append(re.compile(item))
|
||||
except TypeError:
|
||||
print((
|
||||
"Invalid type \"{}\" value \"{}\"."
|
||||
" Expected string based object. Skipping."
|
||||
).format(str(type(item)), str(item)))
|
||||
return regexes
|
||||
|
||||
|
||||
def _profile_exclusion(matching_profiles, logger):
|
||||
"""Find out most matching profile byt host, task and family match.
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,17 @@ def is_running_from_build():
|
|||
return True
|
||||
|
||||
|
||||
def is_running_staging():
|
||||
"""Currently used OpenPype is staging version.
|
||||
|
||||
Returns:
|
||||
bool: True if openpype version containt 'staging'.
|
||||
"""
|
||||
if "staging" in get_openpype_version():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_pype_info():
|
||||
"""Information about currently used Pype process."""
|
||||
executable_args = get_pype_execute_args()
|
||||
|
|
|
|||
|
|
@ -417,7 +417,6 @@ class OpenPypeModule:
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def connect_with_modules(self, enabled_modules):
|
||||
"""Connect with other enabled modules."""
|
||||
pass
|
||||
|
|
@ -438,10 +437,6 @@ class OpenPypeAddOn(OpenPypeModule):
|
|||
"""Initialization is not be required for most of addons."""
|
||||
pass
|
||||
|
||||
def connect_with_modules(self, enabled_modules):
|
||||
"""Do not require to implement connection with modules for addon."""
|
||||
pass
|
||||
|
||||
|
||||
class ModulesManager:
|
||||
"""Manager of Pype modules helps to load and prepare them to work.
|
||||
|
|
|
|||
|
|
@ -52,12 +52,12 @@ class AvalonModule(OpenPypeModule, ITrayModule):
|
|||
def tray_init(self):
|
||||
# Add library tool
|
||||
try:
|
||||
from avalon.tools.libraryloader import app
|
||||
from avalon import style
|
||||
from Qt import QtGui
|
||||
from avalon import style
|
||||
from openpype.tools.libraryloader import LibraryLoaderWindow
|
||||
|
||||
self.libraryloader = app.Window(
|
||||
icon=QtGui.QIcon(resources.pype_icon_filepath()),
|
||||
self.libraryloader = LibraryLoaderWindow(
|
||||
icon=QtGui.QIcon(resources.get_openpype_icon_filepath()),
|
||||
show_projects=True,
|
||||
show_libraries=True
|
||||
)
|
||||
|
|
@ -68,9 +68,6 @@ class AvalonModule(OpenPypeModule, ITrayModule):
|
|||
exc_info=True
|
||||
)
|
||||
|
||||
def connect_with_modules(self, _enabled_modules):
|
||||
return
|
||||
|
||||
# Definition of Tray menu
|
||||
def tray_menu(self, tray_menu):
|
||||
from Qt import QtWidgets
|
||||
|
|
|
|||
|
|
@ -94,9 +94,6 @@ class ClockifyModule(
|
|||
"server": [CLOCKIFY_FTRACK_SERVER_PATH]
|
||||
}
|
||||
|
||||
def connect_with_modules(self, *_a, **_kw):
|
||||
return
|
||||
|
||||
def clockify_timer_stopped(self):
|
||||
self.bool_timer_run = False
|
||||
# Call `ITimersManager` method
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class MessageWidget(QtWidgets.QWidget):
|
|||
super(MessageWidget, self).__init__()
|
||||
|
||||
# Icon
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(
|
||||
|
|
@ -90,7 +90,7 @@ class ClockifySettings(QtWidgets.QWidget):
|
|||
self.validated = False
|
||||
|
||||
# Icon
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowTitle("Clockify settings")
|
||||
|
|
|
|||
|
|
@ -26,9 +26,6 @@ class DeadlineModule(OpenPypeModule, IPluginPaths):
|
|||
"not specified. Disabling module."))
|
||||
return
|
||||
|
||||
def connect_with_modules(self, *_a, **_kw):
|
||||
return
|
||||
|
||||
def get_plugin_paths(self):
|
||||
"""Deadline plugin paths."""
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ class DeleteOldVersions(BaseAction):
|
|||
)
|
||||
icon = statics_icon("ftrack", "action_icons", "OpenPypeAdmin.svg")
|
||||
|
||||
settings_key = "delete_old_versions"
|
||||
|
||||
dbcon = AvalonMongoDB()
|
||||
|
||||
inteface_title = "Choose your preferences"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class CredentialsDialog(QtWidgets.QDialog):
|
|||
self._is_logged = False
|
||||
self._in_advance_mode = False
|
||||
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(
|
||||
|
|
|
|||
|
|
@ -40,10 +40,6 @@ class LogViewModule(OpenPypeModule, ITrayModule):
|
|||
def tray_exit(self):
|
||||
return
|
||||
|
||||
def connect_with_modules(self, _enabled_modules):
|
||||
"""Nothing special."""
|
||||
return
|
||||
|
||||
def _show_logs_gui(self):
|
||||
if self.window:
|
||||
self.window.show()
|
||||
|
|
|
|||
|
|
@ -51,9 +51,6 @@ class MusterModule(OpenPypeModule, ITrayModule):
|
|||
"""Nothing special for Muster."""
|
||||
return
|
||||
|
||||
def connect_with_modules(self, *_a, **_kw):
|
||||
return
|
||||
|
||||
# Definition of Tray menu
|
||||
def tray_menu(self, parent):
|
||||
"""Add **change credentials** option to tray menu."""
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class MusterLogin(QtWidgets.QWidget):
|
|||
self.module = module
|
||||
|
||||
# Icon
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@ class ProjectManagerAction(OpenPypeModule, ITrayAction):
|
|||
# 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()
|
||||
|
|
|
|||
|
|
@ -18,9 +18,6 @@ class PythonInterpreterAction(OpenPypeModule, ITrayAction):
|
|||
if self._interpreter_window is not None:
|
||||
self._interpreter_window.save_registry()
|
||||
|
||||
def connect_with_modules(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def create_interpreter_window(self):
|
||||
"""Initializa Settings Qt window."""
|
||||
if self._interpreter_window:
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@ class PythonInterpreterWidget(QtWidgets.QWidget):
|
|||
super(PythonInterpreterWidget, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("OpenPype Console")
|
||||
self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath()))
|
||||
self.setWindowIcon(QtGui.QIcon(resources.get_openpype_icon_filepath()))
|
||||
|
||||
self.ansi_escape = re.compile(
|
||||
r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]"
|
||||
|
|
@ -387,8 +387,6 @@ class PythonInterpreterWidget(QtWidgets.QWidget):
|
|||
|
||||
self.setStyleSheet(load_stylesheet())
|
||||
|
||||
self.resize(self.default_width, self.default_height)
|
||||
|
||||
self._init_from_registry()
|
||||
|
||||
if self._tab_widget.count() < 1:
|
||||
|
|
@ -396,16 +394,23 @@ class PythonInterpreterWidget(QtWidgets.QWidget):
|
|||
|
||||
def _init_from_registry(self):
|
||||
setting_registry = PythonInterpreterRegistry()
|
||||
|
||||
width = None
|
||||
height = None
|
||||
try:
|
||||
width = setting_registry.get_item("width")
|
||||
height = setting_registry.get_item("height")
|
||||
if width is not None and height is not None:
|
||||
self.resize(width, height)
|
||||
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if width is None or width < 200:
|
||||
width = self.default_width
|
||||
|
||||
if height is None or height < 200:
|
||||
height = self.default_height
|
||||
|
||||
self.resize(width, height)
|
||||
|
||||
try:
|
||||
sizes = setting_registry.get_item("splitter_sizes")
|
||||
if len(sizes) == len(self._widgets_splitter.sizes()):
|
||||
|
|
|
|||
|
|
@ -19,9 +19,6 @@ class SettingsAction(OpenPypeModule, ITrayAction):
|
|||
# Tray attributes
|
||||
self.settings_window = None
|
||||
|
||||
def connect_with_modules(self, *_a, **_kw):
|
||||
return
|
||||
|
||||
def tray_init(self):
|
||||
"""Initialization in tray implementation of ITrayAction."""
|
||||
self.create_settings_window()
|
||||
|
|
@ -84,9 +81,6 @@ class LocalSettingsAction(OpenPypeModule, ITrayAction):
|
|||
self.settings_window = None
|
||||
self._first_trigger = True
|
||||
|
||||
def connect_with_modules(self, *_a, **_kw):
|
||||
return
|
||||
|
||||
def tray_init(self):
|
||||
"""Initialization in tray implementation of ITrayAction."""
|
||||
self.create_settings_window()
|
||||
|
|
|
|||
|
|
@ -17,10 +17,6 @@ class SlackIntegrationModule(OpenPypeModule, IPluginPaths, ILaunchHookPaths):
|
|||
slack_settings = modules_settings[self.name]
|
||||
self.enabled = slack_settings["enabled"]
|
||||
|
||||
def connect_with_modules(self, _enabled_modules):
|
||||
"""Nothing special."""
|
||||
return
|
||||
|
||||
def get_launch_hook_paths(self):
|
||||
"""Implementation of `ILaunchHookPaths`."""
|
||||
return os.path.join(SLACK_MODULE_DIR, "launch_hooks")
|
||||
|
|
|
|||
|
|
@ -680,9 +680,6 @@ class SyncServerModule(OpenPypeModule, ITrayModule):
|
|||
|
||||
return sites
|
||||
|
||||
def connect_with_modules(self, *_a, **kw):
|
||||
return
|
||||
|
||||
def tray_init(self):
|
||||
"""
|
||||
Actual initialization of Sync Server.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class SyncServerWindow(QtWidgets.QDialog):
|
|||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath()))
|
||||
self.setWindowIcon(QtGui.QIcon(resources.get_openpype_icon_filepath()))
|
||||
self.resize(1450, 700)
|
||||
|
||||
self.timer = QtCore.QTimer()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from bson.objectid import ObjectId
|
|||
from Qt import QtCore
|
||||
from Qt.QtCore import Qt
|
||||
|
||||
from avalon.tools.delegates import pretty_timestamp
|
||||
from openpype.tools.utils.delegates import pretty_timestamp
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
from openpype.lib import PypeLogger
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from openpype.tools.settings import (
|
|||
from openpype.api import get_local_site_id
|
||||
from openpype.lib import PypeLogger
|
||||
|
||||
from avalon.tools.delegates import pretty_timestamp
|
||||
from openpype.tools.utils.delegates import pretty_timestamp
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
from .models import (
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class WidgetUserIdle(QtWidgets.QWidget):
|
|||
|
||||
self.module = module
|
||||
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowCloseButtonHint
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
self._set_representations(contexts)
|
||||
|
||||
self.setWindowTitle("OpenPype - Deliver versions")
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
import openpype.lib
|
||||
from openpype.lib import should_decompress, \
|
||||
get_decompress_dir, decompress
|
||||
from openpype.lib import (
|
||||
get_ffmpeg_tool_path,
|
||||
|
||||
run_subprocess,
|
||||
split_command_to_list,
|
||||
path_to_subprocess_arg,
|
||||
|
||||
should_decompress,
|
||||
get_decompress_dir,
|
||||
decompress
|
||||
)
|
||||
import shutil
|
||||
|
||||
|
||||
|
|
@ -85,17 +92,19 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin):
|
|||
|
||||
self.log.info("output {}".format(full_output_path))
|
||||
|
||||
ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg")
|
||||
ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
|
||||
ffmpeg_args = self.ffmpeg_args or {}
|
||||
|
||||
jpeg_items = []
|
||||
jpeg_items.append("\"{}\"".format(ffmpeg_path))
|
||||
jpeg_items.append(path_to_subprocess_arg(ffmpeg_path))
|
||||
# override file if already exists
|
||||
jpeg_items.append("-y")
|
||||
# use same input args like with mov
|
||||
jpeg_items.extend(ffmpeg_args.get("input") or [])
|
||||
# input file
|
||||
jpeg_items.append("-i \"{}\"".format(full_input_path))
|
||||
jpeg_items.append("-i {}".format(
|
||||
path_to_subprocess_arg(full_input_path)
|
||||
))
|
||||
# output arguments from presets
|
||||
jpeg_items.extend(ffmpeg_args.get("output") or [])
|
||||
|
||||
|
|
@ -104,15 +113,16 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin):
|
|||
jpeg_items.append("-vframes 1")
|
||||
|
||||
# output file
|
||||
jpeg_items.append("\"{}\"".format(full_output_path))
|
||||
jpeg_items.append(path_to_subprocess_arg(full_output_path))
|
||||
|
||||
subprocess_jpeg = " ".join(jpeg_items)
|
||||
subprocess_command = " ".join(jpeg_items)
|
||||
subprocess_args = split_command_to_list(subprocess_command)
|
||||
|
||||
# run subprocess
|
||||
self.log.debug("{}".format(subprocess_jpeg))
|
||||
self.log.debug("{}".format(subprocess_command))
|
||||
try: # temporary until oiiotool is supported cross platform
|
||||
openpype.api.run_subprocess(
|
||||
subprocess_jpeg, shell=True, logger=self.log
|
||||
run_subprocess(
|
||||
subprocess_args, shell=True, logger=self.log
|
||||
)
|
||||
except RuntimeError as exp:
|
||||
if "Compression" in str(exp):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ import os
|
|||
import pyblish
|
||||
import openpype.api
|
||||
from openpype.lib import (
|
||||
get_ffmpeg_tool_path
|
||||
get_ffmpeg_tool_path,
|
||||
split_command_to_list,
|
||||
path_to_subprocess_arg
|
||||
)
|
||||
import tempfile
|
||||
import opentimelineio as otio
|
||||
|
|
@ -56,14 +58,17 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
|||
audio_inputs.insert(0, empty)
|
||||
|
||||
# create cmd
|
||||
cmd = '"{}"'.format(self.ffmpeg_path) + " "
|
||||
cmd = path_to_subprocess_arg(self.ffmpeg_path) + " "
|
||||
cmd += self.create_cmd(audio_inputs)
|
||||
cmd += "\"{}\"".format(audio_temp_fpath)
|
||||
cmd += path_to_subprocess_arg(audio_temp_fpath)
|
||||
|
||||
# Split command to list for subprocess
|
||||
cmd_list = split_command_to_list(cmd)
|
||||
|
||||
# run subprocess
|
||||
self.log.debug("Executing: {}".format(cmd))
|
||||
openpype.api.run_subprocess(
|
||||
cmd, logger=self.log
|
||||
cmd_list, logger=self.log
|
||||
)
|
||||
|
||||
# remove empty
|
||||
|
|
@ -99,16 +104,16 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
|||
# temp audio file
|
||||
audio_fpath = self.create_temp_file(name)
|
||||
|
||||
cmd = " ".join([
|
||||
'"{}"'.format(self.ffmpeg_path),
|
||||
"-ss {}".format(start_sec),
|
||||
"-t {}".format(duration_sec),
|
||||
"-i \"{}\"".format(audio_file),
|
||||
cmd = [
|
||||
self.ffmpeg_path,
|
||||
"-ss", str(start_sec),
|
||||
"-t", str(duration_sec),
|
||||
"-i", audio_file,
|
||||
audio_fpath
|
||||
])
|
||||
]
|
||||
|
||||
# run subprocess
|
||||
self.log.debug("Executing: {}".format(cmd))
|
||||
self.log.debug("Executing: {}".format(" ".join(cmd)))
|
||||
openpype.api.run_subprocess(
|
||||
cmd, logger=self.log
|
||||
)
|
||||
|
|
@ -220,17 +225,17 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
|||
max_duration_sec = max(end_secs)
|
||||
|
||||
# create empty cmd
|
||||
cmd = " ".join([
|
||||
'"{}"'.format(self.ffmpeg_path),
|
||||
"-f lavfi",
|
||||
"-i anullsrc=channel_layout=stereo:sample_rate=48000",
|
||||
"-t {}".format(max_duration_sec),
|
||||
"\"{}\"".format(empty_fpath)
|
||||
])
|
||||
cmd = [
|
||||
self.ffmpeg_path,
|
||||
"-f", "lavfi",
|
||||
"-i", "anullsrc=channel_layout=stereo:sample_rate=48000",
|
||||
"-t", str(max_duration_sec),
|
||||
empty_fpath
|
||||
]
|
||||
|
||||
# generate empty with ffmpeg
|
||||
# run subprocess
|
||||
self.log.debug("Executing: {}".format(cmd))
|
||||
self.log.debug("Executing: {}".format(" ".join(cmd)))
|
||||
|
||||
openpype.api.run_subprocess(
|
||||
cmd, logger=self.log
|
||||
|
|
@ -261,10 +266,14 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
|||
for index, input in enumerate(inputs):
|
||||
input_format = input.copy()
|
||||
input_format.update({"i": index})
|
||||
input_format["mediaPath"] = path_to_subprocess_arg(
|
||||
input_format["mediaPath"]
|
||||
)
|
||||
|
||||
_inputs += (
|
||||
"-ss {startSec} "
|
||||
"-t {durationSec} "
|
||||
"-i \"{mediaPath}\" "
|
||||
"-i {mediaPath} "
|
||||
).format(**input_format)
|
||||
|
||||
_filters += "[{i}]adelay={delayMilSec}:all=1[r{i}]; ".format(
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
|
|||
out_frame_start += end_offset
|
||||
|
||||
# start command list
|
||||
command = ['"{}"'.format(ffmpeg_path)]
|
||||
command = [ffmpeg_path]
|
||||
|
||||
if sequence:
|
||||
input_dir, collection = sequence
|
||||
|
|
@ -324,8 +324,8 @@ class ExtractOTIOReview(openpype.api.Extractor):
|
|||
|
||||
# form command for rendering gap files
|
||||
command.extend([
|
||||
"-start_number {}".format(in_frame_start),
|
||||
"-i \"{}\"".format(input_path)
|
||||
"-start_number", str(in_frame_start),
|
||||
"-i", input_path
|
||||
])
|
||||
|
||||
elif video:
|
||||
|
|
@ -334,13 +334,15 @@ class ExtractOTIOReview(openpype.api.Extractor):
|
|||
input_fps = otio_range.start_time.rate
|
||||
frame_duration = otio_range.duration.value
|
||||
sec_start = openpype.lib.frames_to_secons(frame_start, input_fps)
|
||||
sec_duration = openpype.lib.frames_to_secons(frame_duration, input_fps)
|
||||
sec_duration = openpype.lib.frames_to_secons(
|
||||
frame_duration, input_fps
|
||||
)
|
||||
|
||||
# form command for rendering gap files
|
||||
command.extend([
|
||||
"-ss {}".format(sec_start),
|
||||
"-t {}".format(sec_duration),
|
||||
"-i \"{}\"".format(video_path)
|
||||
"-ss", str(sec_start),
|
||||
"-t", str(sec_duration),
|
||||
"-i", video_path
|
||||
])
|
||||
|
||||
elif gap:
|
||||
|
|
@ -349,22 +351,24 @@ class ExtractOTIOReview(openpype.api.Extractor):
|
|||
|
||||
# form command for rendering gap files
|
||||
command.extend([
|
||||
"-t {} -r {}".format(sec_duration, self.actual_fps),
|
||||
"-f lavfi",
|
||||
"-i color=c=black:s={}x{}".format(self.to_width,
|
||||
self.to_height),
|
||||
"-tune stillimage"
|
||||
"-t", str(sec_duration),
|
||||
"-r", str(self.actual_fps),
|
||||
"-f", "lavfi",
|
||||
"-i", "color=c=black:s={}x{}".format(
|
||||
self.to_width, self.to_height
|
||||
),
|
||||
"-tune", "stillimage"
|
||||
])
|
||||
|
||||
# add output attributes
|
||||
command.extend([
|
||||
"-start_number {}".format(out_frame_start),
|
||||
"\"{}\"".format(output_path)
|
||||
"-start_number", str(out_frame_start),
|
||||
output_path
|
||||
])
|
||||
# execute
|
||||
self.log.debug("Executing: {}".format(" ".join(command)))
|
||||
output = openpype.api.run_subprocess(
|
||||
" ".join(command), logger=self.log
|
||||
command, logger=self.log
|
||||
)
|
||||
self.log.debug("Output: {}".format(output))
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor):
|
|||
output_path = self._get_ffmpeg_output(input_file_path)
|
||||
|
||||
# start command list
|
||||
command = ['"{}"'.format(ffmpeg_path)]
|
||||
command = [ffmpeg_path]
|
||||
|
||||
video_path = input_file_path
|
||||
frame_start = otio_range.start_time.value
|
||||
|
|
@ -86,17 +86,17 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor):
|
|||
|
||||
# form command for rendering gap files
|
||||
command.extend([
|
||||
"-ss {}".format(sec_start),
|
||||
"-t {}".format(sec_duration),
|
||||
"-i \"{}\"".format(video_path),
|
||||
"-c copy",
|
||||
"-ss", str(sec_start),
|
||||
"-t", str(sec_duration),
|
||||
"-i", video_path,
|
||||
"-c", "copy",
|
||||
output_path
|
||||
])
|
||||
|
||||
# execute
|
||||
self.log.debug("Executing: {}".format(" ".join(command)))
|
||||
output = openpype.api.run_subprocess(
|
||||
" ".join(command), logger=self.log
|
||||
command, logger=self.log
|
||||
)
|
||||
self.log.debug("Output: {}".format(output))
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ import openpype.api
|
|||
from openpype.lib import (
|
||||
get_ffmpeg_tool_path,
|
||||
ffprobe_streams,
|
||||
|
||||
split_command_to_list,
|
||||
path_to_subprocess_arg,
|
||||
|
||||
should_decompress,
|
||||
get_decompress_dir,
|
||||
decompress
|
||||
|
|
@ -216,12 +220,15 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
raise NotImplementedError
|
||||
|
||||
subprcs_cmd = " ".join(ffmpeg_args)
|
||||
subprocess_args = split_command_to_list(subprcs_cmd)
|
||||
|
||||
# run subprocess
|
||||
self.log.debug("Executing: {}".format(subprcs_cmd))
|
||||
self.log.debug(
|
||||
"Executing: {}".format(" ".join(subprocess_args))
|
||||
)
|
||||
|
||||
openpype.api.run_subprocess(
|
||||
subprcs_cmd, shell=True, logger=self.log
|
||||
subprocess_args, shell=True, logger=self.log
|
||||
)
|
||||
|
||||
# delete files added to fill gaps
|
||||
|
|
@ -480,7 +487,9 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
|
||||
# Add video/image input path
|
||||
ffmpeg_input_args.append(
|
||||
"-i \"{}\"".format(temp_data["full_input_path"])
|
||||
"-i {}".format(
|
||||
path_to_subprocess_arg(temp_data["full_input_path"])
|
||||
)
|
||||
)
|
||||
|
||||
# Add audio arguments if there are any. Skipped when output are images.
|
||||
|
|
@ -538,7 +547,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
|
||||
# NOTE This must be latest added item to output arguments.
|
||||
ffmpeg_output_args.append(
|
||||
"\"{}\"".format(temp_data["full_output_path"])
|
||||
path_to_subprocess_arg(temp_data["full_output_path"])
|
||||
)
|
||||
|
||||
return self.ffmpeg_full_args(
|
||||
|
|
@ -607,7 +616,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
audio_filters.append(arg)
|
||||
|
||||
all_args = []
|
||||
all_args.append("\"{}\"".format(self.ffmpeg_path))
|
||||
all_args.append(path_to_subprocess_arg(self.ffmpeg_path))
|
||||
all_args.extend(input_args)
|
||||
if video_filters:
|
||||
all_args.append("-filter:v")
|
||||
|
|
@ -854,7 +863,9 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
audio_in_args.append("-to {:0.10f}".format(audio_duration))
|
||||
|
||||
# Add audio input path
|
||||
audio_in_args.append("-i \"{}\"".format(audio["filename"]))
|
||||
audio_in_args.append("-i {}".format(
|
||||
path_to_subprocess_arg(audio["filename"])
|
||||
))
|
||||
|
||||
# NOTE: These were changed from input to output arguments.
|
||||
# NOTE: value in "-ac" was hardcoded to 2, changed to audio inputs len.
|
||||
|
|
|
|||
|
|
@ -117,11 +117,13 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
input_args.extend(repre["_profile"].get('input', []))
|
||||
else:
|
||||
input_args.extend(repre["outputDef"].get('input', []))
|
||||
input_args.append("-loop 1 -i {}".format(slate_path))
|
||||
input_args.append("-loop 1 -i {}".format(
|
||||
openpype.lib.path_to_subprocess_arg(slate_path)
|
||||
))
|
||||
input_args.extend([
|
||||
"-r {}".format(fps),
|
||||
"-t 0.04"]
|
||||
)
|
||||
"-t 0.04"
|
||||
])
|
||||
|
||||
if use_legacy_code:
|
||||
codec_args = repre["_profile"].get('codec', [])
|
||||
|
|
@ -188,20 +190,26 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
output_args.append("-y")
|
||||
|
||||
slate_v_path = slate_path.replace(".png", ext)
|
||||
output_args.append(slate_v_path)
|
||||
output_args.append(
|
||||
openpype.lib.path_to_subprocess_arg(slate_v_path)
|
||||
)
|
||||
_remove_at_end.append(slate_v_path)
|
||||
|
||||
slate_args = [
|
||||
"\"{}\"".format(ffmpeg_path),
|
||||
openpype.lib.path_to_subprocess_arg(ffmpeg_path),
|
||||
" ".join(input_args),
|
||||
" ".join(output_args)
|
||||
]
|
||||
slate_subprcs_cmd = " ".join(slate_args)
|
||||
slate_subprocess_args = openpype.lib.split_command_to_list(
|
||||
" ".join(slate_args)
|
||||
)
|
||||
|
||||
# run slate generation subprocess
|
||||
self.log.debug("Slate Executing: {}".format(slate_subprcs_cmd))
|
||||
self.log.debug(
|
||||
"Slate Executing: {}".format(" ".join(slate_subprocess_args))
|
||||
)
|
||||
openpype.api.run_subprocess(
|
||||
slate_subprcs_cmd, shell=True, logger=self.log
|
||||
slate_subprocess_args, shell=True, logger=self.log
|
||||
)
|
||||
|
||||
# create ffmpeg concat text file path
|
||||
|
|
@ -221,23 +229,22 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
])
|
||||
|
||||
# concat slate and videos together
|
||||
conc_input_args = ["-y", "-f concat", "-safe 0"]
|
||||
conc_input_args.append("-i {}".format(conc_text_path))
|
||||
|
||||
conc_output_args = ["-c copy"]
|
||||
conc_output_args.append(output_path)
|
||||
|
||||
concat_args = [
|
||||
ffmpeg_path,
|
||||
" ".join(conc_input_args),
|
||||
" ".join(conc_output_args)
|
||||
"-y",
|
||||
"-f", "concat",
|
||||
"-safe", "0",
|
||||
"-i", conc_text_path,
|
||||
"-c", "copy",
|
||||
output_path
|
||||
]
|
||||
concat_subprcs_cmd = " ".join(concat_args)
|
||||
|
||||
# ffmpeg concat subprocess
|
||||
self.log.debug("Executing concat: {}".format(concat_subprcs_cmd))
|
||||
self.log.debug(
|
||||
"Executing concat: {}".format(" ".join(concat_args))
|
||||
)
|
||||
openpype.api.run_subprocess(
|
||||
concat_subprcs_cmd, shell=True, logger=self.log
|
||||
concat_args, shell=True, logger=self.log
|
||||
)
|
||||
|
||||
self.log.debug("__ repre[tags]: {}".format(repre["tags"]))
|
||||
|
|
|
|||
|
|
@ -106,12 +106,16 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
"family", "hierarchy", "task", "username"
|
||||
]
|
||||
default_template_name = "publish"
|
||||
template_name_profiles = None
|
||||
|
||||
# suffix to denote temporary files, use without '.'
|
||||
TMP_FILE_EXT = 'tmp'
|
||||
|
||||
# file_url : file_size of all published and uploaded files
|
||||
integrated_file_sizes = {}
|
||||
|
||||
TMP_FILE_EXT = 'tmp' # suffix to denote temporary files, use without '.'
|
||||
# Attributes set by settings
|
||||
template_name_profiles = None
|
||||
subset_grouping_profiles = None
|
||||
|
||||
def process(self, instance):
|
||||
self.integrated_file_sizes = {}
|
||||
|
|
@ -165,10 +169,24 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
hierarchy = "/".join(parents)
|
||||
anatomy_data["hierarchy"] = hierarchy
|
||||
|
||||
# Make sure task name in anatomy data is same as on instance.data
|
||||
task_name = instance.data.get("task")
|
||||
if task_name:
|
||||
anatomy_data["task"] = task_name
|
||||
else:
|
||||
# Just set 'task_name' variable to context task
|
||||
task_name = anatomy_data["task"]
|
||||
|
||||
# Find task type for current task name
|
||||
# - this should be already prepared on instance
|
||||
asset_tasks = (
|
||||
asset_entity.get("data", {}).get("tasks")
|
||||
) or {}
|
||||
task_info = asset_tasks.get(task_name) or {}
|
||||
task_type = task_info.get("type")
|
||||
instance.data["task_type"] = task_type
|
||||
|
||||
# Fill family in anatomy data
|
||||
anatomy_data["family"] = instance.data.get("family")
|
||||
|
||||
stagingdir = instance.data.get("stagingDir")
|
||||
|
|
@ -298,14 +316,19 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
else:
|
||||
orig_transfers = list(instance.data['transfers'])
|
||||
|
||||
task_name = io.Session.get("AVALON_TASK")
|
||||
family = self.main_family_from_instance(instance)
|
||||
|
||||
key_values = {"families": family,
|
||||
"tasks": task_name,
|
||||
"hosts": instance.data["anatomyData"]["app"]}
|
||||
profile = filter_profiles(self.template_name_profiles, key_values,
|
||||
logger=self.log)
|
||||
key_values = {
|
||||
"families": family,
|
||||
"tasks": task_name,
|
||||
"hosts": instance.context.data["hostName"],
|
||||
"task_types": task_type
|
||||
}
|
||||
profile = filter_profiles(
|
||||
self.template_name_profiles,
|
||||
key_values,
|
||||
logger=self.log
|
||||
)
|
||||
|
||||
template_name = "publish"
|
||||
if profile:
|
||||
|
|
@ -730,6 +753,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
|
||||
subset = io.find_one({"_id": _id})
|
||||
|
||||
# QUESTION Why is changing of group and updating it's
|
||||
# families in 'get_subset'?
|
||||
self._set_subset_group(instance, subset["_id"])
|
||||
|
||||
# Update families on subset.
|
||||
|
|
@ -753,54 +778,74 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
subset_id (str): DB's subset _id
|
||||
|
||||
"""
|
||||
# add group if available
|
||||
integrate_new_sett = (instance.context.data["project_settings"]
|
||||
["global"]
|
||||
["publish"]
|
||||
["IntegrateAssetNew"])
|
||||
|
||||
profiles = integrate_new_sett["subset_grouping_profiles"]
|
||||
|
||||
filtering_criteria = {
|
||||
"families": instance.data["family"],
|
||||
"hosts": instance.data["anatomyData"]["app"],
|
||||
"tasks": instance.data["anatomyData"]["task"] or
|
||||
io.Session["AVALON_TASK"]
|
||||
}
|
||||
matching_profile = filter_profiles(profiles, filtering_criteria)
|
||||
|
||||
filled_template = None
|
||||
if matching_profile:
|
||||
template = matching_profile["template"]
|
||||
fill_pairs = (
|
||||
("family", filtering_criteria["families"]),
|
||||
("task", filtering_criteria["tasks"]),
|
||||
("host", filtering_criteria["hosts"]),
|
||||
("subset", instance.data["subset"]),
|
||||
("renderlayer", instance.data.get("renderlayer"))
|
||||
)
|
||||
fill_pairs = prepare_template_data(fill_pairs)
|
||||
|
||||
try:
|
||||
filled_template = \
|
||||
format_template_with_optional_keys(fill_pairs, template)
|
||||
except KeyError:
|
||||
keys = []
|
||||
if fill_pairs:
|
||||
keys = fill_pairs.keys()
|
||||
|
||||
msg = "Subset grouping failed. " \
|
||||
"Only {} are expected in Settings".format(','.join(keys))
|
||||
self.log.warning(msg)
|
||||
|
||||
if instance.data.get("subsetGroup") or filled_template:
|
||||
subset_group = instance.data.get('subsetGroup') or filled_template
|
||||
# Fist look into instance data
|
||||
subset_group = instance.data.get("subsetGroup")
|
||||
if not subset_group:
|
||||
subset_group = self._get_subset_group(instance)
|
||||
|
||||
if subset_group:
|
||||
io.update_many({
|
||||
'type': 'subset',
|
||||
'_id': io.ObjectId(subset_id)
|
||||
}, {'$set': {'data.subsetGroup': subset_group}})
|
||||
|
||||
def _get_subset_group(self, instance):
|
||||
"""Look into subset group profiles set by settings.
|
||||
|
||||
Attribute 'subset_grouping_profiles' is defined by OpenPype settings.
|
||||
"""
|
||||
# Skip if 'subset_grouping_profiles' is empty
|
||||
if not self.subset_grouping_profiles:
|
||||
return None
|
||||
|
||||
# QUESTION
|
||||
# - is there a chance that task name is not filled in anatomy
|
||||
# data?
|
||||
# - should we use context task in that case?
|
||||
task_name = (
|
||||
instance.data["anatomyData"]["task"]
|
||||
or io.Session["AVALON_TASK"]
|
||||
)
|
||||
task_type = instance.data["task_type"]
|
||||
filtering_criteria = {
|
||||
"families": instance.data["family"],
|
||||
"hosts": instance.context.data["hostName"],
|
||||
"tasks": task_name,
|
||||
"task_types": task_type
|
||||
}
|
||||
matching_profile = filter_profiles(
|
||||
self.subset_grouping_profiles,
|
||||
filtering_criteria
|
||||
)
|
||||
# Skip if there is not matchin profile
|
||||
if not matching_profile:
|
||||
return None
|
||||
|
||||
filled_template = None
|
||||
template = matching_profile["template"]
|
||||
fill_pairs = (
|
||||
("family", filtering_criteria["families"]),
|
||||
("task", filtering_criteria["tasks"]),
|
||||
("host", filtering_criteria["hosts"]),
|
||||
("subset", instance.data["subset"]),
|
||||
("renderlayer", instance.data.get("renderlayer"))
|
||||
)
|
||||
fill_pairs = prepare_template_data(fill_pairs)
|
||||
|
||||
try:
|
||||
filled_template = \
|
||||
format_template_with_optional_keys(fill_pairs, template)
|
||||
except KeyError:
|
||||
keys = []
|
||||
if fill_pairs:
|
||||
keys = fill_pairs.keys()
|
||||
|
||||
msg = "Subset grouping failed. " \
|
||||
"Only {} are expected in Settings".format(','.join(keys))
|
||||
self.log.warning(msg)
|
||||
|
||||
return filled_template
|
||||
|
||||
def create_version(self, subset, version_number, data=None):
|
||||
""" Copy given source to destination
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
|
||||
from openpype.lib.pype_info import is_running_staging
|
||||
|
||||
RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
@ -30,22 +30,22 @@ def get_liberation_font_path(bold=False, italic=False):
|
|||
return font_path
|
||||
|
||||
|
||||
def pype_icon_filepath(debug=None):
|
||||
if debug is None:
|
||||
debug = bool(os.getenv("OPENPYPE_DEV"))
|
||||
def get_openpype_icon_filepath(staging=None):
|
||||
if staging is None:
|
||||
staging = is_running_staging()
|
||||
|
||||
if debug:
|
||||
if staging:
|
||||
icon_file_name = "openpype_icon_staging.png"
|
||||
else:
|
||||
icon_file_name = "openpype_icon.png"
|
||||
return get_resource("icons", icon_file_name)
|
||||
|
||||
|
||||
def pype_splash_filepath(debug=None):
|
||||
if debug is None:
|
||||
debug = bool(os.getenv("OPENPYPE_DEV"))
|
||||
def get_openpype_splash_filepath(staging=None):
|
||||
if staging is None:
|
||||
staging = is_running_staging()
|
||||
|
||||
if debug:
|
||||
if staging:
|
||||
splash_file_name = "openpype_splash_staging.png"
|
||||
else:
|
||||
splash_file_name = "openpype_splash.png"
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@
|
|||
"standalonepublisher"
|
||||
],
|
||||
"families": [],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": true,
|
||||
"advanced_filtering": []
|
||||
|
|
@ -221,6 +222,7 @@
|
|||
"matchmove",
|
||||
"shot"
|
||||
],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": false,
|
||||
"advanced_filtering": []
|
||||
|
|
@ -232,6 +234,7 @@
|
|||
"families": [
|
||||
"plate"
|
||||
],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": false,
|
||||
"advanced_filtering": [
|
||||
|
|
@ -256,6 +259,7 @@
|
|||
"rig",
|
||||
"camera"
|
||||
],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": true,
|
||||
"advanced_filtering": []
|
||||
|
|
@ -267,6 +271,7 @@
|
|||
"families": [
|
||||
"renderPass"
|
||||
],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": false,
|
||||
"advanced_filtering": []
|
||||
|
|
@ -276,6 +281,7 @@
|
|||
"tvpaint"
|
||||
],
|
||||
"families": [],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": true,
|
||||
"advanced_filtering": []
|
||||
|
|
@ -288,6 +294,7 @@
|
|||
"write",
|
||||
"render"
|
||||
],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": false,
|
||||
"advanced_filtering": [
|
||||
|
|
@ -307,6 +314,7 @@
|
|||
"render",
|
||||
"workfile"
|
||||
],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": true,
|
||||
"advanced_filtering": []
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@
|
|||
{
|
||||
"families": [],
|
||||
"hosts": [],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template_name": "publish"
|
||||
},
|
||||
|
|
@ -162,6 +163,7 @@
|
|||
"prerender"
|
||||
],
|
||||
"hosts": [],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template_name": "render"
|
||||
}
|
||||
|
|
@ -170,6 +172,7 @@
|
|||
{
|
||||
"families": [],
|
||||
"hosts": [],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template": ""
|
||||
}
|
||||
|
|
@ -205,6 +208,7 @@
|
|||
{
|
||||
"families": [],
|
||||
"hosts": [],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template": "{family}{Variant}"
|
||||
},
|
||||
|
|
@ -213,6 +217,7 @@
|
|||
"render"
|
||||
],
|
||||
"hosts": [],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template": "{family}{Task}{Variant}"
|
||||
},
|
||||
|
|
@ -224,6 +229,7 @@
|
|||
"hosts": [
|
||||
"tvpaint"
|
||||
],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template": "{family}{Task}_{Render_layer}_{Render_pass}"
|
||||
},
|
||||
|
|
@ -235,6 +241,7 @@
|
|||
"hosts": [
|
||||
"tvpaint"
|
||||
],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template": "{family}{Task}"
|
||||
},
|
||||
|
|
@ -245,6 +252,7 @@
|
|||
"hosts": [
|
||||
"aftereffects"
|
||||
],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template": "render{Task}{Variant}"
|
||||
}
|
||||
|
|
@ -261,6 +269,7 @@
|
|||
"last_workfile_on_startup": [
|
||||
{
|
||||
"hosts": [],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"enabled": true
|
||||
}
|
||||
|
|
@ -268,6 +277,7 @@
|
|||
"open_workfile_tool_on_startup": [
|
||||
{
|
||||
"hosts": [],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"enabled": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -520,6 +520,7 @@
|
|||
"workfile_build": {
|
||||
"profiles": [
|
||||
{
|
||||
"task_types": [],
|
||||
"tasks": [
|
||||
"Lighting"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -30,7 +30,13 @@
|
|||
},
|
||||
"publish": {
|
||||
"PreCollectNukeInstances": {
|
||||
"sync_workfile_version": true
|
||||
"sync_workfile_version_on_families": [
|
||||
"nukenodes",
|
||||
"camera",
|
||||
"gizmo",
|
||||
"source",
|
||||
"render"
|
||||
]
|
||||
},
|
||||
"ValidateContainers": {
|
||||
"enabled": true,
|
||||
|
|
@ -163,6 +169,7 @@
|
|||
"builder_on_start": false,
|
||||
"profiles": [
|
||||
{
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"current_context": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@
|
|||
"profiles": [
|
||||
{
|
||||
"families": [],
|
||||
"tasks": [],
|
||||
"hosts": [],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"channel_messages": []
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -650,6 +650,11 @@
|
|||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
"label": "Task names",
|
||||
|
|
|
|||
|
|
@ -52,18 +52,23 @@
|
|||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
"label": "Task names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "hosts-enum",
|
||||
"key": "hosts",
|
||||
"label": "Host names",
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
"label": "Task names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -502,6 +502,11 @@
|
|||
"label": "Hosts",
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
"label": "Task names",
|
||||
|
|
@ -543,6 +548,11 @@
|
|||
"label": "Hosts",
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
"label": "Task names",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@
|
|||
"label": "Hosts",
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
"label": "Task names",
|
||||
|
|
@ -126,9 +131,14 @@
|
|||
"unreal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
"label": "Tasks",
|
||||
"label": "Task names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
|
|
@ -161,9 +171,15 @@
|
|||
"nuke"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "list",
|
||||
"object_type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
"label": "Tasks",
|
||||
"label": "Task names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,9 +16,30 @@
|
|||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "sync_workfile_version",
|
||||
"label": "Sync Version from workfile"
|
||||
"type": "enum",
|
||||
"key": "sync_workfile_version_on_families",
|
||||
"label": "Sync workfile version for families",
|
||||
"multiselection": true,
|
||||
"enum_items": [
|
||||
{
|
||||
"nukenodes": "nukenodes"
|
||||
},
|
||||
{
|
||||
"camera": "camera"
|
||||
},
|
||||
{
|
||||
"gizmo": "gizmo"
|
||||
},
|
||||
{
|
||||
"source": "source"
|
||||
},
|
||||
{
|
||||
"prerender": "prerender"
|
||||
},
|
||||
{
|
||||
"render": "render"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,9 +11,14 @@
|
|||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
"label": "Tasks",
|
||||
"label": "Task names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
|
|
@ -94,4 +99,4 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,14 @@
|
|||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
"label": "Tasks",
|
||||
"label": "Task names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -91,4 +91,4 @@ def load_stylesheet():
|
|||
|
||||
|
||||
def app_icon_path():
|
||||
return resources.pype_icon_filepath()
|
||||
return resources.get_openpype_icon_filepath()
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class ApplicationAction(api.Action):
|
|||
|
||||
def _show_message_box(self, title, message, details=None):
|
||||
dialog = QtWidgets.QMessageBox()
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
dialog.setWindowIcon(icon)
|
||||
dialog.setStyleSheet(style.load_stylesheet())
|
||||
dialog.setWindowTitle(title)
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
|
||||
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
|
|
|
|||
11
openpype/tools/libraryloader/__init__.py
Normal file
11
openpype/tools/libraryloader/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from .app import (
|
||||
LibraryLoaderWindow,
|
||||
show,
|
||||
cli
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LibraryLoaderWindow",
|
||||
"show",
|
||||
"cli",
|
||||
]
|
||||
5
openpype/tools/libraryloader/__main__.py
Normal file
5
openpype/tools/libraryloader/__main__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from . import cli
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.exit(cli(sys.argv[1:]))
|
||||
591
openpype/tools/libraryloader/app.py
Normal file
591
openpype/tools/libraryloader/app.py
Normal file
|
|
@ -0,0 +1,591 @@
|
|||
import sys
|
||||
import time
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from avalon import style
|
||||
from avalon.api import AvalonMongoDB
|
||||
from openpype.tools.utils import lib as tools_lib
|
||||
from openpype.tools.loader.widgets import (
|
||||
ThumbnailWidget,
|
||||
VersionWidget,
|
||||
FamilyListWidget,
|
||||
RepresentationWidget
|
||||
)
|
||||
from openpype.tools.utils.widgets import AssetWidget
|
||||
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
from . import lib
|
||||
from .widgets import LibrarySubsetWidget
|
||||
|
||||
module = sys.modules[__name__]
|
||||
module.window = None
|
||||
|
||||
|
||||
class LibraryLoaderWindow(QtWidgets.QDialog):
|
||||
"""Asset library loader interface"""
|
||||
|
||||
tool_title = "Library Loader 0.5"
|
||||
tool_name = "library_loader"
|
||||
|
||||
def __init__(
|
||||
self, parent=None, icon=None, show_projects=False, show_libraries=True
|
||||
):
|
||||
super(LibraryLoaderWindow, self).__init__(parent)
|
||||
|
||||
self._initial_refresh = False
|
||||
self._ignore_project_change = False
|
||||
|
||||
# Enable minimize and maximize for app
|
||||
self.setWindowTitle(self.tool_title)
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
if icon is not None:
|
||||
self.setWindowIcon(icon)
|
||||
# self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
|
||||
body = QtWidgets.QWidget()
|
||||
footer = QtWidgets.QWidget()
|
||||
footer.setFixedHeight(20)
|
||||
|
||||
container = QtWidgets.QWidget()
|
||||
|
||||
self.dbcon = AvalonMongoDB()
|
||||
self.dbcon.install()
|
||||
self.dbcon.Session["AVALON_PROJECT"] = None
|
||||
|
||||
self.show_projects = show_projects
|
||||
self.show_libraries = show_libraries
|
||||
|
||||
# Groups config
|
||||
self.groups_config = tools_lib.GroupsConfig(self.dbcon)
|
||||
self.family_config_cache = tools_lib.FamilyConfigCache(self.dbcon)
|
||||
|
||||
assets = AssetWidget(
|
||||
self.dbcon, multiselection=True, parent=self
|
||||
)
|
||||
families = FamilyListWidget(
|
||||
self.dbcon, self.family_config_cache, parent=self
|
||||
)
|
||||
subsets = LibrarySubsetWidget(
|
||||
self.dbcon,
|
||||
self.groups_config,
|
||||
self.family_config_cache,
|
||||
tool_name=self.tool_name,
|
||||
parent=self
|
||||
)
|
||||
|
||||
version = VersionWidget(self.dbcon)
|
||||
thumbnail = ThumbnailWidget(self.dbcon)
|
||||
|
||||
# Project
|
||||
self.combo_projects = QtWidgets.QComboBox()
|
||||
|
||||
# Create splitter to show / hide family filters
|
||||
asset_filter_splitter = QtWidgets.QSplitter()
|
||||
asset_filter_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
asset_filter_splitter.addWidget(self.combo_projects)
|
||||
asset_filter_splitter.addWidget(assets)
|
||||
asset_filter_splitter.addWidget(families)
|
||||
asset_filter_splitter.setStretchFactor(1, 65)
|
||||
asset_filter_splitter.setStretchFactor(2, 35)
|
||||
|
||||
manager = ModulesManager()
|
||||
sync_server = manager.modules_by_name["sync_server"]
|
||||
|
||||
representations = RepresentationWidget(self.dbcon)
|
||||
thumb_ver_splitter = QtWidgets.QSplitter()
|
||||
thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
thumb_ver_splitter.addWidget(thumbnail)
|
||||
thumb_ver_splitter.addWidget(version)
|
||||
if sync_server.enabled:
|
||||
thumb_ver_splitter.addWidget(representations)
|
||||
thumb_ver_splitter.setStretchFactor(0, 30)
|
||||
thumb_ver_splitter.setStretchFactor(1, 35)
|
||||
|
||||
container_layout = QtWidgets.QHBoxLayout(container)
|
||||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
split = QtWidgets.QSplitter()
|
||||
split.addWidget(asset_filter_splitter)
|
||||
split.addWidget(subsets)
|
||||
split.addWidget(thumb_ver_splitter)
|
||||
split.setSizes([180, 950, 200])
|
||||
container_layout.addWidget(split)
|
||||
|
||||
body_layout = QtWidgets.QHBoxLayout(body)
|
||||
body_layout.addWidget(container)
|
||||
body_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
message = QtWidgets.QLabel()
|
||||
message.hide()
|
||||
|
||||
footer_layout = QtWidgets.QVBoxLayout(footer)
|
||||
footer_layout.addWidget(message)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(body)
|
||||
layout.addWidget(footer)
|
||||
|
||||
self.data = {
|
||||
"widgets": {
|
||||
"families": families,
|
||||
"assets": assets,
|
||||
"subsets": subsets,
|
||||
"version": version,
|
||||
"thumbnail": thumbnail,
|
||||
"representations": representations
|
||||
},
|
||||
"label": {
|
||||
"message": message,
|
||||
},
|
||||
"state": {
|
||||
"assetIds": None
|
||||
}
|
||||
}
|
||||
|
||||
families.active_changed.connect(subsets.set_family_filters)
|
||||
assets.selection_changed.connect(self.on_assetschanged)
|
||||
assets.refresh_triggered.connect(self.on_assetschanged)
|
||||
assets.view.clicked.connect(self.on_assetview_click)
|
||||
subsets.active_changed.connect(self.on_subsetschanged)
|
||||
subsets.version_changed.connect(self.on_versionschanged)
|
||||
self.combo_projects.currentTextChanged.connect(self.on_project_change)
|
||||
|
||||
self.sync_server = sync_server
|
||||
|
||||
# Set default thumbnail on start
|
||||
thumbnail.set_thumbnail(None)
|
||||
|
||||
# Defaults
|
||||
if sync_server.enabled:
|
||||
split.setSizes([250, 1000, 550])
|
||||
self.resize(1800, 900)
|
||||
else:
|
||||
split.setSizes([250, 850, 200])
|
||||
self.resize(1300, 700)
|
||||
|
||||
def showEvent(self, event):
|
||||
super(LibraryLoaderWindow, self).showEvent(event)
|
||||
if not self._initial_refresh:
|
||||
self.refresh()
|
||||
|
||||
def on_assetview_click(self, *args):
|
||||
subsets_widget = self.data["widgets"]["subsets"]
|
||||
selection_model = subsets_widget.view.selectionModel()
|
||||
if selection_model.selectedIndexes():
|
||||
selection_model.clearSelection()
|
||||
|
||||
def _set_projects(self):
|
||||
# Store current project
|
||||
old_project_name = self.current_project
|
||||
|
||||
self._ignore_project_change = True
|
||||
|
||||
# Cleanup
|
||||
self.combo_projects.clear()
|
||||
|
||||
# Fill combobox with projects
|
||||
select_project_item = QtGui.QStandardItem("< Select project >")
|
||||
select_project_item.setData(None, QtCore.Qt.UserRole + 1)
|
||||
|
||||
combobox_items = [select_project_item]
|
||||
|
||||
project_names = self.get_filtered_projects()
|
||||
|
||||
for project_name in sorted(project_names):
|
||||
item = QtGui.QStandardItem(project_name)
|
||||
item.setData(project_name, QtCore.Qt.UserRole + 1)
|
||||
combobox_items.append(item)
|
||||
|
||||
root_item = self.combo_projects.model().invisibleRootItem()
|
||||
root_item.appendRows(combobox_items)
|
||||
|
||||
index = 0
|
||||
self._ignore_project_change = False
|
||||
|
||||
if old_project_name:
|
||||
index = self.combo_projects.findText(
|
||||
old_project_name, QtCore.Qt.MatchFixedString
|
||||
)
|
||||
|
||||
self.combo_projects.setCurrentIndex(index)
|
||||
|
||||
def get_filtered_projects(self):
|
||||
projects = list()
|
||||
for project in self.dbcon.projects():
|
||||
is_library = project.get("data", {}).get("library_project", False)
|
||||
if (
|
||||
(is_library and self.show_libraries) or
|
||||
(not is_library and self.show_projects)
|
||||
):
|
||||
projects.append(project["name"])
|
||||
|
||||
return projects
|
||||
|
||||
def on_project_change(self):
|
||||
if self._ignore_project_change:
|
||||
return
|
||||
|
||||
row = self.combo_projects.currentIndex()
|
||||
index = self.combo_projects.model().index(row, 0)
|
||||
project_name = index.data(QtCore.Qt.UserRole + 1)
|
||||
|
||||
self.dbcon.Session["AVALON_PROJECT"] = project_name
|
||||
|
||||
_config = lib.find_config()
|
||||
if hasattr(_config, "install"):
|
||||
_config.install()
|
||||
else:
|
||||
print(
|
||||
"Config `%s` has no function `install`" % _config.__name__
|
||||
)
|
||||
|
||||
self.family_config_cache.refresh()
|
||||
self.groups_config.refresh()
|
||||
|
||||
self._refresh_assets()
|
||||
self._assetschanged()
|
||||
|
||||
project_name = self.dbcon.active_project() or "No project selected"
|
||||
title = "{} - {}".format(self.tool_title, project_name)
|
||||
self.setWindowTitle(title)
|
||||
|
||||
subsets = self.data["widgets"]["subsets"]
|
||||
subsets.on_project_change(self.dbcon.Session["AVALON_PROJECT"])
|
||||
|
||||
representations = self.data["widgets"]["representations"]
|
||||
representations.on_project_change(self.dbcon.Session["AVALON_PROJECT"])
|
||||
|
||||
@property
|
||||
def current_project(self):
|
||||
if (
|
||||
not self.dbcon.active_project() or
|
||||
self.dbcon.active_project() == ""
|
||||
):
|
||||
return None
|
||||
|
||||
return self.dbcon.active_project()
|
||||
|
||||
# -------------------------------
|
||||
# Delay calling blocking methods
|
||||
# -------------------------------
|
||||
|
||||
def refresh(self):
|
||||
self.echo("Fetching results..")
|
||||
tools_lib.schedule(self._refresh, 50, channel="mongo")
|
||||
|
||||
def on_assetschanged(self, *args):
|
||||
self.echo("Fetching asset..")
|
||||
tools_lib.schedule(self._assetschanged, 50, channel="mongo")
|
||||
|
||||
def on_subsetschanged(self, *args):
|
||||
self.echo("Fetching subset..")
|
||||
tools_lib.schedule(self._subsetschanged, 50, channel="mongo")
|
||||
|
||||
def on_versionschanged(self, *args):
|
||||
self.echo("Fetching version..")
|
||||
tools_lib.schedule(self._versionschanged, 150, channel="mongo")
|
||||
|
||||
def set_context(self, context, refresh=True):
|
||||
self.echo("Setting context: {}".format(context))
|
||||
lib.schedule(
|
||||
lambda: self._set_context(context, refresh=refresh),
|
||||
50, channel="mongo"
|
||||
)
|
||||
|
||||
# ------------------------------
|
||||
def _refresh(self):
|
||||
if not self._initial_refresh:
|
||||
self._initial_refresh = True
|
||||
self._set_projects()
|
||||
|
||||
def _refresh_assets(self):
|
||||
"""Load assets from database"""
|
||||
if self.current_project is not None:
|
||||
# Ensure a project is loaded
|
||||
project_doc = self.dbcon.find_one(
|
||||
{"type": "project"},
|
||||
{"type": 1}
|
||||
)
|
||||
assert project_doc, "This is a bug"
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
assets_widget.model.stop_fetch_thread()
|
||||
assets_widget.refresh()
|
||||
assets_widget.setFocus()
|
||||
|
||||
families = self.data["widgets"]["families"]
|
||||
families.refresh()
|
||||
|
||||
def clear_assets_underlines(self):
|
||||
last_asset_ids = self.data["state"]["assetIds"]
|
||||
if not last_asset_ids:
|
||||
return
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
id_role = assets_widget.model.ObjectIdRole
|
||||
|
||||
for index in tools_lib.iter_model_rows(assets_widget.model, 0):
|
||||
if index.data(id_role) not in last_asset_ids:
|
||||
continue
|
||||
|
||||
assets_widget.model.setData(
|
||||
index, [], assets_widget.model.subsetColorsRole
|
||||
)
|
||||
|
||||
def _assetschanged(self):
|
||||
"""Selected assets have changed"""
|
||||
t1 = time.time()
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
subsets_widget = self.data["widgets"]["subsets"]
|
||||
subsets_model = subsets_widget.model
|
||||
|
||||
subsets_model.clear()
|
||||
self.clear_assets_underlines()
|
||||
|
||||
if not self.dbcon.Session.get("AVALON_PROJECT"):
|
||||
subsets_widget.set_loading_state(
|
||||
loading=False,
|
||||
empty=True
|
||||
)
|
||||
return
|
||||
|
||||
# filter None docs they are silo
|
||||
asset_docs = assets_widget.get_selected_assets()
|
||||
if len(asset_docs) == 0:
|
||||
return
|
||||
|
||||
asset_ids = [asset_doc["_id"] for asset_doc in asset_docs]
|
||||
# Start loading
|
||||
subsets_widget.set_loading_state(
|
||||
loading=bool(asset_ids),
|
||||
empty=True
|
||||
)
|
||||
|
||||
def on_refreshed(has_item):
|
||||
empty = not has_item
|
||||
subsets_widget.set_loading_state(loading=False, empty=empty)
|
||||
subsets_model.refreshed.disconnect()
|
||||
self.echo("Duration: %.3fs" % (time.time() - t1))
|
||||
|
||||
subsets_model.refreshed.connect(on_refreshed)
|
||||
|
||||
subsets_model.set_assets(asset_ids)
|
||||
subsets_widget.view.setColumnHidden(
|
||||
subsets_model.Columns.index("asset"),
|
||||
len(asset_ids) < 2
|
||||
)
|
||||
|
||||
# Clear the version information on asset change
|
||||
self.data["widgets"]["version"].set_version(None)
|
||||
self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs)
|
||||
|
||||
self.data["state"]["assetIds"] = asset_ids
|
||||
|
||||
representations = self.data["widgets"]["representations"]
|
||||
representations.set_version_ids([]) # reset repre list
|
||||
|
||||
self.echo("Duration: %.3fs" % (time.time() - t1))
|
||||
|
||||
def _subsetschanged(self):
|
||||
asset_ids = self.data["state"]["assetIds"]
|
||||
# Skip setting colors if not asset multiselection
|
||||
if not asset_ids or len(asset_ids) < 2:
|
||||
self._versionschanged()
|
||||
return
|
||||
|
||||
subsets = self.data["widgets"]["subsets"]
|
||||
selected_subsets = subsets.selected_subsets(_merged=True, _other=False)
|
||||
|
||||
asset_models = {}
|
||||
asset_ids = []
|
||||
for subset_node in selected_subsets:
|
||||
asset_ids.extend(subset_node.get("assetIds", []))
|
||||
asset_ids = set(asset_ids)
|
||||
|
||||
for subset_node in selected_subsets:
|
||||
for asset_id in asset_ids:
|
||||
if asset_id not in asset_models:
|
||||
asset_models[asset_id] = []
|
||||
|
||||
color = None
|
||||
if asset_id in subset_node.get("assetIds", []):
|
||||
color = subset_node["subsetColor"]
|
||||
|
||||
asset_models[asset_id].append(color)
|
||||
|
||||
self.clear_assets_underlines()
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
indexes = assets_widget.view.selectionModel().selectedRows()
|
||||
|
||||
for index in indexes:
|
||||
id = index.data(assets_widget.model.ObjectIdRole)
|
||||
if id not in asset_models:
|
||||
continue
|
||||
|
||||
assets_widget.model.setData(
|
||||
index, asset_models[id], assets_widget.model.subsetColorsRole
|
||||
)
|
||||
# Trigger repaint
|
||||
assets_widget.view.updateGeometries()
|
||||
# Set version in Version Widget
|
||||
self._versionschanged()
|
||||
|
||||
def _versionschanged(self):
|
||||
|
||||
subsets = self.data["widgets"]["subsets"]
|
||||
selection = subsets.view.selectionModel()
|
||||
|
||||
# Active must be in the selected rows otherwise we
|
||||
# assume it's not actually an "active" current index.
|
||||
version_docs = None
|
||||
version_doc = None
|
||||
active = selection.currentIndex()
|
||||
rows = selection.selectedRows(column=active.column())
|
||||
if active and active in rows:
|
||||
item = active.data(subsets.model.ItemRole)
|
||||
if (
|
||||
item is not None
|
||||
and not (item.get("isGroup") or item.get("isMerged"))
|
||||
):
|
||||
version_doc = item["version_document"]
|
||||
|
||||
if rows:
|
||||
version_docs = []
|
||||
for index in rows:
|
||||
if not index or not index.isValid():
|
||||
continue
|
||||
item = index.data(subsets.model.ItemRole)
|
||||
if (
|
||||
item is None
|
||||
or item.get("isGroup")
|
||||
or item.get("isMerged")
|
||||
):
|
||||
continue
|
||||
version_docs.append(item["version_document"])
|
||||
|
||||
self.data["widgets"]["version"].set_version(version_doc)
|
||||
|
||||
thumbnail_docs = version_docs
|
||||
if not thumbnail_docs:
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
asset_docs = assets_widget.get_selected_assets()
|
||||
if len(asset_docs) > 0:
|
||||
thumbnail_docs = asset_docs
|
||||
|
||||
self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs)
|
||||
|
||||
representations = self.data["widgets"]["representations"]
|
||||
version_ids = [doc["_id"] for doc in version_docs or []]
|
||||
representations.set_version_ids(version_ids)
|
||||
|
||||
def _set_context(self, context, refresh=True):
|
||||
"""Set the selection in the interface using a context.
|
||||
The context must contain `asset` data by name.
|
||||
Note: Prior to setting context ensure `refresh` is triggered so that
|
||||
the "silos" are listed correctly, aside from that setting the
|
||||
context will force a refresh further down because it changes
|
||||
the active silo and asset.
|
||||
Args:
|
||||
context (dict): The context to apply.
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
asset = context.get("asset", None)
|
||||
if asset is None:
|
||||
return
|
||||
|
||||
if refresh:
|
||||
# Workaround:
|
||||
# Force a direct (non-scheduled) refresh prior to setting the
|
||||
# asset widget's silo and asset selection to ensure it's correctly
|
||||
# displaying the silo tabs. Calling `window.refresh()` and directly
|
||||
# `window.set_context()` the `set_context()` seems to override the
|
||||
# scheduled refresh and the silo tabs are not shown.
|
||||
self._refresh_assets()
|
||||
|
||||
asset_widget = self.data["widgets"]["assets"]
|
||||
asset_widget.select_assets(asset)
|
||||
|
||||
def echo(self, message):
|
||||
widget = self.data["label"]["message"]
|
||||
widget.setText(str(message))
|
||||
widget.show()
|
||||
print(message)
|
||||
|
||||
tools_lib.schedule(widget.hide, 5000, channel="message")
|
||||
|
||||
def closeEvent(self, event):
|
||||
# Kill on holding SHIFT
|
||||
modifiers = QtWidgets.QApplication.queryKeyboardModifiers()
|
||||
shift_pressed = QtCore.Qt.ShiftModifier & modifiers
|
||||
|
||||
if shift_pressed:
|
||||
print("Force quitted..")
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
|
||||
print("Good bye")
|
||||
return super(LibraryLoaderWindow, self).closeEvent(event)
|
||||
|
||||
|
||||
def show(
|
||||
debug=False, parent=None, icon=None,
|
||||
show_projects=False, show_libraries=True
|
||||
):
|
||||
"""Display Loader GUI
|
||||
|
||||
Arguments:
|
||||
debug (bool, optional): Run loader in debug-mode,
|
||||
defaults to False
|
||||
parent (QtCore.QObject, optional): The Qt object to parent to.
|
||||
use_context (bool): Whether to apply the current context upon launch
|
||||
|
||||
"""
|
||||
# Remember window
|
||||
if module.window is not None:
|
||||
try:
|
||||
module.window.show()
|
||||
|
||||
# If the window is minimized then unminimize it.
|
||||
if module.window.windowState() & QtCore.Qt.WindowMinimized:
|
||||
module.window.setWindowState(QtCore.Qt.WindowActive)
|
||||
|
||||
# Raise and activate the window
|
||||
module.window.raise_() # for MacOS
|
||||
module.window.activateWindow() # for Windows
|
||||
module.window.refresh()
|
||||
return
|
||||
except RuntimeError as e:
|
||||
if not e.message.rstrip().endswith("already deleted."):
|
||||
raise
|
||||
|
||||
# Garbage collected
|
||||
module.window = None
|
||||
|
||||
if debug:
|
||||
import traceback
|
||||
sys.excepthook = lambda typ, val, tb: traceback.print_last()
|
||||
|
||||
with tools_lib.application():
|
||||
window = LibraryLoaderWindow(
|
||||
parent, icon, show_projects, show_libraries
|
||||
)
|
||||
window.setStyleSheet(style.load_stylesheet())
|
||||
window.show()
|
||||
|
||||
module.window = window
|
||||
|
||||
|
||||
def cli(args):
|
||||
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("project")
|
||||
|
||||
show(show_projects=True, show_libraries=True)
|
||||
33
openpype/tools/libraryloader/lib.py
Normal file
33
openpype/tools/libraryloader/lib.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import os
|
||||
import importlib
|
||||
import logging
|
||||
from openpype.api import Anatomy
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# `find_config` from `pipeline`
|
||||
def find_config():
|
||||
log.info("Finding configuration for project..")
|
||||
|
||||
config = os.environ["AVALON_CONFIG"]
|
||||
|
||||
if not config:
|
||||
raise EnvironmentError(
|
||||
"No configuration found in "
|
||||
"the project nor environment"
|
||||
)
|
||||
|
||||
log.info("Found %s, loading.." % config)
|
||||
return importlib.import_module(config)
|
||||
|
||||
|
||||
class RegisteredRoots:
|
||||
roots_per_project = {}
|
||||
|
||||
@classmethod
|
||||
def registered_root(cls, project_name):
|
||||
if project_name not in cls.roots_per_project:
|
||||
cls.roots_per_project[project_name] = Anatomy(project_name).roots
|
||||
|
||||
return cls.roots_per_project[project_name]
|
||||
18
openpype/tools/libraryloader/widgets.py
Normal file
18
openpype/tools/libraryloader/widgets.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from Qt import QtWidgets
|
||||
|
||||
from .lib import RegisteredRoots
|
||||
from openpype.tools.loader.widgets import SubsetWidget
|
||||
|
||||
|
||||
class LibrarySubsetWidget(SubsetWidget):
|
||||
def on_copy_source(self):
|
||||
"""Copy formatted source path to clipboard"""
|
||||
source = self.data.get("source", None)
|
||||
if not source:
|
||||
return
|
||||
|
||||
project_name = self.dbcon.Session["AVALON_PROJECT"]
|
||||
root = RegisteredRoots.registered_root(project_name)
|
||||
path = source.format(root=root)
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
clipboard.setText(path)
|
||||
11
openpype/tools/loader/__init__.py
Normal file
11
openpype/tools/loader/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from .app import (
|
||||
LoaderWindow,
|
||||
show,
|
||||
cli,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"LoaderWindow",
|
||||
"show",
|
||||
"cli",
|
||||
)
|
||||
33
openpype/tools/loader/__main__.py
Normal file
33
openpype/tools/loader/__main__.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""Main entrypoint for standalone debugging
|
||||
|
||||
Used for running 'avalon.tool.loader.__main__' as a module (-m), useful for
|
||||
debugging without need to start host.
|
||||
|
||||
Modify AVALON_MONGO accordingly
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from . import cli
|
||||
|
||||
|
||||
def my_exception_hook(exctype, value, traceback):
|
||||
# Print the error and traceback
|
||||
print(exctype, value, traceback)
|
||||
# Call the normal Exception hook after
|
||||
sys._excepthook(exctype, value, traceback)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
os.environ["AVALON_MONGO"] = "mongodb://localhost:27017"
|
||||
os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:27017"
|
||||
os.environ["AVALON_DB"] = "avalon"
|
||||
os.environ["AVALON_TIMEOUT"] = "1000"
|
||||
os.environ["OPENPYPE_DEBUG"] = "1"
|
||||
os.environ["AVALON_CONFIG"] = "pype"
|
||||
os.environ["AVALON_ASSET"] = "Jungle"
|
||||
|
||||
# Set the exception hook to our wrapping function
|
||||
sys.excepthook = my_exception_hook
|
||||
|
||||
sys.exit(cli(sys.argv[1:]))
|
||||
674
openpype/tools/loader/app.py
Normal file
674
openpype/tools/loader/app.py
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
import sys
|
||||
import time
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
from avalon import api, io, style, pipeline
|
||||
|
||||
from openpype.tools.utils.widgets import AssetWidget
|
||||
|
||||
from openpype.tools.utils import lib
|
||||
|
||||
from .widgets import (
|
||||
SubsetWidget,
|
||||
VersionWidget,
|
||||
FamilyListWidget,
|
||||
ThumbnailWidget,
|
||||
RepresentationWidget,
|
||||
OverlayFrame
|
||||
)
|
||||
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
module = sys.modules[__name__]
|
||||
module.window = None
|
||||
|
||||
|
||||
# Register callback on task change
|
||||
# - callback can't be defined in Window as it is weak reference callback
|
||||
# so `WeakSet` will remove it immidiatelly
|
||||
def on_context_task_change(*args, **kwargs):
|
||||
if module.window:
|
||||
module.window.on_context_task_change(*args, **kwargs)
|
||||
|
||||
|
||||
pipeline.on("taskChanged", on_context_task_change)
|
||||
|
||||
|
||||
class LoaderWindow(QtWidgets.QDialog):
|
||||
"""Asset loader interface"""
|
||||
|
||||
tool_name = "loader"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(LoaderWindow, self).__init__(parent)
|
||||
title = "Asset Loader 2.1"
|
||||
project_name = api.Session.get("AVALON_PROJECT")
|
||||
if project_name:
|
||||
title += " - {}".format(project_name)
|
||||
self.setWindowTitle(title)
|
||||
|
||||
# Groups config
|
||||
self.groups_config = lib.GroupsConfig(io)
|
||||
self.family_config_cache = lib.FamilyConfigCache(io)
|
||||
|
||||
# Enable minimize and maximize for app
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
||||
body = QtWidgets.QWidget()
|
||||
footer = QtWidgets.QWidget()
|
||||
footer.setFixedHeight(20)
|
||||
|
||||
container = QtWidgets.QWidget()
|
||||
|
||||
assets = AssetWidget(io, multiselection=True, parent=self)
|
||||
assets.set_current_asset_btn_visibility(True)
|
||||
|
||||
families = FamilyListWidget(io, self.family_config_cache, self)
|
||||
subsets = SubsetWidget(
|
||||
io,
|
||||
self.groups_config,
|
||||
self.family_config_cache,
|
||||
tool_name=self.tool_name,
|
||||
parent=self
|
||||
)
|
||||
version = VersionWidget(io)
|
||||
thumbnail = ThumbnailWidget(io)
|
||||
representations = RepresentationWidget(io, self.tool_name)
|
||||
|
||||
manager = ModulesManager()
|
||||
sync_server = manager.modules_by_name["sync_server"]
|
||||
|
||||
thumb_ver_splitter = QtWidgets.QSplitter()
|
||||
thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
thumb_ver_splitter.addWidget(thumbnail)
|
||||
thumb_ver_splitter.addWidget(version)
|
||||
if sync_server.enabled:
|
||||
thumb_ver_splitter.addWidget(representations)
|
||||
thumb_ver_splitter.setStretchFactor(0, 30)
|
||||
thumb_ver_splitter.setStretchFactor(1, 35)
|
||||
|
||||
# Create splitter to show / hide family filters
|
||||
asset_filter_splitter = QtWidgets.QSplitter()
|
||||
asset_filter_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
asset_filter_splitter.addWidget(assets)
|
||||
asset_filter_splitter.addWidget(families)
|
||||
asset_filter_splitter.setStretchFactor(0, 65)
|
||||
asset_filter_splitter.setStretchFactor(1, 35)
|
||||
|
||||
container_layout = QtWidgets.QHBoxLayout(container)
|
||||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
split = QtWidgets.QSplitter()
|
||||
split.addWidget(asset_filter_splitter)
|
||||
split.addWidget(subsets)
|
||||
split.addWidget(thumb_ver_splitter)
|
||||
|
||||
container_layout.addWidget(split)
|
||||
|
||||
body_layout = QtWidgets.QHBoxLayout(body)
|
||||
body_layout.addWidget(container)
|
||||
body_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
message = QtWidgets.QLabel()
|
||||
message.hide()
|
||||
|
||||
footer_layout = QtWidgets.QVBoxLayout(footer)
|
||||
footer_layout.addWidget(message)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(body)
|
||||
layout.addWidget(footer)
|
||||
|
||||
self.data = {
|
||||
"widgets": {
|
||||
"families": families,
|
||||
"assets": assets,
|
||||
"subsets": subsets,
|
||||
"version": version,
|
||||
"thumbnail": thumbnail,
|
||||
"representations": representations
|
||||
},
|
||||
"label": {
|
||||
"message": message,
|
||||
},
|
||||
"state": {
|
||||
"assetIds": None
|
||||
}
|
||||
}
|
||||
|
||||
overlay_frame = OverlayFrame("Loading...", self)
|
||||
overlay_frame.setVisible(False)
|
||||
|
||||
families.active_changed.connect(subsets.set_family_filters)
|
||||
assets.selection_changed.connect(self.on_assetschanged)
|
||||
assets.refresh_triggered.connect(self.on_assetschanged)
|
||||
assets.view.clicked.connect(self.on_assetview_click)
|
||||
subsets.active_changed.connect(self.on_subsetschanged)
|
||||
subsets.version_changed.connect(self.on_versionschanged)
|
||||
|
||||
subsets.load_started.connect(self._on_load_start)
|
||||
subsets.load_ended.connect(self._on_load_end)
|
||||
representations.load_started.connect(self._on_load_start)
|
||||
representations.load_ended.connect(self._on_load_end)
|
||||
|
||||
self._overlay_frame = overlay_frame
|
||||
|
||||
self.family_config_cache.refresh()
|
||||
self.groups_config.refresh()
|
||||
|
||||
self._refresh()
|
||||
self._assetschanged()
|
||||
|
||||
# Defaults
|
||||
if sync_server.enabled:
|
||||
split.setSizes([250, 1000, 550])
|
||||
self.resize(1800, 900)
|
||||
else:
|
||||
split.setSizes([250, 850, 200])
|
||||
self.resize(1300, 700)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(LoaderWindow, self).resizeEvent(event)
|
||||
self._overlay_frame.resize(self.size())
|
||||
|
||||
def moveEvent(self, event):
|
||||
super(LoaderWindow, self).moveEvent(event)
|
||||
self._overlay_frame.move(0, 0)
|
||||
|
||||
# -------------------------------
|
||||
# Delay calling blocking methods
|
||||
# -------------------------------
|
||||
|
||||
def on_assetview_click(self, *args):
|
||||
subsets_widget = self.data["widgets"]["subsets"]
|
||||
selection_model = subsets_widget.view.selectionModel()
|
||||
if selection_model.selectedIndexes():
|
||||
selection_model.clearSelection()
|
||||
|
||||
def refresh(self):
|
||||
self.echo("Fetching results..")
|
||||
lib.schedule(self._refresh, 50, channel="mongo")
|
||||
|
||||
def on_assetschanged(self, *args):
|
||||
self.echo("Fetching asset..")
|
||||
lib.schedule(self._assetschanged, 50, channel="mongo")
|
||||
|
||||
def on_subsetschanged(self, *args):
|
||||
self.echo("Fetching subset..")
|
||||
lib.schedule(self._subsetschanged, 50, channel="mongo")
|
||||
|
||||
def on_versionschanged(self, *args):
|
||||
self.echo("Fetching version..")
|
||||
lib.schedule(self._versionschanged, 150, channel="mongo")
|
||||
|
||||
def set_context(self, context, refresh=True):
|
||||
self.echo("Setting context: {}".format(context))
|
||||
lib.schedule(lambda: self._set_context(context, refresh=refresh),
|
||||
50, channel="mongo")
|
||||
|
||||
def _on_load_start(self):
|
||||
# Show overlay and process events so it's repainted
|
||||
self._overlay_frame.setVisible(True)
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
def _hide_overlay(self):
|
||||
self._overlay_frame.setVisible(False)
|
||||
|
||||
def _on_load_end(self):
|
||||
# Delay hiding as click events happened during loading should be
|
||||
# blocked
|
||||
QtCore.QTimer.singleShot(100, self._hide_overlay)
|
||||
|
||||
# ------------------------------
|
||||
|
||||
def on_context_task_change(self, *args, **kwargs):
|
||||
# Change to context asset on context change
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
assets_widget.select_assets(io.Session["AVALON_ASSET"])
|
||||
|
||||
def _refresh(self):
|
||||
"""Load assets from database"""
|
||||
|
||||
# Ensure a project is loaded
|
||||
project = io.find_one({"type": "project"}, {"type": 1})
|
||||
assert project, "Project was not found! This is a bug"
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
assets_widget.refresh()
|
||||
assets_widget.setFocus()
|
||||
|
||||
families = self.data["widgets"]["families"]
|
||||
families.refresh()
|
||||
|
||||
def clear_assets_underlines(self):
|
||||
"""Clear colors from asset data to remove colored underlines
|
||||
When multiple assets are selected colored underlines mark which asset
|
||||
own selected subsets. These colors must be cleared from asset data
|
||||
on selection change so they match current selection.
|
||||
"""
|
||||
last_asset_ids = self.data["state"]["assetIds"]
|
||||
if not last_asset_ids:
|
||||
return
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
id_role = assets_widget.model.ObjectIdRole
|
||||
|
||||
for index in lib.iter_model_rows(assets_widget.model, 0):
|
||||
if index.data(id_role) not in last_asset_ids:
|
||||
continue
|
||||
|
||||
assets_widget.model.setData(
|
||||
index, [], assets_widget.model.subsetColorsRole
|
||||
)
|
||||
|
||||
def _assetschanged(self):
|
||||
"""Selected assets have changed"""
|
||||
t1 = time.time()
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
subsets_widget = self.data["widgets"]["subsets"]
|
||||
subsets_model = subsets_widget.model
|
||||
|
||||
subsets_model.clear()
|
||||
self.clear_assets_underlines()
|
||||
|
||||
# filter None docs they are silo
|
||||
asset_docs = assets_widget.get_selected_assets()
|
||||
|
||||
asset_ids = [asset_doc["_id"] for asset_doc in asset_docs]
|
||||
# Start loading
|
||||
subsets_widget.set_loading_state(
|
||||
loading=bool(asset_ids),
|
||||
empty=True
|
||||
)
|
||||
|
||||
def on_refreshed(has_item):
|
||||
empty = not has_item
|
||||
subsets_widget.set_loading_state(loading=False, empty=empty)
|
||||
subsets_model.refreshed.disconnect()
|
||||
self.echo("Duration: %.3fs" % (time.time() - t1))
|
||||
|
||||
subsets_model.refreshed.connect(on_refreshed)
|
||||
|
||||
subsets_model.set_assets(asset_ids)
|
||||
subsets_widget.view.setColumnHidden(
|
||||
subsets_model.Columns.index("asset"),
|
||||
len(asset_ids) < 2
|
||||
)
|
||||
|
||||
# Clear the version information on asset change
|
||||
self.data["widgets"]["version"].set_version(None)
|
||||
self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs)
|
||||
|
||||
self.data["state"]["assetIds"] = asset_ids
|
||||
|
||||
representations = self.data["widgets"]["representations"]
|
||||
representations.set_version_ids([]) # reset repre list
|
||||
|
||||
def _subsetschanged(self):
|
||||
asset_ids = self.data["state"]["assetIds"]
|
||||
# Skip setting colors if not asset multiselection
|
||||
if not asset_ids or len(asset_ids) < 2:
|
||||
self._versionschanged()
|
||||
return
|
||||
|
||||
subsets = self.data["widgets"]["subsets"]
|
||||
selected_subsets = subsets.selected_subsets(_merged=True, _other=False)
|
||||
|
||||
asset_models = {}
|
||||
asset_ids = []
|
||||
for subset_node in selected_subsets:
|
||||
asset_ids.extend(subset_node.get("assetIds", []))
|
||||
asset_ids = set(asset_ids)
|
||||
|
||||
for subset_node in selected_subsets:
|
||||
for asset_id in asset_ids:
|
||||
if asset_id not in asset_models:
|
||||
asset_models[asset_id] = []
|
||||
|
||||
color = None
|
||||
if asset_id in subset_node.get("assetIds", []):
|
||||
color = subset_node["subsetColor"]
|
||||
|
||||
asset_models[asset_id].append(color)
|
||||
|
||||
self.clear_assets_underlines()
|
||||
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
indexes = assets_widget.view.selectionModel().selectedRows()
|
||||
|
||||
for index in indexes:
|
||||
id = index.data(assets_widget.model.ObjectIdRole)
|
||||
if id not in asset_models:
|
||||
continue
|
||||
|
||||
assets_widget.model.setData(
|
||||
index, asset_models[id], assets_widget.model.subsetColorsRole
|
||||
)
|
||||
# Trigger repaint
|
||||
assets_widget.view.updateGeometries()
|
||||
# Set version in Version Widget
|
||||
self._versionschanged()
|
||||
|
||||
def _versionschanged(self):
|
||||
subsets = self.data["widgets"]["subsets"]
|
||||
selection = subsets.view.selectionModel()
|
||||
|
||||
# Active must be in the selected rows otherwise we
|
||||
# assume it's not actually an "active" current index.
|
||||
version_docs = None
|
||||
version_doc = None
|
||||
active = selection.currentIndex()
|
||||
rows = selection.selectedRows(column=active.column())
|
||||
if active:
|
||||
if active in rows:
|
||||
item = active.data(subsets.model.ItemRole)
|
||||
if (
|
||||
item is not None and
|
||||
not (item.get("isGroup") or item.get("isMerged"))
|
||||
):
|
||||
version_doc = item["version_document"]
|
||||
|
||||
if rows:
|
||||
version_docs = []
|
||||
for index in rows:
|
||||
if not index or not index.isValid():
|
||||
continue
|
||||
item = index.data(subsets.model.ItemRole)
|
||||
if item is None:
|
||||
continue
|
||||
if item.get("isGroup") or item.get("isMerged"):
|
||||
for child in item.children():
|
||||
version_docs.append(child["version_document"])
|
||||
else:
|
||||
version_docs.append(item["version_document"])
|
||||
|
||||
self.data["widgets"]["version"].set_version(version_doc)
|
||||
|
||||
thumbnail_docs = version_docs
|
||||
assets_widget = self.data["widgets"]["assets"]
|
||||
asset_docs = assets_widget.get_selected_assets()
|
||||
if not thumbnail_docs:
|
||||
if len(asset_docs) > 0:
|
||||
thumbnail_docs = asset_docs
|
||||
|
||||
self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs)
|
||||
|
||||
representations = self.data["widgets"]["representations"]
|
||||
version_ids = [doc["_id"] for doc in version_docs or []]
|
||||
representations.set_version_ids(version_ids)
|
||||
|
||||
# representations.change_visibility("subset", len(rows) > 1)
|
||||
# representations.change_visibility("asset", len(asset_docs) > 1)
|
||||
|
||||
def _set_context(self, context, refresh=True):
|
||||
"""Set the selection in the interface using a context.
|
||||
|
||||
The context must contain `asset` data by name.
|
||||
|
||||
Note: Prior to setting context ensure `refresh` is triggered so that
|
||||
the "silos" are listed correctly, aside from that setting the
|
||||
context will force a refresh further down because it changes
|
||||
the active silo and asset.
|
||||
|
||||
Args:
|
||||
context (dict): The context to apply.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
asset = context.get("asset", None)
|
||||
if asset is None:
|
||||
return
|
||||
|
||||
if refresh:
|
||||
# Workaround:
|
||||
# Force a direct (non-scheduled) refresh prior to setting the
|
||||
# asset widget's silo and asset selection to ensure it's correctly
|
||||
# displaying the silo tabs. Calling `window.refresh()` and directly
|
||||
# `window.set_context()` the `set_context()` seems to override the
|
||||
# scheduled refresh and the silo tabs are not shown.
|
||||
self._refresh()
|
||||
|
||||
asset_widget = self.data["widgets"]["assets"]
|
||||
asset_widget.select_assets(asset)
|
||||
|
||||
def echo(self, message):
|
||||
widget = self.data["label"]["message"]
|
||||
widget.setText(str(message))
|
||||
widget.show()
|
||||
print(message)
|
||||
|
||||
lib.schedule(widget.hide, 5000, channel="message")
|
||||
|
||||
def closeEvent(self, event):
|
||||
# Kill on holding SHIFT
|
||||
modifiers = QtWidgets.QApplication.queryKeyboardModifiers()
|
||||
shift_pressed = QtCore.Qt.ShiftModifier & modifiers
|
||||
|
||||
if shift_pressed:
|
||||
print("Force quitted..")
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
|
||||
print("Good bye")
|
||||
return super(LoaderWindow, self).closeEvent(event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
modifiers = event.modifiers()
|
||||
ctrl_pressed = QtCore.Qt.ControlModifier & modifiers
|
||||
|
||||
# Grouping subsets on pressing Ctrl + G
|
||||
if (ctrl_pressed and event.key() == QtCore.Qt.Key_G and
|
||||
not event.isAutoRepeat()):
|
||||
self.show_grouping_dialog()
|
||||
return
|
||||
|
||||
super(LoaderWindow, self).keyPressEvent(event)
|
||||
event.setAccepted(True) # Avoid interfering other widgets
|
||||
|
||||
def show_grouping_dialog(self):
|
||||
subsets = self.data["widgets"]["subsets"]
|
||||
if not subsets.is_groupable():
|
||||
self.echo("Grouping not enabled.")
|
||||
return
|
||||
|
||||
selected = []
|
||||
merged_items = []
|
||||
for item in subsets.selected_subsets(_merged=True):
|
||||
if item.get("isMerged"):
|
||||
merged_items.append(item)
|
||||
else:
|
||||
selected.append(item)
|
||||
|
||||
for merged_item in merged_items:
|
||||
for child_item in merged_item.children():
|
||||
selected.append(child_item)
|
||||
|
||||
if not selected:
|
||||
self.echo("No selected subset.")
|
||||
return
|
||||
|
||||
dialog = SubsetGroupingDialog(
|
||||
items=selected, groups_config=self.groups_config, parent=self
|
||||
)
|
||||
dialog.grouped.connect(self._assetschanged)
|
||||
dialog.show()
|
||||
|
||||
|
||||
class SubsetGroupingDialog(QtWidgets.QDialog):
|
||||
grouped = QtCore.Signal()
|
||||
|
||||
def __init__(self, items, groups_config, parent=None):
|
||||
super(SubsetGroupingDialog, self).__init__(parent=parent)
|
||||
self.setWindowTitle("Grouping Subsets")
|
||||
self.setMinimumWidth(250)
|
||||
self.setModal(True)
|
||||
|
||||
self.items = items
|
||||
self.groups_config = groups_config
|
||||
self.subsets = parent.data["widgets"]["subsets"]
|
||||
self.asset_ids = parent.data["state"]["assetIds"]
|
||||
|
||||
name = QtWidgets.QLineEdit()
|
||||
name.setPlaceholderText("Remain blank to ungroup..")
|
||||
|
||||
# Menu for pre-defined subset groups
|
||||
name_button = QtWidgets.QPushButton()
|
||||
name_button.setFixedWidth(18)
|
||||
name_button.setFixedHeight(20)
|
||||
name_menu = QtWidgets.QMenu(name_button)
|
||||
name_button.setMenu(name_menu)
|
||||
|
||||
name_layout = QtWidgets.QHBoxLayout()
|
||||
name_layout.addWidget(name)
|
||||
name_layout.addWidget(name_button)
|
||||
name_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
group_btn = QtWidgets.QPushButton("Apply")
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(QtWidgets.QLabel("Group Name"))
|
||||
layout.addLayout(name_layout)
|
||||
layout.addWidget(group_btn)
|
||||
|
||||
group_btn.clicked.connect(self.on_group)
|
||||
group_btn.setAutoDefault(True)
|
||||
group_btn.setDefault(True)
|
||||
|
||||
self.name = name
|
||||
self.name_menu = name_menu
|
||||
|
||||
self._build_menu()
|
||||
|
||||
def _build_menu(self):
|
||||
menu = self.name_menu
|
||||
button = menu.parent()
|
||||
# Get and destroy the action group
|
||||
group = button.findChild(QtWidgets.QActionGroup)
|
||||
if group:
|
||||
group.deleteLater()
|
||||
|
||||
active_groups = self.groups_config.active_groups(self.asset_ids)
|
||||
|
||||
# Build new action group
|
||||
group = QtWidgets.QActionGroup(button)
|
||||
group_names = list()
|
||||
for data in sorted(active_groups, key=lambda x: x["order"]):
|
||||
name = data["name"]
|
||||
if name in group_names:
|
||||
continue
|
||||
group_names.append(name)
|
||||
icon = data["icon"]
|
||||
|
||||
action = group.addAction(name)
|
||||
action.setIcon(icon)
|
||||
menu.addAction(action)
|
||||
|
||||
group.triggered.connect(self._on_action_clicked)
|
||||
button.setEnabled(not menu.isEmpty())
|
||||
|
||||
def _on_action_clicked(self, action):
|
||||
self.name.setText(action.text())
|
||||
|
||||
def on_group(self):
|
||||
name = self.name.text().strip()
|
||||
self.subsets.group_subsets(name, self.asset_ids, self.items)
|
||||
|
||||
with lib.preserve_selection(tree_view=self.subsets.view,
|
||||
current_index=False):
|
||||
self.grouped.emit()
|
||||
self.close()
|
||||
|
||||
|
||||
def show(debug=False, parent=None, use_context=False):
|
||||
"""Display Loader GUI
|
||||
|
||||
Arguments:
|
||||
debug (bool, optional): Run loader in debug-mode,
|
||||
defaults to False
|
||||
parent (QtCore.QObject, optional): The Qt object to parent to.
|
||||
use_context (bool): Whether to apply the current context upon launch
|
||||
|
||||
"""
|
||||
|
||||
# Remember window
|
||||
if module.window is not None:
|
||||
try:
|
||||
module.window.show()
|
||||
|
||||
# If the window is minimized then unminimize it.
|
||||
if module.window.windowState() & QtCore.Qt.WindowMinimized:
|
||||
module.window.setWindowState(QtCore.Qt.WindowActive)
|
||||
|
||||
# Raise and activate the window
|
||||
module.window.raise_() # for MacOS
|
||||
module.window.activateWindow() # for Windows
|
||||
module.window.refresh()
|
||||
return
|
||||
except (AttributeError, RuntimeError):
|
||||
# Garbage collected
|
||||
module.window = None
|
||||
|
||||
if debug:
|
||||
import traceback
|
||||
sys.excepthook = lambda typ, val, tb: traceback.print_last()
|
||||
|
||||
io.install()
|
||||
|
||||
any_project = next(
|
||||
project for project in io.projects()
|
||||
if project.get("active", True) is not False
|
||||
)
|
||||
|
||||
api.Session["AVALON_PROJECT"] = any_project["name"]
|
||||
module.project = any_project["name"]
|
||||
|
||||
with lib.application():
|
||||
window = LoaderWindow(parent)
|
||||
window.setStyleSheet(style.load_stylesheet())
|
||||
window.show()
|
||||
|
||||
if use_context:
|
||||
context = {"asset": api.Session["AVALON_ASSET"]}
|
||||
window.set_context(context, refresh=True)
|
||||
else:
|
||||
window.refresh()
|
||||
|
||||
module.window = window
|
||||
|
||||
# Pull window to the front.
|
||||
module.window.raise_()
|
||||
module.window.activateWindow()
|
||||
|
||||
|
||||
def cli(args):
|
||||
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("project")
|
||||
|
||||
args = parser.parse_args(args)
|
||||
project = args.project
|
||||
|
||||
print("Entering Project: %s" % project)
|
||||
|
||||
io.install()
|
||||
|
||||
# Store settings
|
||||
api.Session["AVALON_PROJECT"] = project
|
||||
|
||||
from avalon import pipeline
|
||||
|
||||
# Find the set config
|
||||
_config = pipeline.find_config()
|
||||
if hasattr(_config, "install"):
|
||||
_config.install()
|
||||
else:
|
||||
print("Config `%s` has no function `install`" %
|
||||
_config.__name__)
|
||||
|
||||
show()
|
||||
BIN
openpype/tools/loader/images/default_thumbnail.png
Normal file
BIN
openpype/tools/loader/images/default_thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
190
openpype/tools/loader/lib.py
Normal file
190
openpype/tools/loader/lib.py
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import inspect
|
||||
from Qt import QtGui
|
||||
|
||||
from avalon.vendor import qtawesome
|
||||
from openpype.tools.utils.widgets import (
|
||||
OptionalAction,
|
||||
OptionDialog
|
||||
)
|
||||
|
||||
|
||||
def change_visibility(model, view, column_name, visible):
|
||||
"""
|
||||
Hides or shows particular 'column_name'.
|
||||
|
||||
"asset" and "subset" columns should be visible only in multiselect
|
||||
"""
|
||||
index = model.Columns.index(column_name)
|
||||
view.setColumnHidden(index, not visible)
|
||||
|
||||
|
||||
def get_selected_items(rows, item_role):
|
||||
items = []
|
||||
for row_index in rows:
|
||||
item = row_index.data(item_role)
|
||||
if item.get("isGroup"):
|
||||
continue
|
||||
|
||||
elif item.get("isMerged"):
|
||||
for idx in range(row_index.model().rowCount(row_index)):
|
||||
child_index = row_index.child(idx, 0)
|
||||
item = child_index.data(item_role)
|
||||
if item not in items:
|
||||
items.append(item)
|
||||
|
||||
else:
|
||||
if item not in items:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def get_options(action, loader, parent, repre_contexts):
|
||||
"""Provides dialog to select value from loader provided options.
|
||||
|
||||
Loader can provide static or dynamically created options based on
|
||||
qargparse variants.
|
||||
|
||||
Args:
|
||||
action (OptionalAction) - action in menu
|
||||
loader (cls of api.Loader) - not initilized yet
|
||||
parent (Qt element to parent dialog to)
|
||||
repre_contexts (list) of dict with full info about selected repres
|
||||
Returns:
|
||||
(dict) - selected value from OptionDialog
|
||||
None when dialog was closed or cancelled, in all other cases {}
|
||||
if no options
|
||||
"""
|
||||
# Pop option dialog
|
||||
options = {}
|
||||
loader_options = loader.get_options(repre_contexts)
|
||||
if getattr(action, "optioned", False) and loader_options:
|
||||
dialog = OptionDialog(parent)
|
||||
dialog.setWindowTitle(action.label + " Options")
|
||||
dialog.create(loader_options)
|
||||
|
||||
if not dialog.exec_():
|
||||
return None
|
||||
|
||||
# Get option
|
||||
options = dialog.parse()
|
||||
|
||||
return options
|
||||
|
||||
|
||||
def add_representation_loaders_to_menu(loaders, menu, repre_contexts):
|
||||
"""
|
||||
Loops through provider loaders and adds them to 'menu'.
|
||||
|
||||
Expects loaders sorted in requested order.
|
||||
Expects loaders de-duplicated if wanted.
|
||||
|
||||
Args:
|
||||
loaders(tuple): representation - loader
|
||||
menu (OptionalMenu):
|
||||
repre_contexts (dict): full info about representations (contains
|
||||
their repre_doc, asset_doc, subset_doc, version_doc),
|
||||
keys are repre_ids
|
||||
|
||||
Returns:
|
||||
menu (OptionalMenu): with new items
|
||||
"""
|
||||
# List the available loaders
|
||||
for representation, loader in loaders:
|
||||
label = None
|
||||
repre_context = None
|
||||
if representation:
|
||||
label = representation.get("custom_label")
|
||||
repre_context = repre_contexts[representation["_id"]]
|
||||
|
||||
if not label:
|
||||
label = get_label_from_loader(loader, representation)
|
||||
|
||||
icon = get_icon_from_loader(loader)
|
||||
|
||||
loader_options = loader.get_options([repre_context])
|
||||
|
||||
use_option = bool(loader_options)
|
||||
action = OptionalAction(label, icon, use_option, menu)
|
||||
if use_option:
|
||||
# Add option box tip
|
||||
action.set_option_tip(loader_options)
|
||||
|
||||
action.setData((representation, loader))
|
||||
|
||||
# Add tooltip and statustip from Loader docstring
|
||||
tip = inspect.getdoc(loader)
|
||||
if tip:
|
||||
action.setToolTip(tip)
|
||||
action.setStatusTip(tip)
|
||||
|
||||
menu.addAction(action)
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
def remove_tool_name_from_loaders(available_loaders, tool_name):
|
||||
if not tool_name:
|
||||
return available_loaders
|
||||
filtered_loaders = []
|
||||
for loader in available_loaders:
|
||||
if hasattr(loader, "tool_names"):
|
||||
if not ("*" in loader.tool_names or
|
||||
tool_name in loader.tool_names):
|
||||
continue
|
||||
filtered_loaders.append(loader)
|
||||
return filtered_loaders
|
||||
|
||||
|
||||
def get_icon_from_loader(loader):
|
||||
"""Pull icon info from loader class"""
|
||||
# Support font-awesome icons using the `.icon` and `.color`
|
||||
# attributes on plug-ins.
|
||||
icon = getattr(loader, "icon", None)
|
||||
if icon is not None:
|
||||
try:
|
||||
key = "fa.{0}".format(icon)
|
||||
color = getattr(loader, "color", "white")
|
||||
icon = qtawesome.icon(key, color=color)
|
||||
except Exception as e:
|
||||
print("Unable to set icon for loader "
|
||||
"{}: {}".format(loader, e))
|
||||
icon = None
|
||||
return icon
|
||||
|
||||
|
||||
def get_label_from_loader(loader, representation=None):
|
||||
"""Pull label info from loader class"""
|
||||
label = getattr(loader, "label", None)
|
||||
if label is None:
|
||||
label = loader.__name__
|
||||
if representation:
|
||||
# Add the representation as suffix
|
||||
label = "{0} ({1})".format(label, representation['name'])
|
||||
return label
|
||||
|
||||
|
||||
def get_no_loader_action(menu, one_item_selected=False):
|
||||
"""Creates dummy no loader option in 'menu'"""
|
||||
submsg = "your selection."
|
||||
if one_item_selected:
|
||||
submsg = "this version."
|
||||
msg = "No compatible loaders for {}".format(submsg)
|
||||
print(msg)
|
||||
icon = qtawesome.icon(
|
||||
"fa.exclamation",
|
||||
color=QtGui.QColor(255, 51, 0)
|
||||
)
|
||||
action = OptionalAction(("*" + msg), icon, False, menu)
|
||||
return action
|
||||
|
||||
|
||||
def sort_loaders(loaders, custom_sorter=None):
|
||||
def sorter(value):
|
||||
"""Sort the Loaders by their order and then their name"""
|
||||
Plugin = value[1]
|
||||
return Plugin.order, Plugin.__name__
|
||||
|
||||
if not custom_sorter:
|
||||
custom_sorter = sorter
|
||||
|
||||
return sorted(loaders, key=custom_sorter)
|
||||
1191
openpype/tools/loader/model.py
Normal file
1191
openpype/tools/loader/model.py
Normal file
File diff suppressed because it is too large
Load diff
1458
openpype/tools/loader/widgets.py
Normal file
1458
openpype/tools/loader/widgets.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -29,7 +29,7 @@ class ProjectManagerWindow(QtWidgets.QWidget):
|
|||
self._user_passed = False
|
||||
|
||||
self.setWindowTitle("OpenPype Project Manager")
|
||||
self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath()))
|
||||
self.setWindowIcon(QtGui.QIcon(resources.get_openpype_icon_filepath()))
|
||||
|
||||
# Top part of window
|
||||
top_part_widget = QtWidgets.QWidget(self)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import json
|
|||
from Qt import QtWidgets, QtGui, QtCore
|
||||
from openpype.tools.settings import CHILD_OFFSET
|
||||
from .widgets import ExpandingWidget
|
||||
from .lib import create_deffered_value_change_timer
|
||||
|
||||
|
||||
class BaseWidget(QtWidgets.QWidget):
|
||||
|
|
@ -329,6 +330,20 @@ class BaseWidget(QtWidgets.QWidget):
|
|||
|
||||
|
||||
class InputWidget(BaseWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InputWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
# Input widgets have always timer available (but may not be used).
|
||||
self._value_change_timer = create_deffered_value_change_timer(
|
||||
self._on_value_change_timer
|
||||
)
|
||||
|
||||
def start_value_timer(self):
|
||||
self._value_change_timer.start()
|
||||
|
||||
def _on_value_change_timer(self):
|
||||
pass
|
||||
|
||||
def create_ui(self):
|
||||
if self.entity.use_label_wrap:
|
||||
label = None
|
||||
|
|
|
|||
|
|
@ -609,14 +609,23 @@ class ProjectWidget(SettingsCategoryWidget):
|
|||
self.project_list_widget.refresh()
|
||||
|
||||
def _on_reset_crash(self):
|
||||
self.project_list_widget.setEnabled(False)
|
||||
self._set_enabled_project_list(False)
|
||||
super(ProjectWidget, self)._on_reset_crash()
|
||||
|
||||
def _on_reset_success(self):
|
||||
if not self.project_list_widget.isEnabled():
|
||||
self.project_list_widget.setEnabled(True)
|
||||
self._set_enabled_project_list(True)
|
||||
super(ProjectWidget, self)._on_reset_success()
|
||||
|
||||
def _set_enabled_project_list(self, enabled):
|
||||
if (
|
||||
enabled
|
||||
and self.modify_defaults_checkbox
|
||||
and self.modify_defaults_checkbox.isChecked()
|
||||
):
|
||||
enabled = False
|
||||
if self.project_list_widget.isEnabled() != enabled:
|
||||
self.project_list_widget.setEnabled(enabled)
|
||||
|
||||
def _create_root_entity(self):
|
||||
self.entity = ProjectSettings(change_state=False)
|
||||
self.entity.on_change_callbacks.append(self._on_entity_change)
|
||||
|
|
@ -637,7 +646,8 @@ class ProjectWidget(SettingsCategoryWidget):
|
|||
|
||||
if self.modify_defaults_checkbox:
|
||||
self.modify_defaults_checkbox.setEnabled(True)
|
||||
self.project_list_widget.setEnabled(True)
|
||||
|
||||
self._set_enabled_project_list(True)
|
||||
|
||||
except DefaultsNotDefined:
|
||||
if not self.modify_defaults_checkbox:
|
||||
|
|
@ -646,7 +656,7 @@ class ProjectWidget(SettingsCategoryWidget):
|
|||
self.entity.set_defaults_state()
|
||||
self.modify_defaults_checkbox.setChecked(True)
|
||||
self.modify_defaults_checkbox.setEnabled(False)
|
||||
self.project_list_widget.setEnabled(False)
|
||||
self._set_enabled_project_list(False)
|
||||
|
||||
except StudioDefaultsNotDefined:
|
||||
self.select_default_project()
|
||||
|
|
@ -666,8 +676,10 @@ class ProjectWidget(SettingsCategoryWidget):
|
|||
|
||||
def _on_modify_defaults(self):
|
||||
if self.modify_defaults_checkbox.isChecked():
|
||||
self._set_enabled_project_list(False)
|
||||
if not self.entity.is_in_defaults_state():
|
||||
self.reset()
|
||||
else:
|
||||
self._set_enabled_project_list(True)
|
||||
if not self.entity.is_in_studio_state():
|
||||
self.reset()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from uuid import uuid4
|
|||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from .base import BaseWidget
|
||||
from .lib import create_deffered_value_change_timer
|
||||
from .widgets import (
|
||||
ExpandingWidget,
|
||||
IconButton
|
||||
|
|
@ -284,6 +285,10 @@ class ModifiableDictItem(QtWidgets.QWidget):
|
|||
|
||||
self.confirm_btn = None
|
||||
|
||||
self._key_change_timer = create_deffered_value_change_timer(
|
||||
self._on_timeout
|
||||
)
|
||||
|
||||
if collapsible_key:
|
||||
self.create_collapsible_ui()
|
||||
else:
|
||||
|
|
@ -516,6 +521,10 @@ class ModifiableDictItem(QtWidgets.QWidget):
|
|||
if self.ignore_input_changes:
|
||||
return
|
||||
|
||||
self._key_change_timer.start()
|
||||
|
||||
def _on_timeout(self):
|
||||
key = self.key_value()
|
||||
is_key_duplicated = self.entity_widget.validate_key_duplication(
|
||||
self.temp_key, key, self
|
||||
)
|
||||
|
|
|
|||
|
|
@ -400,7 +400,9 @@ class TextWidget(InputWidget):
|
|||
def _on_value_change(self):
|
||||
if self.ignore_input_changes:
|
||||
return
|
||||
self.start_value_timer()
|
||||
|
||||
def _on_value_change_timer(self):
|
||||
self.entity.set(self.input_value())
|
||||
|
||||
|
||||
|
|
@ -474,6 +476,9 @@ class NumberWidget(InputWidget):
|
|||
if self.ignore_input_changes:
|
||||
return
|
||||
|
||||
self.start_value_timer()
|
||||
|
||||
def _on_value_change_timer(self):
|
||||
value = self.input_field.value()
|
||||
if self._slider_widget is not None and not self._ignore_input_change:
|
||||
self._ignore_slider_change = True
|
||||
|
|
@ -571,7 +576,9 @@ class RawJsonWidget(InputWidget):
|
|||
def _on_value_change(self):
|
||||
if self.ignore_input_changes:
|
||||
return
|
||||
self.start_value_timer()
|
||||
|
||||
def _on_value_change_timer(self):
|
||||
self._is_invalid = self.input_field.has_invalid_value()
|
||||
if not self.is_invalid:
|
||||
self.entity.set(self.input_field.json_value())
|
||||
|
|
@ -786,4 +793,7 @@ class PathInputWidget(InputWidget):
|
|||
def _on_value_change(self):
|
||||
if self.ignore_input_changes:
|
||||
return
|
||||
self.start_value_timer()
|
||||
|
||||
def _on_value_change_timer(self):
|
||||
self.entity.set(self.input_value())
|
||||
|
|
|
|||
18
openpype/tools/settings/settings/lib.py
Normal file
18
openpype/tools/settings/settings/lib.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from Qt import QtCore
|
||||
|
||||
# Offset of value change trigger in ms
|
||||
VALUE_CHANGE_OFFSET_MS = 300
|
||||
|
||||
|
||||
def create_deffered_value_change_timer(callback):
|
||||
"""Deffer value change callback.
|
||||
|
||||
UI won't trigger all callbacks on each value change but after predefined
|
||||
time. Timer is reset on each start so callback is triggered after user
|
||||
finish editing.
|
||||
"""
|
||||
timer = QtCore.QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.setInterval(VALUE_CHANGE_OFFSET_MS)
|
||||
timer.timeout.connect(callback)
|
||||
return timer
|
||||
|
|
@ -10,4 +10,4 @@ def load_stylesheet():
|
|||
|
||||
|
||||
def app_icon_path():
|
||||
return resources.pype_icon_filepath()
|
||||
return resources.get_openpype_icon_filepath()
|
||||
|
|
|
|||
|
|
@ -146,6 +146,15 @@ QSlider::handle:vertical {
|
|||
border: 1px solid #464b54;
|
||||
background: #21252B;
|
||||
}
|
||||
|
||||
#ProjectListWidget QListView:disabled {
|
||||
background: #282C34;
|
||||
}
|
||||
|
||||
#ProjectListWidget QListView::item:disabled {
|
||||
color: #4e5254;
|
||||
}
|
||||
|
||||
#ProjectListWidget QLabel {
|
||||
background: transparent;
|
||||
font-weight: bold;
|
||||
|
|
@ -249,8 +258,6 @@ QTabBar::tab:!selected:hover {
|
|||
background: #333840;
|
||||
}
|
||||
|
||||
|
||||
|
||||
QTabBar::tab:first:selected {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
|
@ -405,12 +412,15 @@ QHeaderView::section {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed {
|
||||
QAbstractItemView::item:pressed {
|
||||
background: #78879b;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active {
|
||||
QAbstractItemView::item:selected:active {
|
||||
background: #3d8ec9;
|
||||
}
|
||||
QAbstractItemView::item:selected:!active {
|
||||
background: #3d8ec9;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ def main():
|
|||
qt_app = QtWidgets.QApplication([])
|
||||
# app.setQuitOnLastWindowClosed(False)
|
||||
qt_app.setStyleSheet(style.load_stylesheet())
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
qt_app.setWindowIcon(icon)
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ class PypeInfoWidget(QtWidgets.QWidget):
|
|||
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowTitle("OpenPype info")
|
||||
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
|
|||
doubleclick_time_ms = 100
|
||||
|
||||
def __init__(self, parent):
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
|
||||
super(SystemTrayIcon, self).__init__(icon, parent)
|
||||
|
||||
|
|
@ -308,7 +308,7 @@ class PypeTrayApplication(QtWidgets.QApplication):
|
|||
splash_widget.hide()
|
||||
|
||||
def set_splash(self):
|
||||
splash_pix = QtGui.QPixmap(resources.pype_splash_filepath())
|
||||
splash_pix = QtGui.QPixmap(resources.get_openpype_splash_filepath())
|
||||
splash = QtWidgets.QSplashScreen(splash_pix)
|
||||
splash.setMask(splash_pix.mask())
|
||||
splash.setEnabled(False)
|
||||
|
|
|
|||
0
openpype/tools/utils/__init__.py
Normal file
0
openpype/tools/utils/__init__.py
Normal file
449
openpype/tools/utils/delegates.py
Normal file
449
openpype/tools/utils/delegates.py
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
import time
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import numbers
|
||||
|
||||
import Qt
|
||||
from Qt import QtWidgets, QtGui, QtCore
|
||||
|
||||
from avalon.lib import HeroVersionType
|
||||
from .models import (
|
||||
AssetModel,
|
||||
TreeModel
|
||||
)
|
||||
from . import lib
|
||||
|
||||
if Qt.__binding__ == "PySide":
|
||||
from PySide.QtGui import QStyleOptionViewItemV4
|
||||
elif Qt.__binding__ == "PyQt4":
|
||||
from PyQt4.QtGui import QStyleOptionViewItemV4
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AssetDelegate(QtWidgets.QItemDelegate):
|
||||
bar_height = 3
|
||||
|
||||
def sizeHint(self, option, index):
|
||||
result = super(AssetDelegate, self).sizeHint(option, index)
|
||||
height = result.height()
|
||||
result.setHeight(height + self.bar_height)
|
||||
|
||||
return result
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
# Qt4 compat
|
||||
if Qt.__binding__ in ("PySide", "PyQt4"):
|
||||
option = QStyleOptionViewItemV4(option)
|
||||
|
||||
painter.save()
|
||||
|
||||
item_rect = QtCore.QRect(option.rect)
|
||||
item_rect.setHeight(option.rect.height() - self.bar_height)
|
||||
|
||||
subset_colors = index.data(AssetModel.subsetColorsRole)
|
||||
subset_colors_width = 0
|
||||
if subset_colors:
|
||||
subset_colors_width = option.rect.width() / len(subset_colors)
|
||||
|
||||
subset_rects = []
|
||||
counter = 0
|
||||
for subset_c in subset_colors:
|
||||
new_color = None
|
||||
new_rect = None
|
||||
if subset_c:
|
||||
new_color = QtGui.QColor(*subset_c)
|
||||
|
||||
new_rect = QtCore.QRect(
|
||||
option.rect.left() + (counter * subset_colors_width),
|
||||
option.rect.top() + (
|
||||
option.rect.height() - self.bar_height
|
||||
),
|
||||
subset_colors_width,
|
||||
self.bar_height
|
||||
)
|
||||
subset_rects.append((new_color, new_rect))
|
||||
counter += 1
|
||||
|
||||
# Background
|
||||
bg_color = QtGui.QColor(60, 60, 60)
|
||||
if option.state & QtWidgets.QStyle.State_Selected:
|
||||
if len(subset_colors) == 0:
|
||||
item_rect.setTop(item_rect.top() + (self.bar_height / 2))
|
||||
if option.state & QtWidgets.QStyle.State_MouseOver:
|
||||
bg_color.setRgb(70, 70, 70)
|
||||
else:
|
||||
item_rect.setTop(item_rect.top() + (self.bar_height / 2))
|
||||
if option.state & QtWidgets.QStyle.State_MouseOver:
|
||||
bg_color.setAlpha(100)
|
||||
else:
|
||||
bg_color.setAlpha(0)
|
||||
|
||||
# When not needed to do a rounded corners (easier and without
|
||||
# painter restore):
|
||||
# painter.fillRect(
|
||||
# item_rect,
|
||||
# QtGui.QBrush(bg_color)
|
||||
# )
|
||||
pen = painter.pen()
|
||||
pen.setStyle(QtCore.Qt.NoPen)
|
||||
pen.setWidth(0)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(QtGui.QBrush(bg_color))
|
||||
painter.drawRoundedRect(option.rect, 3, 3)
|
||||
|
||||
if option.state & QtWidgets.QStyle.State_Selected:
|
||||
for color, subset_rect in subset_rects:
|
||||
if not color or not subset_rect:
|
||||
continue
|
||||
painter.fillRect(subset_rect, QtGui.QBrush(color))
|
||||
|
||||
painter.restore()
|
||||
painter.save()
|
||||
|
||||
# Icon
|
||||
icon_index = index.model().index(
|
||||
index.row(), index.column(), index.parent()
|
||||
)
|
||||
# - Default icon_rect if not icon
|
||||
icon_rect = QtCore.QRect(
|
||||
item_rect.left(),
|
||||
item_rect.top(),
|
||||
# To make sure it's same size all the time
|
||||
option.rect.height() - self.bar_height,
|
||||
option.rect.height() - self.bar_height
|
||||
)
|
||||
icon = index.model().data(icon_index, QtCore.Qt.DecorationRole)
|
||||
|
||||
if icon:
|
||||
mode = QtGui.QIcon.Normal
|
||||
if not (option.state & QtWidgets.QStyle.State_Enabled):
|
||||
mode = QtGui.QIcon.Disabled
|
||||
elif option.state & QtWidgets.QStyle.State_Selected:
|
||||
mode = QtGui.QIcon.Selected
|
||||
|
||||
if isinstance(icon, QtGui.QPixmap):
|
||||
icon = QtGui.QIcon(icon)
|
||||
option.decorationSize = icon.size() / icon.devicePixelRatio()
|
||||
|
||||
elif isinstance(icon, QtGui.QColor):
|
||||
pixmap = QtGui.QPixmap(option.decorationSize)
|
||||
pixmap.fill(icon)
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
|
||||
elif isinstance(icon, QtGui.QImage):
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(icon))
|
||||
option.decorationSize = icon.size() / icon.devicePixelRatio()
|
||||
|
||||
elif isinstance(icon, QtGui.QIcon):
|
||||
state = QtGui.QIcon.Off
|
||||
if option.state & QtWidgets.QStyle.State_Open:
|
||||
state = QtGui.QIcon.On
|
||||
actualSize = option.icon.actualSize(
|
||||
option.decorationSize, mode, state
|
||||
)
|
||||
option.decorationSize = QtCore.QSize(
|
||||
min(option.decorationSize.width(), actualSize.width()),
|
||||
min(option.decorationSize.height(), actualSize.height())
|
||||
)
|
||||
|
||||
state = QtGui.QIcon.Off
|
||||
if option.state & QtWidgets.QStyle.State_Open:
|
||||
state = QtGui.QIcon.On
|
||||
|
||||
icon.paint(
|
||||
painter, icon_rect,
|
||||
QtCore.Qt.AlignLeft, mode, state
|
||||
)
|
||||
|
||||
# Text
|
||||
text_rect = QtCore.QRect(
|
||||
icon_rect.left() + icon_rect.width() + 2,
|
||||
item_rect.top(),
|
||||
item_rect.width(),
|
||||
item_rect.height()
|
||||
)
|
||||
|
||||
painter.drawText(
|
||||
text_rect, QtCore.Qt.AlignVCenter,
|
||||
index.data(QtCore.Qt.DisplayRole)
|
||||
)
|
||||
|
||||
painter.restore()
|
||||
|
||||
|
||||
class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""A delegate that display version integer formatted as version string."""
|
||||
|
||||
version_changed = QtCore.Signal()
|
||||
first_run = False
|
||||
lock = False
|
||||
|
||||
def __init__(self, dbcon, *args, **kwargs):
|
||||
self.dbcon = dbcon
|
||||
super(VersionDelegate, self).__init__(*args, **kwargs)
|
||||
|
||||
def displayText(self, value, locale):
|
||||
if isinstance(value, HeroVersionType):
|
||||
return lib.format_version(value, True)
|
||||
assert isinstance(value, numbers.Integral), (
|
||||
"Version is not integer. \"{}\" {}".format(value, str(type(value)))
|
||||
)
|
||||
return lib.format_version(value)
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
fg_color = index.data(QtCore.Qt.ForegroundRole)
|
||||
if fg_color:
|
||||
if isinstance(fg_color, QtGui.QBrush):
|
||||
fg_color = fg_color.color()
|
||||
elif isinstance(fg_color, QtGui.QColor):
|
||||
pass
|
||||
else:
|
||||
fg_color = None
|
||||
|
||||
if not fg_color:
|
||||
return super(VersionDelegate, self).paint(painter, option, index)
|
||||
|
||||
if option.widget:
|
||||
style = option.widget.style()
|
||||
else:
|
||||
style = QtWidgets.QApplication.style()
|
||||
|
||||
style.drawControl(
|
||||
style.CE_ItemViewItem, option, painter, option.widget
|
||||
)
|
||||
|
||||
painter.save()
|
||||
|
||||
text = self.displayText(
|
||||
index.data(QtCore.Qt.DisplayRole), option.locale
|
||||
)
|
||||
pen = painter.pen()
|
||||
pen.setColor(fg_color)
|
||||
painter.setPen(pen)
|
||||
|
||||
text_rect = style.subElementRect(style.SE_ItemViewItemText, option)
|
||||
text_margin = style.proxy().pixelMetric(
|
||||
style.PM_FocusFrameHMargin, option, option.widget
|
||||
) + 1
|
||||
|
||||
painter.drawText(
|
||||
text_rect.adjusted(text_margin, 0, - text_margin, 0),
|
||||
option.displayAlignment,
|
||||
text
|
||||
)
|
||||
|
||||
painter.restore()
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
item = index.data(TreeModel.ItemRole)
|
||||
if item.get("isGroup") or item.get("isMerged"):
|
||||
return
|
||||
|
||||
editor = QtWidgets.QComboBox(parent)
|
||||
|
||||
def commit_data():
|
||||
if not self.first_run:
|
||||
self.commitData.emit(editor) # Update model data
|
||||
self.version_changed.emit() # Display model data
|
||||
editor.currentIndexChanged.connect(commit_data)
|
||||
|
||||
self.first_run = True
|
||||
self.lock = False
|
||||
|
||||
return editor
|
||||
|
||||
def setEditorData(self, editor, index):
|
||||
if self.lock:
|
||||
# Only set editor data once per delegation
|
||||
return
|
||||
|
||||
editor.clear()
|
||||
|
||||
# Current value of the index
|
||||
item = index.data(TreeModel.ItemRole)
|
||||
value = index.data(QtCore.Qt.DisplayRole)
|
||||
if item["version_document"]["type"] != "hero_version":
|
||||
assert isinstance(value, numbers.Integral), (
|
||||
"Version is not integer"
|
||||
)
|
||||
|
||||
# Add all available versions to the editor
|
||||
parent_id = item["version_document"]["parent"]
|
||||
version_docs = list(self.dbcon.find(
|
||||
{
|
||||
"type": "version",
|
||||
"parent": parent_id
|
||||
},
|
||||
sort=[("name", 1)]
|
||||
))
|
||||
|
||||
hero_version_doc = self.dbcon.find_one(
|
||||
{
|
||||
"type": "hero_version",
|
||||
"parent": parent_id
|
||||
}, {
|
||||
"name": 1,
|
||||
"data.tags": 1,
|
||||
"version_id": 1
|
||||
}
|
||||
)
|
||||
|
||||
doc_for_hero_version = None
|
||||
|
||||
selected = None
|
||||
items = []
|
||||
for version_doc in version_docs:
|
||||
version_tags = version_doc["data"].get("tags") or []
|
||||
if "deleted" in version_tags:
|
||||
continue
|
||||
|
||||
if (
|
||||
hero_version_doc
|
||||
and doc_for_hero_version is None
|
||||
and hero_version_doc["version_id"] == version_doc["_id"]
|
||||
):
|
||||
doc_for_hero_version = version_doc
|
||||
|
||||
label = lib.format_version(version_doc["name"])
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setData(version_doc, QtCore.Qt.UserRole)
|
||||
items.append(item)
|
||||
|
||||
if version_doc["name"] == value:
|
||||
selected = item
|
||||
|
||||
if hero_version_doc and doc_for_hero_version:
|
||||
version_name = doc_for_hero_version["name"]
|
||||
label = lib.format_version(version_name, True)
|
||||
if isinstance(value, HeroVersionType):
|
||||
index = len(version_docs)
|
||||
hero_version_doc["name"] = HeroVersionType(version_name)
|
||||
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setData(hero_version_doc, QtCore.Qt.UserRole)
|
||||
items.append(item)
|
||||
|
||||
# Reverse items so latest versions be upper
|
||||
items = list(reversed(items))
|
||||
for item in items:
|
||||
editor.model().appendRow(item)
|
||||
|
||||
index = 0
|
||||
if selected:
|
||||
index = selected.row()
|
||||
|
||||
# Will trigger index-change signal
|
||||
editor.setCurrentIndex(index)
|
||||
self.first_run = False
|
||||
self.lock = True
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
"""Apply the integer version back in the model"""
|
||||
version = editor.itemData(editor.currentIndex())
|
||||
model.setData(index, version["name"])
|
||||
|
||||
|
||||
def pretty_date(t, now=None, strftime="%b %d %Y %H:%M"):
|
||||
"""Parse datetime to readable timestamp
|
||||
|
||||
Within first ten seconds:
|
||||
- "just now",
|
||||
Within first minute ago:
|
||||
- "%S seconds ago"
|
||||
Within one hour ago:
|
||||
- "%M minutes ago".
|
||||
Within one day ago:
|
||||
- "%H:%M hours ago"
|
||||
Else:
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
|
||||
"""
|
||||
|
||||
assert isinstance(t, datetime)
|
||||
if now is None:
|
||||
now = datetime.now()
|
||||
assert isinstance(now, datetime)
|
||||
diff = now - t
|
||||
|
||||
second_diff = diff.seconds
|
||||
day_diff = diff.days
|
||||
|
||||
# future (consider as just now)
|
||||
if day_diff < 0:
|
||||
return "just now"
|
||||
|
||||
# history
|
||||
if day_diff == 0:
|
||||
if second_diff < 10:
|
||||
return "just now"
|
||||
if second_diff < 60:
|
||||
return str(second_diff) + " seconds ago"
|
||||
if second_diff < 120:
|
||||
return "a minute ago"
|
||||
if second_diff < 3600:
|
||||
return str(second_diff // 60) + " minutes ago"
|
||||
if second_diff < 86400:
|
||||
minutes = (second_diff % 3600) // 60
|
||||
hours = second_diff // 3600
|
||||
return "{0}:{1:02d} hours ago".format(hours, minutes)
|
||||
|
||||
return t.strftime(strftime)
|
||||
|
||||
|
||||
def pretty_timestamp(t, now=None):
|
||||
"""Parse timestamp to user readable format
|
||||
|
||||
>>> pretty_timestamp("20170614T151122Z", now="20170614T151123Z")
|
||||
'just now'
|
||||
|
||||
>>> pretty_timestamp("20170614T151122Z", now="20170614T171222Z")
|
||||
'2:01 hours ago'
|
||||
|
||||
Args:
|
||||
t (str): The time string to parse.
|
||||
now (str, optional)
|
||||
|
||||
Returns:
|
||||
str: human readable "recent" date.
|
||||
|
||||
"""
|
||||
|
||||
if now is not None:
|
||||
try:
|
||||
now = time.strptime(now, "%Y%m%dT%H%M%SZ")
|
||||
now = datetime.fromtimestamp(time.mktime(now))
|
||||
except ValueError as e:
|
||||
log.warning("Can't parse 'now' time format: {0} {1}".format(t, e))
|
||||
return None
|
||||
|
||||
if isinstance(t, float):
|
||||
dt = datetime.fromtimestamp(t)
|
||||
else:
|
||||
# Parse the time format as if it is `str` result from
|
||||
# `pyblish.lib.time()` which usually is stored in Avalon database.
|
||||
try:
|
||||
t = time.strptime(t, "%Y%m%dT%H%M%SZ")
|
||||
except ValueError as e:
|
||||
log.warning("Can't parse time format: {0} {1}".format(t, e))
|
||||
return None
|
||||
dt = datetime.fromtimestamp(time.mktime(t))
|
||||
|
||||
# prettify
|
||||
return pretty_date(dt, now=now)
|
||||
|
||||
|
||||
class PrettyTimeDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""A delegate that displays a timestamp as a pretty date.
|
||||
|
||||
This displays dates like `pretty_date`.
|
||||
|
||||
"""
|
||||
|
||||
def displayText(self, value, locale):
|
||||
|
||||
if value is None:
|
||||
# Ignore None value
|
||||
return
|
||||
|
||||
return pretty_timestamp(value)
|
||||
622
openpype/tools/utils/lib.py
Normal file
622
openpype/tools/utils/lib.py
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
import os
|
||||
import sys
|
||||
import contextlib
|
||||
import collections
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from avalon import io, api, style
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
self = sys.modules[__name__]
|
||||
self._jobs = dict()
|
||||
|
||||
|
||||
class SharedObjects:
|
||||
# Variable for family cache in global context
|
||||
# QUESTION is this safe? More than one tool can refresh at the same time.
|
||||
family_cache = None
|
||||
|
||||
|
||||
def global_family_cache():
|
||||
if SharedObjects.family_cache is None:
|
||||
SharedObjects.family_cache = FamilyConfigCache(io)
|
||||
return SharedObjects.family_cache
|
||||
|
||||
|
||||
def format_version(value, hero_version=False):
|
||||
"""Formats integer to displayable version name"""
|
||||
label = "v{0:03d}".format(value)
|
||||
if not hero_version:
|
||||
return label
|
||||
return "[{}]".format(label)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def application():
|
||||
app = QtWidgets.QApplication.instance()
|
||||
|
||||
if not app:
|
||||
print("Starting new QApplication..")
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
yield app
|
||||
app.exec_()
|
||||
else:
|
||||
print("Using existing QApplication..")
|
||||
yield app
|
||||
|
||||
|
||||
def defer(delay, func):
|
||||
"""Append artificial delay to `func`
|
||||
|
||||
This aids in keeping the GUI responsive, but complicates logic
|
||||
when producing tests. To combat this, the environment variable ensures
|
||||
that every operation is synchonous.
|
||||
|
||||
Arguments:
|
||||
delay (float): Delay multiplier; default 1, 0 means no delay
|
||||
func (callable): Any callable
|
||||
|
||||
"""
|
||||
|
||||
delay *= float(os.getenv("PYBLISH_DELAY", 1))
|
||||
if delay > 0:
|
||||
return QtCore.QTimer.singleShot(delay, func)
|
||||
else:
|
||||
return func()
|
||||
|
||||
|
||||
def schedule(func, time, channel="default"):
|
||||
"""Run `func` at a later `time` in a dedicated `channel`
|
||||
|
||||
Given an arbitrary function, call this function after a given
|
||||
timeout. It will ensure that only one "job" is running within
|
||||
the given channel at any one time and cancel any currently
|
||||
running job if a new job is submitted before the timeout.
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
self._jobs[channel].stop()
|
||||
except (AttributeError, KeyError, RuntimeError):
|
||||
pass
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.timeout.connect(func)
|
||||
timer.start(time)
|
||||
|
||||
self._jobs[channel] = timer
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def dummy():
|
||||
"""Dummy context manager
|
||||
|
||||
Usage:
|
||||
>> with some_context() if False else dummy():
|
||||
.. pass
|
||||
|
||||
"""
|
||||
|
||||
yield
|
||||
|
||||
|
||||
def iter_model_rows(model, column, include_root=False):
|
||||
"""Iterate over all row indices in a model"""
|
||||
indices = [QtCore.QModelIndex()] # start iteration at root
|
||||
|
||||
for index in indices:
|
||||
# Add children to the iterations
|
||||
child_rows = model.rowCount(index)
|
||||
for child_row in range(child_rows):
|
||||
child_index = model.index(child_row, column, index)
|
||||
indices.append(child_index)
|
||||
|
||||
if not include_root and not index.isValid():
|
||||
continue
|
||||
|
||||
yield index
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def preserve_states(tree_view,
|
||||
column=0,
|
||||
role=None,
|
||||
preserve_expanded=True,
|
||||
preserve_selection=True,
|
||||
expanded_role=QtCore.Qt.DisplayRole,
|
||||
selection_role=QtCore.Qt.DisplayRole):
|
||||
"""Preserves row selection in QTreeView by column's data role.
|
||||
This function is created to maintain the selection status of
|
||||
the model items. When refresh is triggered the items which are expanded
|
||||
will stay expanded and vise versa.
|
||||
tree_view (QWidgets.QTreeView): the tree view nested in the application
|
||||
column (int): the column to retrieve the data from
|
||||
role (int): the role which dictates what will be returned
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# When `role` is set then override both expanded and selection roles
|
||||
if role:
|
||||
expanded_role = role
|
||||
selection_role = role
|
||||
|
||||
model = tree_view.model()
|
||||
selection_model = tree_view.selectionModel()
|
||||
flags = selection_model.Select | selection_model.Rows
|
||||
|
||||
expanded = set()
|
||||
|
||||
if preserve_expanded:
|
||||
for index in iter_model_rows(
|
||||
model, column=column, include_root=False
|
||||
):
|
||||
if tree_view.isExpanded(index):
|
||||
value = index.data(expanded_role)
|
||||
expanded.add(value)
|
||||
|
||||
selected = None
|
||||
|
||||
if preserve_selection:
|
||||
selected_rows = selection_model.selectedRows()
|
||||
if selected_rows:
|
||||
selected = set(row.data(selection_role) for row in selected_rows)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if expanded:
|
||||
for index in iter_model_rows(
|
||||
model, column=0, include_root=False
|
||||
):
|
||||
value = index.data(expanded_role)
|
||||
is_expanded = value in expanded
|
||||
# skip if new index was created meanwhile
|
||||
if is_expanded is None:
|
||||
continue
|
||||
tree_view.setExpanded(index, is_expanded)
|
||||
|
||||
if selected:
|
||||
# Go through all indices, select the ones with similar data
|
||||
for index in iter_model_rows(
|
||||
model, column=column, include_root=False
|
||||
):
|
||||
value = index.data(selection_role)
|
||||
state = value in selected
|
||||
if state:
|
||||
tree_view.scrollTo(index) # Ensure item is visible
|
||||
selection_model.select(index, flags)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def preserve_expanded_rows(tree_view, column=0, role=None):
|
||||
"""Preserves expanded row in QTreeView by column's data role.
|
||||
|
||||
This function is created to maintain the expand vs collapse status of
|
||||
the model items. When refresh is triggered the items which are expanded
|
||||
will stay expanded and vise versa.
|
||||
|
||||
Arguments:
|
||||
tree_view (QWidgets.QTreeView): the tree view which is
|
||||
nested in the application
|
||||
column (int): the column to retrieve the data from
|
||||
role (int): the role which dictates what will be returned
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
model = tree_view.model()
|
||||
|
||||
expanded = set()
|
||||
|
||||
for index in iter_model_rows(model, column=column, include_root=False):
|
||||
if tree_view.isExpanded(index):
|
||||
value = index.data(role)
|
||||
expanded.add(value)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if not expanded:
|
||||
return
|
||||
|
||||
for index in iter_model_rows(model, column=column, include_root=False):
|
||||
value = index.data(role)
|
||||
state = value in expanded
|
||||
if state:
|
||||
tree_view.expand(index)
|
||||
else:
|
||||
tree_view.collapse(index)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def preserve_selection(tree_view, column=0, role=None, current_index=True):
|
||||
"""Preserves row selection in QTreeView by column's data role.
|
||||
|
||||
This function is created to maintain the selection status of
|
||||
the model items. When refresh is triggered the items which are expanded
|
||||
will stay expanded and vise versa.
|
||||
|
||||
tree_view (QWidgets.QTreeView): the tree view nested in the application
|
||||
column (int): the column to retrieve the data from
|
||||
role (int): the role which dictates what will be returned
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
model = tree_view.model()
|
||||
selection_model = tree_view.selectionModel()
|
||||
flags = selection_model.Select | selection_model.Rows
|
||||
|
||||
if current_index:
|
||||
current_index_value = tree_view.currentIndex().data(role)
|
||||
else:
|
||||
current_index_value = None
|
||||
|
||||
selected_rows = selection_model.selectedRows()
|
||||
if not selected_rows:
|
||||
yield
|
||||
return
|
||||
|
||||
selected = set(row.data(role) for row in selected_rows)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if not selected:
|
||||
return
|
||||
|
||||
# Go through all indices, select the ones with similar data
|
||||
for index in iter_model_rows(model, column=column, include_root=False):
|
||||
value = index.data(role)
|
||||
state = value in selected
|
||||
if state:
|
||||
tree_view.scrollTo(index) # Ensure item is visible
|
||||
selection_model.select(index, flags)
|
||||
|
||||
if current_index_value and value == current_index_value:
|
||||
selection_model.setCurrentIndex(
|
||||
index, selection_model.NoUpdate
|
||||
)
|
||||
|
||||
|
||||
class FamilyConfigCache:
|
||||
default_color = "#0091B2"
|
||||
_default_icon = None
|
||||
_default_item = None
|
||||
|
||||
def __init__(self, dbcon):
|
||||
self.dbcon = dbcon
|
||||
self.family_configs = {}
|
||||
|
||||
@classmethod
|
||||
def default_icon(cls):
|
||||
if cls._default_icon is None:
|
||||
cls._default_icon = qtawesome.icon(
|
||||
"fa.folder", color=cls.default_color
|
||||
)
|
||||
return cls._default_icon
|
||||
|
||||
@classmethod
|
||||
def default_item(cls):
|
||||
if cls._default_item is None:
|
||||
cls._default_item = {"icon": cls.default_icon()}
|
||||
return cls._default_item
|
||||
|
||||
def family_config(self, family_name):
|
||||
"""Get value from config with fallback to default"""
|
||||
return self.family_configs.get(family_name, self.default_item())
|
||||
|
||||
def refresh(self):
|
||||
"""Get the family configurations from the database
|
||||
|
||||
The configuration must be stored on the project under `config`.
|
||||
For example:
|
||||
|
||||
{"config": {
|
||||
"families": [
|
||||
{"name": "avalon.camera", label: "Camera", "icon": "photo"},
|
||||
{"name": "avalon.anim", label: "Animation", "icon": "male"},
|
||||
]
|
||||
}}
|
||||
|
||||
It is possible to override the default behavior and set specific
|
||||
families checked. For example we only want the families imagesequence
|
||||
and camera to be visible in the Loader.
|
||||
|
||||
# This will turn every item off
|
||||
api.data["familyStateDefault"] = False
|
||||
|
||||
# Only allow the imagesequence and camera
|
||||
api.data["familyStateToggled"] = ["imagesequence", "camera"]
|
||||
|
||||
"""
|
||||
|
||||
self.family_configs.clear()
|
||||
|
||||
families = []
|
||||
|
||||
# Update the icons from the project configuration
|
||||
project_name = self.dbcon.Session.get("AVALON_PROJECT")
|
||||
if project_name:
|
||||
project_doc = self.dbcon.find_one(
|
||||
{"type": "project"},
|
||||
projection={"config.families": True}
|
||||
)
|
||||
|
||||
if not project_doc:
|
||||
print((
|
||||
"Project \"{}\" not found!"
|
||||
" Can't refresh family icons cache."
|
||||
).format(project_name))
|
||||
else:
|
||||
families = project_doc["config"].get("families") or []
|
||||
|
||||
# Check if any family state are being overwritten by the configuration
|
||||
default_state = api.data.get("familiesStateDefault", True)
|
||||
toggled = set(api.data.get("familiesStateToggled") or [])
|
||||
|
||||
# Replace icons with a Qt icon we can use in the user interfaces
|
||||
for family in families:
|
||||
name = family["name"]
|
||||
# Set family icon
|
||||
icon = family.get("icon", None)
|
||||
if icon:
|
||||
family["icon"] = qtawesome.icon(
|
||||
"fa.{}".format(icon),
|
||||
color=self.default_color
|
||||
)
|
||||
else:
|
||||
family["icon"] = self.default_icon()
|
||||
|
||||
# Update state
|
||||
if name in toggled:
|
||||
state = True
|
||||
else:
|
||||
state = default_state
|
||||
family["state"] = state
|
||||
|
||||
self.family_configs[name] = family
|
||||
|
||||
return self.family_configs
|
||||
|
||||
|
||||
class GroupsConfig:
|
||||
# Subset group item's default icon and order
|
||||
_default_group_config = None
|
||||
|
||||
def __init__(self, dbcon):
|
||||
self.dbcon = dbcon
|
||||
self.groups = {}
|
||||
|
||||
@classmethod
|
||||
def default_group_config(cls):
|
||||
if cls._default_group_config is None:
|
||||
cls._default_group_config = {
|
||||
"icon": qtawesome.icon(
|
||||
"fa.object-group",
|
||||
color=style.colors.default
|
||||
),
|
||||
"order": 0
|
||||
}
|
||||
return cls._default_group_config
|
||||
|
||||
def refresh(self):
|
||||
"""Get subset group configurations from the database
|
||||
|
||||
The 'group' configuration must be stored in the project `config` field.
|
||||
See schema `config-1.0.json`
|
||||
|
||||
"""
|
||||
# Clear cached groups
|
||||
self.groups.clear()
|
||||
|
||||
group_configs = []
|
||||
project_name = self.dbcon.Session.get("AVALON_PROJECT")
|
||||
if project_name:
|
||||
# Get pre-defined group name and apperance from project config
|
||||
project_doc = self.dbcon.find_one(
|
||||
{"type": "project"},
|
||||
projection={"config.groups": True}
|
||||
)
|
||||
|
||||
if project_doc:
|
||||
group_configs = project_doc["config"].get("groups") or []
|
||||
else:
|
||||
print("Project not found! \"{}\"".format(project_name))
|
||||
|
||||
# Build pre-defined group configs
|
||||
for config in group_configs:
|
||||
name = config["name"]
|
||||
icon = "fa." + config.get("icon", "object-group")
|
||||
color = config.get("color", style.colors.default)
|
||||
order = float(config.get("order", 0))
|
||||
|
||||
self.groups[name] = {
|
||||
"icon": qtawesome.icon(icon, color=color),
|
||||
"order": order
|
||||
}
|
||||
|
||||
return self.groups
|
||||
|
||||
def ordered_groups(self, group_names):
|
||||
# default order zero included
|
||||
_orders = set([0])
|
||||
for config in self.groups.values():
|
||||
_orders.add(config["order"])
|
||||
|
||||
# Remap order to list index
|
||||
orders = sorted(_orders)
|
||||
|
||||
_groups = list()
|
||||
for name in group_names:
|
||||
# Get group config
|
||||
config = self.groups.get(name) or self.default_group_config()
|
||||
# Base order
|
||||
remapped_order = orders.index(config["order"])
|
||||
|
||||
data = {
|
||||
"name": name,
|
||||
"icon": config["icon"],
|
||||
"_order": remapped_order,
|
||||
}
|
||||
|
||||
_groups.append(data)
|
||||
|
||||
# Sort by tuple (base_order, name)
|
||||
# If there are multiple groups in same order, will sorted by name.
|
||||
ordered_groups = sorted(
|
||||
_groups, key=lambda _group: (_group.pop("_order"), _group["name"])
|
||||
)
|
||||
|
||||
total = len(ordered_groups)
|
||||
order_temp = "%0{}d".format(len(str(total)))
|
||||
|
||||
# Update sorted order to config
|
||||
for index, group_data in enumerate(ordered_groups):
|
||||
order = index
|
||||
inverse_order = total - index
|
||||
|
||||
# Format orders into fixed length string for groups sorting
|
||||
group_data["order"] = order_temp % order
|
||||
group_data["inverseOrder"] = order_temp % inverse_order
|
||||
|
||||
return ordered_groups
|
||||
|
||||
def active_groups(self, asset_ids, include_predefined=True):
|
||||
"""Collect all active groups from each subset"""
|
||||
# Collect groups from subsets
|
||||
group_names = set(
|
||||
self.dbcon.distinct(
|
||||
"data.subsetGroup",
|
||||
{"type": "subset", "parent": {"$in": asset_ids}}
|
||||
)
|
||||
)
|
||||
if include_predefined:
|
||||
# Ensure all predefined group configs will be included
|
||||
group_names.update(self.groups.keys())
|
||||
|
||||
return self.ordered_groups(group_names)
|
||||
|
||||
def split_subsets_for_groups(self, subset_docs, grouping):
|
||||
"""Collect all active groups from each subset"""
|
||||
subset_docs_without_group = collections.defaultdict(list)
|
||||
subset_docs_by_group = collections.defaultdict(dict)
|
||||
for subset_doc in subset_docs:
|
||||
subset_name = subset_doc["name"]
|
||||
if grouping:
|
||||
group_name = subset_doc["data"].get("subsetGroup")
|
||||
if group_name:
|
||||
if subset_name not in subset_docs_by_group[group_name]:
|
||||
subset_docs_by_group[group_name][subset_name] = []
|
||||
|
||||
subset_docs_by_group[group_name][subset_name].append(
|
||||
subset_doc
|
||||
)
|
||||
continue
|
||||
|
||||
subset_docs_without_group[subset_name].append(subset_doc)
|
||||
|
||||
ordered_groups = self.ordered_groups(subset_docs_by_group.keys())
|
||||
|
||||
return ordered_groups, subset_docs_without_group, subset_docs_by_group
|
||||
|
||||
|
||||
def create_qthread(func, *args, **kwargs):
|
||||
class Thread(QtCore.QThread):
|
||||
def run(self):
|
||||
func(*args, **kwargs)
|
||||
return Thread()
|
||||
|
||||
|
||||
def get_repre_icons():
|
||||
try:
|
||||
from openpype_modules import sync_server
|
||||
except Exception:
|
||||
# Backwards compatibility
|
||||
from openpype.modules import sync_server
|
||||
|
||||
resource_path = os.path.join(
|
||||
os.path.dirname(sync_server.sync_server_module.__file__),
|
||||
"providers", "resources"
|
||||
)
|
||||
icons = {}
|
||||
# TODO get from sync module
|
||||
for provider in ['studio', 'local_drive', 'gdrive']:
|
||||
pix_url = "{}/{}.png".format(resource_path, provider)
|
||||
icons[provider] = QtGui.QIcon(pix_url)
|
||||
|
||||
return icons
|
||||
|
||||
|
||||
def get_progress_for_repre(doc, active_site, remote_site):
|
||||
"""
|
||||
Calculates average progress for representation.
|
||||
|
||||
If site has created_dt >> fully available >> progress == 1
|
||||
|
||||
Could be calculated in aggregate if it would be too slow
|
||||
Args:
|
||||
doc(dict): representation dict
|
||||
Returns:
|
||||
(dict) with active and remote sites progress
|
||||
{'studio': 1.0, 'gdrive': -1} - gdrive site is not present
|
||||
-1 is used to highlight the site should be added
|
||||
{'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not
|
||||
uploaded yet
|
||||
"""
|
||||
progress = {active_site: -1,
|
||||
remote_site: -1}
|
||||
if not doc:
|
||||
return progress
|
||||
|
||||
files = {active_site: 0, remote_site: 0}
|
||||
doc_files = doc.get("files") or []
|
||||
for doc_file in doc_files:
|
||||
if not isinstance(doc_file, dict):
|
||||
continue
|
||||
|
||||
sites = doc_file.get("sites") or []
|
||||
for site in sites:
|
||||
if (
|
||||
# Pype 2 compatibility
|
||||
not isinstance(site, dict)
|
||||
# Check if site name is one of progress sites
|
||||
or site["name"] not in progress
|
||||
):
|
||||
continue
|
||||
|
||||
files[site["name"]] += 1
|
||||
norm_progress = max(progress[site["name"]], 0)
|
||||
if site.get("created_dt"):
|
||||
progress[site["name"]] = norm_progress + 1
|
||||
elif site.get("progress"):
|
||||
progress[site["name"]] = norm_progress + site["progress"]
|
||||
else: # site exists, might be failed, do not add again
|
||||
progress[site["name"]] = 0
|
||||
|
||||
# for example 13 fully avail. files out of 26 >> 13/26 = 0.5
|
||||
avg_progress = {}
|
||||
avg_progress[active_site] = \
|
||||
progress[active_site] / max(files[active_site], 1)
|
||||
avg_progress[remote_site] = \
|
||||
progress[remote_site] / max(files[remote_site], 1)
|
||||
return avg_progress
|
||||
|
||||
|
||||
def is_sync_loader(loader):
|
||||
return is_remove_site_loader(loader) or is_add_site_loader(loader)
|
||||
|
||||
|
||||
def is_remove_site_loader(loader):
|
||||
return hasattr(loader, "remove_site_on_representation")
|
||||
|
||||
|
||||
def is_add_site_loader(loader):
|
||||
return hasattr(loader, "add_site_to_representation")
|
||||
500
openpype/tools/utils/models.py
Normal file
500
openpype/tools/utils/models.py
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
import re
|
||||
import time
|
||||
import logging
|
||||
import collections
|
||||
|
||||
import Qt
|
||||
from Qt import QtCore, QtGui
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon import style, io
|
||||
from . import lib
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TreeModel(QtCore.QAbstractItemModel):
|
||||
|
||||
Columns = list()
|
||||
ItemRole = QtCore.Qt.UserRole + 1
|
||||
item_class = None
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(TreeModel, self).__init__(parent)
|
||||
self._root_item = self.ItemClass()
|
||||
|
||||
@property
|
||||
def ItemClass(self):
|
||||
if self.item_class is not None:
|
||||
return self.item_class
|
||||
return Item
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
if parent is None or not parent.isValid():
|
||||
parent_item = self._root_item
|
||||
else:
|
||||
parent_item = parent.internalPointer()
|
||||
return parent_item.childCount()
|
||||
|
||||
def columnCount(self, parent):
|
||||
return len(self.Columns)
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return None
|
||||
|
||||
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
|
||||
item = index.internalPointer()
|
||||
column = index.column()
|
||||
|
||||
key = self.Columns[column]
|
||||
return item.get(key, None)
|
||||
|
||||
if role == self.ItemRole:
|
||||
return index.internalPointer()
|
||||
|
||||
def setData(self, index, value, role=QtCore.Qt.EditRole):
|
||||
"""Change the data on the items.
|
||||
|
||||
Returns:
|
||||
bool: Whether the edit was successful
|
||||
"""
|
||||
|
||||
if index.isValid():
|
||||
if role == QtCore.Qt.EditRole:
|
||||
|
||||
item = index.internalPointer()
|
||||
column = index.column()
|
||||
key = self.Columns[column]
|
||||
item[key] = value
|
||||
|
||||
# passing `list()` for PyQt5 (see PYSIDE-462)
|
||||
if Qt.__binding__ in ("PyQt4", "PySide"):
|
||||
self.dataChanged.emit(index, index)
|
||||
else:
|
||||
self.dataChanged.emit(index, index, [role])
|
||||
|
||||
# must return true if successful
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def setColumns(self, keys):
|
||||
assert isinstance(keys, (list, tuple))
|
||||
self.Columns = keys
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
if section < len(self.Columns):
|
||||
return self.Columns[section]
|
||||
|
||||
super(TreeModel, self).headerData(section, orientation, role)
|
||||
|
||||
def flags(self, index):
|
||||
flags = QtCore.Qt.ItemIsEnabled
|
||||
|
||||
item = index.internalPointer()
|
||||
if item.get("enabled", True):
|
||||
flags |= QtCore.Qt.ItemIsSelectable
|
||||
|
||||
return flags
|
||||
|
||||
def parent(self, index):
|
||||
|
||||
item = index.internalPointer()
|
||||
parent_item = item.parent()
|
||||
|
||||
# If it has no parents we return invalid
|
||||
if parent_item == self._root_item or not parent_item:
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
return self.createIndex(parent_item.row(), 0, parent_item)
|
||||
|
||||
def index(self, row, column, parent=None):
|
||||
"""Return index for row/column under parent"""
|
||||
|
||||
if parent is None or not parent.isValid():
|
||||
parent_item = self._root_item
|
||||
else:
|
||||
parent_item = parent.internalPointer()
|
||||
|
||||
child_item = parent_item.child(row)
|
||||
if child_item:
|
||||
return self.createIndex(row, column, child_item)
|
||||
else:
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
def add_child(self, item, parent=None):
|
||||
if parent is None:
|
||||
parent = self._root_item
|
||||
|
||||
parent.add_child(item)
|
||||
|
||||
def column_name(self, column):
|
||||
"""Return column key by index"""
|
||||
|
||||
if column < len(self.Columns):
|
||||
return self.Columns[column]
|
||||
|
||||
def clear(self):
|
||||
self.beginResetModel()
|
||||
self._root_item = self.ItemClass()
|
||||
self.endResetModel()
|
||||
|
||||
|
||||
class Item(dict):
|
||||
"""An item that can be represented in a tree view using `TreeModel`.
|
||||
|
||||
The item can store data just like a regular dictionary.
|
||||
|
||||
>>> data = {"name": "John", "score": 10}
|
||||
>>> item = Item(data)
|
||||
>>> assert item["name"] == "John"
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, data=None):
|
||||
super(Item, self).__init__()
|
||||
|
||||
self._children = list()
|
||||
self._parent = None
|
||||
|
||||
if data is not None:
|
||||
assert isinstance(data, dict)
|
||||
self.update(data)
|
||||
|
||||
def childCount(self):
|
||||
return len(self._children)
|
||||
|
||||
def child(self, row):
|
||||
|
||||
if row >= len(self._children):
|
||||
log.warning("Invalid row as child: {0}".format(row))
|
||||
return
|
||||
|
||||
return self._children[row]
|
||||
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
def parent(self):
|
||||
return self._parent
|
||||
|
||||
def row(self):
|
||||
"""
|
||||
Returns:
|
||||
int: Index of this item under parent"""
|
||||
if self._parent is not None:
|
||||
siblings = self.parent().children()
|
||||
return siblings.index(self)
|
||||
return -1
|
||||
|
||||
def add_child(self, child):
|
||||
"""Add a child to this item"""
|
||||
child._parent = self
|
||||
self._children.append(child)
|
||||
|
||||
|
||||
class AssetModel(TreeModel):
|
||||
"""A model listing assets in the silo in the active project.
|
||||
|
||||
The assets are displayed in a treeview, they are visually parented by
|
||||
a `visualParent` field in the database containing an `_id` to a parent
|
||||
asset.
|
||||
|
||||
"""
|
||||
|
||||
Columns = ["label"]
|
||||
Name = 0
|
||||
Deprecated = 2
|
||||
ObjectId = 3
|
||||
|
||||
DocumentRole = QtCore.Qt.UserRole + 2
|
||||
ObjectIdRole = QtCore.Qt.UserRole + 3
|
||||
subsetColorsRole = QtCore.Qt.UserRole + 4
|
||||
|
||||
doc_fetched = QtCore.Signal(bool)
|
||||
refreshed = QtCore.Signal(bool)
|
||||
|
||||
# Asset document projection
|
||||
asset_projection = {
|
||||
"type": 1,
|
||||
"schema": 1,
|
||||
"name": 1,
|
||||
"silo": 1,
|
||||
"data.visualParent": 1,
|
||||
"data.label": 1,
|
||||
"data.tags": 1,
|
||||
"data.icon": 1,
|
||||
"data.color": 1,
|
||||
"data.deprecated": 1
|
||||
}
|
||||
|
||||
def __init__(self, dbcon=None, parent=None, asset_projection=None):
|
||||
super(AssetModel, self).__init__(parent=parent)
|
||||
if dbcon is None:
|
||||
dbcon = io
|
||||
self.dbcon = dbcon
|
||||
self.asset_colors = {}
|
||||
|
||||
# Projections for Mongo queries
|
||||
# - let ability to modify them if used in tools that require more than
|
||||
# defaults
|
||||
if asset_projection:
|
||||
self.asset_projection = asset_projection
|
||||
|
||||
self.asset_projection = asset_projection
|
||||
|
||||
self._doc_fetching_thread = None
|
||||
self._doc_fetching_stop = False
|
||||
self._doc_payload = {}
|
||||
|
||||
self.doc_fetched.connect(self.on_doc_fetched)
|
||||
|
||||
self.refresh()
|
||||
|
||||
def _add_hierarchy(self, assets, parent=None, silos=None):
|
||||
"""Add the assets that are related to the parent as children items.
|
||||
|
||||
This method does *not* query the database. These instead are queried
|
||||
in a single batch upfront as an optimization to reduce database
|
||||
queries. Resulting in up to 10x speed increase.
|
||||
|
||||
Args:
|
||||
assets (dict): All assets in the currently active silo stored
|
||||
by key/value
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
# Reset colors
|
||||
self.asset_colors = {}
|
||||
|
||||
if silos:
|
||||
# WARNING: Silo item "_id" is set to silo value
|
||||
# mainly because GUI issue with perserve selection and expanded row
|
||||
# and because of easier hierarchy parenting (in "assets")
|
||||
for silo in silos:
|
||||
item = Item({
|
||||
"_id": silo,
|
||||
"name": silo,
|
||||
"label": silo,
|
||||
"type": "silo"
|
||||
})
|
||||
self.add_child(item, parent=parent)
|
||||
self._add_hierarchy(assets, parent=item)
|
||||
|
||||
parent_id = parent["_id"] if parent else None
|
||||
current_assets = assets.get(parent_id, list())
|
||||
|
||||
for asset in current_assets:
|
||||
# get label from data, otherwise use name
|
||||
data = asset.get("data", {})
|
||||
label = data.get("label", asset["name"])
|
||||
tags = data.get("tags", [])
|
||||
|
||||
# store for the asset for optimization
|
||||
deprecated = "deprecated" in tags
|
||||
|
||||
item = Item({
|
||||
"_id": asset["_id"],
|
||||
"name": asset["name"],
|
||||
"label": label,
|
||||
"type": asset["type"],
|
||||
"tags": ", ".join(tags),
|
||||
"deprecated": deprecated,
|
||||
"_document": asset
|
||||
})
|
||||
self.add_child(item, parent=parent)
|
||||
|
||||
# Add asset's children recursively if it has children
|
||||
if asset["_id"] in assets:
|
||||
self._add_hierarchy(assets, parent=item)
|
||||
|
||||
self.asset_colors[asset["_id"]] = []
|
||||
|
||||
def on_doc_fetched(self, was_stopped):
|
||||
if was_stopped:
|
||||
self.stop_fetch_thread()
|
||||
return
|
||||
|
||||
self.beginResetModel()
|
||||
|
||||
assets_by_parent = self._doc_payload.get("assets_by_parent")
|
||||
silos = self._doc_payload.get("silos")
|
||||
if assets_by_parent is not None:
|
||||
# Build the hierarchical tree items recursively
|
||||
self._add_hierarchy(
|
||||
assets_by_parent,
|
||||
parent=None,
|
||||
silos=silos
|
||||
)
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
has_content = bool(assets_by_parent) or bool(silos)
|
||||
self.refreshed.emit(has_content)
|
||||
|
||||
self.stop_fetch_thread()
|
||||
|
||||
def fetch(self):
|
||||
self._doc_payload = self._fetch() or {}
|
||||
# Emit doc fetched only if was not stopped
|
||||
self.doc_fetched.emit(self._doc_fetching_stop)
|
||||
|
||||
def _fetch(self):
|
||||
if not self.dbcon.Session.get("AVALON_PROJECT"):
|
||||
return
|
||||
|
||||
project_doc = self.dbcon.find_one(
|
||||
{"type": "project"},
|
||||
{"_id": True}
|
||||
)
|
||||
if not project_doc:
|
||||
return
|
||||
|
||||
# Get all assets sorted by name
|
||||
db_assets = self.dbcon.find(
|
||||
{"type": "asset"},
|
||||
self.asset_projection
|
||||
).sort("name", 1)
|
||||
|
||||
# Group the assets by their visual parent's id
|
||||
assets_by_parent = collections.defaultdict(list)
|
||||
for asset in db_assets:
|
||||
if self._doc_fetching_stop:
|
||||
return
|
||||
parent_id = asset.get("data", {}).get("visualParent")
|
||||
assets_by_parent[parent_id].append(asset)
|
||||
|
||||
return {
|
||||
"assets_by_parent": assets_by_parent,
|
||||
"silos": None
|
||||
}
|
||||
|
||||
def stop_fetch_thread(self):
|
||||
if self._doc_fetching_thread is not None:
|
||||
self._doc_fetching_stop = True
|
||||
while self._doc_fetching_thread.isRunning():
|
||||
time.sleep(0.001)
|
||||
self._doc_fetching_thread = None
|
||||
|
||||
def refresh(self, force=False):
|
||||
"""Refresh the data for the model."""
|
||||
# Skip fetch if there is already other thread fetching documents
|
||||
if self._doc_fetching_thread is not None:
|
||||
if not force:
|
||||
return
|
||||
self.stop_fetch_thread()
|
||||
|
||||
# Clear model items
|
||||
self.clear()
|
||||
|
||||
# Fetch documents from mongo
|
||||
# Restart payload
|
||||
self._doc_payload = {}
|
||||
self._doc_fetching_stop = False
|
||||
self._doc_fetching_thread = lib.create_qthread(self.fetch)
|
||||
self._doc_fetching_thread.start()
|
||||
|
||||
def flags(self, index):
|
||||
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||
|
||||
def setData(self, index, value, role=QtCore.Qt.EditRole):
|
||||
if not index.isValid():
|
||||
return False
|
||||
|
||||
if role == self.subsetColorsRole:
|
||||
asset_id = index.data(self.ObjectIdRole)
|
||||
self.asset_colors[asset_id] = value
|
||||
|
||||
if Qt.__binding__ in ("PyQt4", "PySide"):
|
||||
self.dataChanged.emit(index, index)
|
||||
else:
|
||||
self.dataChanged.emit(index, index, [role])
|
||||
|
||||
return True
|
||||
|
||||
return super(AssetModel, self).setData(index, value, role)
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
item = index.internalPointer()
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
column = index.column()
|
||||
if column == self.Name:
|
||||
# Allow a custom icon and custom icon color to be defined
|
||||
data = item.get("_document", {}).get("data", {})
|
||||
icon = data.get("icon", None)
|
||||
if icon is None and item.get("type") == "silo":
|
||||
icon = "database"
|
||||
color = data.get("color", style.colors.default)
|
||||
|
||||
if icon is None:
|
||||
# Use default icons if no custom one is specified.
|
||||
# If it has children show a full folder, otherwise
|
||||
# show an open folder
|
||||
has_children = self.rowCount(index) > 0
|
||||
icon = "folder" if has_children else "folder-o"
|
||||
|
||||
# Make the color darker when the asset is deprecated
|
||||
if item.get("deprecated", False):
|
||||
color = QtGui.QColor(color).darker(250)
|
||||
|
||||
try:
|
||||
key = "fa.{0}".format(icon) # font-awesome key
|
||||
icon = qtawesome.icon(key, color=color)
|
||||
return icon
|
||||
except Exception as exception:
|
||||
# Log an error message instead of erroring out completely
|
||||
# when the icon couldn't be created (e.g. invalid name)
|
||||
log.error(exception)
|
||||
|
||||
return
|
||||
|
||||
if role == QtCore.Qt.ForegroundRole: # font color
|
||||
if "deprecated" in item.get("tags", []):
|
||||
return QtGui.QColor(style.colors.light).darker(250)
|
||||
|
||||
if role == self.ObjectIdRole:
|
||||
return item.get("_id", None)
|
||||
|
||||
if role == self.DocumentRole:
|
||||
return item.get("_document", None)
|
||||
|
||||
if role == self.subsetColorsRole:
|
||||
asset_id = item.get("_id", None)
|
||||
return self.asset_colors.get(asset_id) or []
|
||||
|
||||
return super(AssetModel, self).data(index, role)
|
||||
|
||||
|
||||
class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""Filters to the regex if any of the children matches allow parent"""
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
regex = self.filterRegExp()
|
||||
if not regex.isEmpty():
|
||||
pattern = regex.pattern()
|
||||
model = self.sourceModel()
|
||||
source_index = model.index(row, self.filterKeyColumn(), parent)
|
||||
if source_index.isValid():
|
||||
# Check current index itself
|
||||
key = model.data(source_index, self.filterRole())
|
||||
if re.search(pattern, key, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
# Check children
|
||||
rows = model.rowCount(source_index)
|
||||
for i in range(rows):
|
||||
if self.filterAcceptsRow(i, source_index):
|
||||
return True
|
||||
|
||||
# Otherwise filter it
|
||||
return False
|
||||
|
||||
return super(
|
||||
RecursiveSortFilterProxyModel, self
|
||||
).filterAcceptsRow(row, parent)
|
||||
86
openpype/tools/utils/views.py
Normal file
86
openpype/tools/utils/views.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import os
|
||||
from avalon import style
|
||||
from Qt import QtWidgets, QtCore, QtGui, QtSvg
|
||||
|
||||
|
||||
class DeselectableTreeView(QtWidgets.QTreeView):
|
||||
"""A tree view that deselects on clicking on an empty area in the view"""
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
|
||||
index = self.indexAt(event.pos())
|
||||
if not index.isValid():
|
||||
# clear the selection
|
||||
self.clearSelection()
|
||||
# clear the current index
|
||||
self.setCurrentIndex(QtCore.QModelIndex())
|
||||
|
||||
QtWidgets.QTreeView.mousePressEvent(self, event)
|
||||
|
||||
|
||||
class TreeViewSpinner(QtWidgets.QTreeView):
|
||||
size = 160
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(TreeViewSpinner, self).__init__(parent=parent)
|
||||
|
||||
loading_image_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(style.__file__)),
|
||||
"svg",
|
||||
"spinner-200.svg"
|
||||
)
|
||||
self.spinner = QtSvg.QSvgRenderer(loading_image_path)
|
||||
|
||||
self.is_loading = False
|
||||
self.is_empty = True
|
||||
|
||||
def paint_loading(self, event):
|
||||
rect = event.rect()
|
||||
rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight())
|
||||
rect.moveTo(
|
||||
rect.x() + rect.width() / 2 - self.size / 2,
|
||||
rect.y() + rect.height() / 2 - self.size / 2
|
||||
)
|
||||
rect.setSize(QtCore.QSizeF(self.size, self.size))
|
||||
painter = QtGui.QPainter(self.viewport())
|
||||
self.spinner.render(painter, rect)
|
||||
|
||||
def paint_empty(self, event):
|
||||
painter = QtGui.QPainter(self.viewport())
|
||||
rect = event.rect()
|
||||
rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight())
|
||||
qtext_opt = QtGui.QTextOption(
|
||||
QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter
|
||||
)
|
||||
painter.drawText(rect, "No Data", qtext_opt)
|
||||
|
||||
def paintEvent(self, event):
|
||||
if self.is_loading:
|
||||
self.paint_loading(event)
|
||||
elif self.is_empty:
|
||||
self.paint_empty(event)
|
||||
else:
|
||||
super(TreeViewSpinner, self).paintEvent(event)
|
||||
|
||||
|
||||
class AssetsView(TreeViewSpinner, DeselectableTreeView):
|
||||
"""Item view.
|
||||
This implements a context menu.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(AssetsView, self).__init__()
|
||||
self.setIndentation(15)
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.setHeaderHidden(True)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
index = self.indexAt(event.pos())
|
||||
if not index.isValid():
|
||||
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
||||
if modifiers == QtCore.Qt.ShiftModifier:
|
||||
return
|
||||
elif modifiers == QtCore.Qt.ControlModifier:
|
||||
return
|
||||
|
||||
super(AssetsView, self).mousePressEvent(event)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue