Merge branch 'develop' into feature/timers_manager_wihtout_interface

This commit is contained in:
iLLiCiTiT 2021-09-20 17:21:38 +02:00
commit a447879ab9
144 changed files with 7692 additions and 598 deletions

View file

@ -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"

View file

@ -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 }}

View file

@ -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)

View file

@ -10,8 +10,10 @@ from .pipeline import (
from avalon.tools import (
creator,
loader,
sceneinventory,
)
from openpype.tools import (
loader,
libraryloader
)

View file

@ -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

View file

@ -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",

View file

@ -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"))

View file

@ -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

View file

@ -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...")

View file

@ -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>

View file

@ -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

View file

@ -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")

View file

@ -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)

View file

@ -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)
@ -727,7 +727,7 @@ class WorkfileSettings(object):
log.error(msg)
nuke.message(msg)
log.warning(">> root_dict: {}".format(root_dict))
log.debug(">> root_dict: {}".format(root_dict))
# first set OCIO
if self._root_node["colorManagement"].value() \
@ -1277,6 +1277,7 @@ class ExporterReview:
def clean_nodes(self):
for node in self._temp_nodes:
nuke.delete(node)
self._temp_nodes = []
self.log.info("Deleted nodes...")
@ -1301,6 +1302,7 @@ class ExporterReviewLut(ExporterReview):
lut_style=None):
# initialize parent class
ExporterReview.__init__(self, klass, instance)
self._temp_nodes = []
# deal with now lut defined in viewer lut
if hasattr(klass, "viewer_lut_raw"):

View file

@ -2,6 +2,7 @@ import nuke
import pyblish.api
from avalon.nuke import maintained_selection
class CreateOutputNode(pyblish.api.ContextPlugin):
"""Adding output node for each ouput write node
So when latly user will want to Load .nk as LifeGroup or Precomp
@ -15,8 +16,8 @@ class CreateOutputNode(pyblish.api.ContextPlugin):
def process(self, context):
# capture selection state
with maintained_selection():
active_node = [node for inst in context[:]
for node in inst[:]
active_node = [node for inst in context
for node in inst
if "ak:family" in node.knobs()]
if active_node:

View file

@ -3,6 +3,12 @@ import pyblish.api
from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api import lib as pnlib
import openpype
try:
from __builtin__ import reload
except ImportError:
from importlib import reload
reload(pnlib)

View file

@ -4,6 +4,13 @@ from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api import lib as pnlib
import openpype
try:
from __builtin__ import reload
except ImportError:
from importlib import reload
reload(pnlib)
class ExtractReviewDataMov(openpype.api.Extractor):
"""Extracts movie and thumbnail with baked in luts

View file

@ -1,3 +1,4 @@
import sys
import os
import nuke
from avalon.nuke import lib as anlib
@ -5,6 +6,10 @@ import pyblish.api
import openpype
if sys.version_info[0] >= 3:
unicode = str
class ExtractThumbnail(openpype.api.Extractor):
"""Extracts movie and thumbnail with baked in luts

View file

@ -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,

View file

@ -3,7 +3,6 @@ import pyblish.api
import os
import openpype.api as pype
from avalon.nuke import lib as anlib
reload(anlib)
class CollectWorkfile(pyblish.api.ContextPlugin):

View file

@ -69,7 +69,8 @@ def evaluate_filepath_new(k_value, k_eval, project_dir, first_frame):
frames = sorted(frames)
firstframe = frames[0]
lastframe = frames[len(frames) - 1]
if lastframe < 0:
if int(lastframe) < 0:
lastframe = firstframe
return filepath, firstframe, lastframe

View file

@ -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",

View file

@ -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():

View file

@ -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

View file

@ -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

View file

@ -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)}")

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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)

View file

@ -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.

View file

@ -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()

View file

@ -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.

View file

@ -2,13 +2,10 @@ import os
import openpype
from openpype import resources
from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayModule,
IWebServerRoutes
)
from openpype_interfaces import ITrayModule
class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
class AvalonModule(OpenPypeModule, ITrayModule):
name = "avalon"
def initialize(self, modules_settings):
@ -55,12 +52,12 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
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
)
@ -71,16 +68,6 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
exc_info=True
)
def connect_with_modules(self, _enabled_modules):
return
def webserver_initialization(self, server_manager):
"""Implementation of IWebServerRoutes interface."""
if self.tray_initialized:
from .rest_api import AvalonRestApiResource
self.rest_api_obj = AvalonRestApiResource(self, server_manager)
# Definition of Tray menu
def tray_menu(self, tray_menu):
from Qt import QtWidgets
@ -108,3 +95,10 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
# for Windows
self.libraryloader.activateWindow()
self.libraryloader.refresh()
# Webserver module implementation
def webserver_initialization(self, server_manager):
"""Add routes for webserver."""
if self.tray_initialized:
from .rest_api import AvalonRestApiResource
self.rest_api_obj = AvalonRestApiResource(self, server_manager)

View file

@ -100,9 +100,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

View file

@ -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")

View file

@ -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__))

View 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"

View file

@ -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(

View file

@ -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()

View file

@ -3,13 +3,10 @@ import json
import appdirs
import requests
from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayModule,
IWebServerRoutes
)
from openpype_interfaces import ITrayModule
class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
class MusterModule(OpenPypeModule, ITrayModule):
"""
Module handling Muster Render credentials. This will display dialog
asking for user credentials for Muster if not already specified.
@ -54,9 +51,6 @@ class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
"""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."""
@ -76,13 +70,6 @@ class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
parent.addMenu(menu)
def webserver_initialization(self, server_manager):
"""Implementation of IWebServerRoutes interface."""
if self.tray_initialized:
from .rest_api import MusterModuleRestApi
self.rest_api_obj = MusterModuleRestApi(self, server_manager)
def load_credentials(self):
"""
Get credentials from JSON file
@ -142,6 +129,14 @@ class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
if self.widget_login:
self.widget_login.show()
# Webserver module implementation
def webserver_initialization(self, server_manager):
"""Add routes for Muster login."""
if self.tray_initialized:
from .rest_api import MusterModuleRestApi
self.rest_api_obj = MusterModuleRestApi(self, server_manager)
def _requests_post(self, *args, **kwargs):
""" Wrapper for requests, disabling SSL certificate validation if
DONT_VERIFY_SSL environment variable is found. This is useful when

View file

@ -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(

View file

@ -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()

View file

@ -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:

View file

@ -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()):

View file

@ -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()

View file

@ -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")

View file

@ -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.

View file

@ -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()

View file

@ -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

View file

@ -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 (

View file

@ -4,8 +4,7 @@ from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITimersManager,
ITrayService,
IIdleManager,
IWebServerRoutes
IIdleManager
)
from avalon.api import AvalonMongoDB
@ -69,9 +68,7 @@ class ExampleTimersManagerConnector:
self._timers_manager_module.timer_stopped(self._module.id)
class TimersManager(
OpenPypeModule, ITrayService, IIdleManager, IWebServerRoutes
):
class TimersManager(OpenPypeModule, ITrayService, IIdleManager):
""" Handles about Timers.
Should be able to start/stop all timers at once.
@ -129,14 +126,6 @@ class TimersManager(
"""Nothing special for TimersManager."""
return
def webserver_initialization(self, server_manager):
"""Implementation of IWebServerRoutes interface."""
if self.tray_initialized:
from .rest_api import TimersManagerModuleRestApi
self.rest_api_obj = TimersManagerModuleRestApi(
self, server_manager
)
def start_timer(self, project_name, asset_name, task_name, hierarchy):
"""
Start timer for 'project_name', 'asset_name' and 'task_name'
@ -320,6 +309,15 @@ class TimersManager(
if self.widget_user_idle.bool_is_showed is False:
self.widget_user_idle.show()
# Webserver module implementation
def webserver_initialization(self, server_manager):
"""Add routes for timers to be able start/stop with rest api."""
if self.tray_initialized:
from .rest_api import TimersManagerModuleRestApi
self.rest_api_obj = TimersManagerModuleRestApi(
self, server_manager
)
def change_timer_from_host(self, project_name, asset_name, task_name):
"""Prepared method for calling change timers on REST api"""
webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL")

View file

@ -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

View file

@ -1,9 +0,0 @@
from abc import abstractmethod
from openpype.modules import OpenPypeInterface
class IWebServerRoutes(OpenPypeInterface):
"""Other modules interface to register their routes."""
@abstractmethod
def webserver_initialization(self, server_manager):
pass

View file

@ -1,12 +1,31 @@
"""WebServerModule spawns aiohttp server in asyncio loop.
Main usage of the module is in OpenPype tray where make sense to add ability
of other modules to add theirs routes. Module which would want use that
option must have implemented method `webserver_initialization` which must
expect `WebServerManager` object where is possible to add routes or paths
with handlers.
WebServerManager is by default created only in tray.
It is possible to create server manager without using module logic at all
using `create_new_server_manager`. That can be handy for standalone scripts
with predefined host and port and separated routes and logic.
Running multiple servers in one process is not recommended and probably won't
work as expected. It is because of few limitations connected to asyncio module.
When module's `create_server_manager` is called it is also set environment
variable "OPENPYPE_WEBSERVER_URL". Which should lead to root access point
of server.
"""
import os
import socket
from openpype import resources
from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayService,
IWebServerRoutes
)
from openpype_interfaces import ITrayService
class WebServerModule(OpenPypeModule, ITrayService):
@ -28,8 +47,15 @@ class WebServerModule(OpenPypeModule, ITrayService):
return
for module in enabled_modules:
if isinstance(module, IWebServerRoutes):
if not hasattr(module, "webserver_initialization"):
continue
try:
module.webserver_initialization(self.server_manager)
except Exception:
self.log.warning((
"Failed to connect module \"{}\" to webserver."
).format(module.name))
def tray_init(self):
self.create_server_manager()

View file

@ -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(

View file

@ -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):

View file

@ -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(

View file

@ -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))

View file

@ -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))

View file

@ -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.

View file

@ -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"]))

View file

@ -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

View file

@ -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"

View file

@ -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": []

View file

@ -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
}

View file

@ -520,6 +520,7 @@
"workfile_build": {
"profiles": [
{
"task_types": [],
"tasks": [
"Lighting"
],

View file

@ -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": [
{

View file

@ -7,8 +7,9 @@
"profiles": [
{
"families": [],
"tasks": [],
"hosts": [],
"task_types": [],
"tasks": [],
"channel_messages": []
}
]

View file

@ -195,7 +195,7 @@
"environment": {}
},
"__dynamic_keys_labels__": {
"13-0": "13.0 (Testing only)",
"13-0": "13.0",
"12-2": "12.2",
"12-0": "12.0",
"11-3": "11.3",
@ -331,7 +331,7 @@
"environment": {}
},
"__dynamic_keys_labels__": {
"13-0": "13.0 (Testing only)",
"13-0": "13.0",
"12-2": "12.2",
"12-0": "12.0",
"11-3": "11.3",

View file

@ -650,6 +650,11 @@
"type": "list",
"object_type": "text"
},
{
"key": "task_types",
"label": "Task types",
"type": "task-types-enum"
},
{
"key": "tasks",
"label": "Task names",

View file

@ -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"
},

View file

@ -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",

View file

@ -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"
},

View file

@ -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"
}
]
}
]
},

View file

@ -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 @@
}
}
]
}
}

View file

@ -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"
},

View file

@ -91,4 +91,4 @@ def load_stylesheet():
def app_icon_path():
return resources.pype_icon_filepath()
return resources.get_openpype_icon_filepath()

View file

@ -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)

View file

@ -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())

View file

@ -0,0 +1,11 @@
from .app import (
LibraryLoaderWindow,
show,
cli
)
__all__ = [
"LibraryLoaderWindow",
"show",
"cli",
]

View file

@ -0,0 +1,5 @@
from . import cli
if __name__ == '__main__':
import sys
sys.exit(cli(sys.argv[1:]))

View 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)

View 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]

View 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)

View file

@ -0,0 +1,11 @@
from .app import (
LoaderWindow,
show,
cli,
)
__all__ = (
"LoaderWindow",
"show",
"cli",
)

View 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:]))

View 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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View 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)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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
)

View file

@ -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())

View 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

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