Merge branch 'develop' into feature/houdini-cb-update
46
CHANGELOG.md
|
|
@ -1,11 +1,16 @@
|
|||
# Changelog
|
||||
|
||||
## [3.3.0-nightly.9](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.3.0-nightly.10](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927)
|
||||
- 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)
|
||||
- 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)
|
||||
- submodules: avalon-core update [\#1911](https://github.com/pypeclub/OpenPype/pull/1911)
|
||||
- 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)
|
||||
|
|
@ -22,21 +27,20 @@
|
|||
- Workfile tool start at host launch support [\#1865](https://github.com/pypeclub/OpenPype/pull/1865)
|
||||
- Anatomy schema validation [\#1864](https://github.com/pypeclub/OpenPype/pull/1864)
|
||||
- Ftrack prepare project structure [\#1861](https://github.com/pypeclub/OpenPype/pull/1861)
|
||||
- Maya: support for configurable `dirmap` 🗺️ [\#1859](https://github.com/pypeclub/OpenPype/pull/1859)
|
||||
- Independent general environments [\#1853](https://github.com/pypeclub/OpenPype/pull/1853)
|
||||
- TVPaint Start Frame [\#1844](https://github.com/pypeclub/OpenPype/pull/1844)
|
||||
- Ftrack push attributes action adds traceback to job [\#1843](https://github.com/pypeclub/OpenPype/pull/1843)
|
||||
- Prepare project action enhance [\#1838](https://github.com/pypeclub/OpenPype/pull/1838)
|
||||
- Standalone Publish of textures family [\#1834](https://github.com/pypeclub/OpenPype/pull/1834)
|
||||
- nuke: settings create missing default subsets [\#1829](https://github.com/pypeclub/OpenPype/pull/1829)
|
||||
- Update poetry lock [\#1823](https://github.com/pypeclub/OpenPype/pull/1823)
|
||||
- Settings: settings for plugins [\#1819](https://github.com/pypeclub/OpenPype/pull/1819)
|
||||
- Settings list can use template or schema as object type [\#1815](https://github.com/pypeclub/OpenPype/pull/1815)
|
||||
- Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797)
|
||||
- Maya: Shader name validation [\#1762](https://github.com/pypeclub/OpenPype/pull/1762)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Nuke: update video file crassing [\#1916](https://github.com/pypeclub/OpenPype/pull/1916)
|
||||
- Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935)
|
||||
- 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)
|
||||
- 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)
|
||||
- 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)
|
||||
|
|
@ -48,20 +52,14 @@
|
|||
- Normalize path returned from Workfiles. [\#1880](https://github.com/pypeclub/OpenPype/pull/1880)
|
||||
- Workfiles tool event arguments fix [\#1862](https://github.com/pypeclub/OpenPype/pull/1862)
|
||||
- imageio: fix grouping [\#1856](https://github.com/pypeclub/OpenPype/pull/1856)
|
||||
- publisher: missing version in subset prop [\#1849](https://github.com/pypeclub/OpenPype/pull/1849)
|
||||
- Ftrack type error fix in sync to avalon event handler [\#1845](https://github.com/pypeclub/OpenPype/pull/1845)
|
||||
- Nuke: updating effects subset fail [\#1841](https://github.com/pypeclub/OpenPype/pull/1841)
|
||||
- nuke: write render node skipped with crop [\#1836](https://github.com/pypeclub/OpenPype/pull/1836)
|
||||
- Project folder structure overrides [\#1813](https://github.com/pypeclub/OpenPype/pull/1813)
|
||||
- Maya: fix yeti settings path in extractor [\#1809](https://github.com/pypeclub/OpenPype/pull/1809)
|
||||
- Failsafe for cross project containers. [\#1806](https://github.com/pypeclub/OpenPype/pull/1806)
|
||||
- Settings error dialog on show [\#1798](https://github.com/pypeclub/OpenPype/pull/1798)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937)
|
||||
- Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932)
|
||||
- Maya: add support for `RedshiftNormalMap` node, fix `tx` linear space 🚀 [\#1863](https://github.com/pypeclub/OpenPype/pull/1863)
|
||||
- Add support for pyenv-win on windows [\#1822](https://github.com/pypeclub/OpenPype/pull/1822)
|
||||
- PS, AE - send actual context when another webserver is running [\#1811](https://github.com/pypeclub/OpenPype/pull/1811)
|
||||
|
||||
## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13)
|
||||
|
||||
|
|
@ -69,16 +67,10 @@
|
|||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Nuke: ftrack family plugin settings preset [\#1805](https://github.com/pypeclub/OpenPype/pull/1805)
|
||||
- Standalone publisher last project [\#1799](https://github.com/pypeclub/OpenPype/pull/1799)
|
||||
- Ftrack Multiple notes as server action [\#1795](https://github.com/pypeclub/OpenPype/pull/1795)
|
||||
- Settings conditional dict [\#1777](https://github.com/pypeclub/OpenPype/pull/1777)
|
||||
- Settings application use python 2 only where needed [\#1776](https://github.com/pypeclub/OpenPype/pull/1776)
|
||||
- Settings UI copy/paste [\#1769](https://github.com/pypeclub/OpenPype/pull/1769)
|
||||
- Workfile tool widths [\#1766](https://github.com/pypeclub/OpenPype/pull/1766)
|
||||
- Push hierarchical attributes care about task parent changes [\#1763](https://github.com/pypeclub/OpenPype/pull/1763)
|
||||
- Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757)
|
||||
- Deadline: Nuke submission additional attributes [\#1756](https://github.com/pypeclub/OpenPype/pull/1756)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
|
|
@ -88,18 +80,12 @@
|
|||
- Fix: staging and `--use-version` option [\#1786](https://github.com/pypeclub/OpenPype/pull/1786)
|
||||
- Otio unrelated error on import [\#1782](https://github.com/pypeclub/OpenPype/pull/1782)
|
||||
- FFprobe streams order [\#1775](https://github.com/pypeclub/OpenPype/pull/1775)
|
||||
- Fix - single file files are str only, cast it to list to count properly [\#1772](https://github.com/pypeclub/OpenPype/pull/1772)
|
||||
- Environments in app executable for MacOS [\#1768](https://github.com/pypeclub/OpenPype/pull/1768)
|
||||
- Project specific environments [\#1767](https://github.com/pypeclub/OpenPype/pull/1767)
|
||||
- Settings UI with refresh button [\#1764](https://github.com/pypeclub/OpenPype/pull/1764)
|
||||
- Standalone publisher thumbnail extractor fix [\#1761](https://github.com/pypeclub/OpenPype/pull/1761)
|
||||
- Anatomy others templates don't cause crash [\#1758](https://github.com/pypeclub/OpenPype/pull/1758)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- PS, AE - send actual context when another webserver is running [\#1811](https://github.com/pypeclub/OpenPype/pull/1811)
|
||||
- Build: don't add Poetry to `PATH` [\#1808](https://github.com/pypeclub/OpenPype/pull/1808)
|
||||
- Bump prismjs from 1.23.0 to 1.24.0 in /website [\#1773](https://github.com/pypeclub/OpenPype/pull/1773)
|
||||
- Bc/fix/docs [\#1771](https://github.com/pypeclub/OpenPype/pull/1771)
|
||||
|
||||
## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24)
|
||||
|
||||
|
|
|
|||
13
README.md
|
|
@ -29,7 +29,7 @@ The main things you will need to run and build OpenPype are:
|
|||
- PowerShell 5.0+ (Windows)
|
||||
- Bash (Linux)
|
||||
- [**Python 3.7.8**](#python) or higher
|
||||
- [**MongoDB**](#database)
|
||||
- [**MongoDB**](#database) (needed only for local development)
|
||||
|
||||
|
||||
It can be built and ran on all common platforms. We develop and test on the following:
|
||||
|
|
@ -126,6 +126,16 @@ pyenv local 3.7.9
|
|||
|
||||
### Linux
|
||||
|
||||
#### Docker
|
||||
Easiest way to build OpenPype on Linux is using [Docker](https://www.docker.com/). Just run:
|
||||
|
||||
```sh
|
||||
sudo ./tools/docker_build.sh
|
||||
```
|
||||
|
||||
If all is successful, you'll find built OpenPype in `./build/` folder.
|
||||
|
||||
#### Manual build
|
||||
You will need [Python 3.7](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll also need [curl](https://curl.se) on systems that doesn't have one preinstalled.
|
||||
|
||||
To build Python related stuff, you need Python header files installed (`python3-dev` on Ubuntu for example).
|
||||
|
|
@ -133,7 +143,6 @@ To build Python related stuff, you need Python header files installed (`python3-
|
|||
You'll need also other tools to build
|
||||
some OpenPype dependencies like [CMake](https://cmake.org/). Python 3 should be part of all modern distributions. You can use your package manager to install **git** and **cmake**.
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Details for Ubuntu</summary>
|
||||
Install git, cmake and curl
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
|
|||
"subset": subset,
|
||||
"label": scene_file,
|
||||
"family": family,
|
||||
"families": [family, "ftrack"],
|
||||
"families": [family],
|
||||
"representations": list()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
|||
|
||||
|
||||
def install():
|
||||
from openpype.settings import get_project_settings
|
||||
|
||||
project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
|
||||
# process path mapping
|
||||
process_dirmap(project_settings)
|
||||
|
||||
pyblish.register_plugin_path(PUBLISH_PATH)
|
||||
avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
|
||||
avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
|
||||
|
|
@ -53,6 +59,40 @@ def install():
|
|||
avalon.data["familiesStateToggled"] = ["imagesequence"]
|
||||
|
||||
|
||||
def process_dirmap(project_settings):
|
||||
# type: (dict) -> None
|
||||
"""Go through all paths in Settings and set them using `dirmap`.
|
||||
|
||||
Args:
|
||||
project_settings (dict): Settings for current project.
|
||||
|
||||
"""
|
||||
if not project_settings["maya"].get("maya-dirmap"):
|
||||
return
|
||||
mapping = project_settings["maya"]["maya-dirmap"]["paths"] or {}
|
||||
mapping_enabled = project_settings["maya"]["maya-dirmap"]["enabled"]
|
||||
if not mapping or not mapping_enabled:
|
||||
return
|
||||
if mapping.get("source-path") and mapping_enabled is True:
|
||||
log.info("Processing directory mapping ...")
|
||||
cmds.dirmap(en=True)
|
||||
for k, sp in enumerate(mapping["source-path"]):
|
||||
try:
|
||||
print("{} -> {}".format(sp, mapping["destination-path"][k]))
|
||||
cmds.dirmap(m=(sp, mapping["destination-path"][k]))
|
||||
cmds.dirmap(m=(mapping["destination-path"][k], sp))
|
||||
except IndexError:
|
||||
# missing corresponding destination path
|
||||
log.error(("invalid dirmap mapping, missing corresponding"
|
||||
" destination directory."))
|
||||
break
|
||||
except RuntimeError:
|
||||
log.error("invalid path {} -> {}, mapping not registered".format(
|
||||
sp, mapping["destination-path"][k]
|
||||
))
|
||||
continue
|
||||
|
||||
|
||||
def uninstall():
|
||||
pyblish.deregister_plugin_path(PUBLISH_PATH)
|
||||
avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)
|
||||
|
|
|
|||
|
|
@ -1,945 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Module handling expected render output from Maya.
|
||||
|
||||
This module is used in :mod:`collect_render` and :mod:`collect_vray_scene`.
|
||||
|
||||
Note:
|
||||
To implement new renderer, just create new class inheriting from
|
||||
:class:`AExpectedFiles` and add it to :func:`ExpectedFiles.get()`.
|
||||
|
||||
Attributes:
|
||||
R_SINGLE_FRAME (:class:`re.Pattern`): Find single frame number.
|
||||
R_FRAME_RANGE (:class:`re.Pattern`): Find frame range.
|
||||
R_FRAME_NUMBER (:class:`re.Pattern`): Find frame number in string.
|
||||
R_LAYER_TOKEN (:class:`re.Pattern`): Find layer token in image prefixes.
|
||||
R_AOV_TOKEN (:class:`re.Pattern`): Find AOV token in image prefixes.
|
||||
R_SUBSTITUTE_AOV_TOKEN (:class:`re.Pattern`): Find and substitute AOV token
|
||||
in image prefixes.
|
||||
R_REMOVE_AOV_TOKEN (:class:`re.Pattern`): Find and remove AOV token in
|
||||
image prefixes.
|
||||
R_CLEAN_FRAME_TOKEN (:class:`re.Pattern`): Find and remove unfilled
|
||||
Renderman frame token in image prefix.
|
||||
R_CLEAN_EXT_TOKEN (:class:`re.Pattern`): Find and remove unfilled Renderman
|
||||
extension token in image prefix.
|
||||
R_SUBSTITUTE_LAYER_TOKEN (:class:`re.Pattern`): Find and substitute render
|
||||
layer token in image prefixes.
|
||||
R_SUBSTITUTE_SCENE_TOKEN (:class:`re.Pattern`): Find and substitute scene
|
||||
token in image prefixes.
|
||||
R_SUBSTITUTE_CAMERA_TOKEN (:class:`re.Pattern`): Find and substitute camera
|
||||
token in image prefixes.
|
||||
RENDERER_NAMES (dict): Renderer names mapping between reported name and
|
||||
*human readable* name.
|
||||
IMAGE_PREFIXES (dict): Mapping between renderers and their respective
|
||||
image prefix attribute names.
|
||||
|
||||
Todo:
|
||||
Determine `multipart` from render instance.
|
||||
|
||||
"""
|
||||
|
||||
import types
|
||||
import re
|
||||
import os
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import six
|
||||
import attr
|
||||
|
||||
import openpype.hosts.maya.api.lib as lib
|
||||
|
||||
from maya import cmds
|
||||
import maya.app.renderSetup.model.renderSetup as renderSetup
|
||||
|
||||
|
||||
R_SINGLE_FRAME = re.compile(r"^(-?)\d+$")
|
||||
R_FRAME_RANGE = re.compile(r"^(?P<sf>(-?)\d+)-(?P<ef>(-?)\d+)$")
|
||||
R_FRAME_NUMBER = re.compile(r".+\.(?P<frame>[0-9]+)\..+")
|
||||
R_LAYER_TOKEN = re.compile(
|
||||
r".*((?:%l)|(?:<layer>)|(?:<renderlayer>)).*", re.IGNORECASE
|
||||
)
|
||||
R_AOV_TOKEN = re.compile(r".*%a.*|.*<aov>.*|.*<renderpass>.*", re.IGNORECASE)
|
||||
R_SUBSTITUTE_AOV_TOKEN = re.compile(r"%a|<aov>|<renderpass>", re.IGNORECASE)
|
||||
R_REMOVE_AOV_TOKEN = re.compile(
|
||||
r"_%a|\.%a|_<aov>|\.<aov>|_<renderpass>|\.<renderpass>", re.IGNORECASE)
|
||||
# to remove unused renderman tokens
|
||||
R_CLEAN_FRAME_TOKEN = re.compile(r"\.?<f\d>\.?", re.IGNORECASE)
|
||||
R_CLEAN_EXT_TOKEN = re.compile(r"\.?<ext>\.?", re.IGNORECASE)
|
||||
|
||||
R_SUBSTITUTE_LAYER_TOKEN = re.compile(
|
||||
r"%l|<layer>|<renderlayer>", re.IGNORECASE
|
||||
)
|
||||
R_SUBSTITUTE_CAMERA_TOKEN = re.compile(r"%c|<camera>", re.IGNORECASE)
|
||||
R_SUBSTITUTE_SCENE_TOKEN = re.compile(r"%s|<scene>", re.IGNORECASE)
|
||||
|
||||
RENDERER_NAMES = {
|
||||
"mentalray": "MentalRay",
|
||||
"vray": "V-Ray",
|
||||
"arnold": "Arnold",
|
||||
"renderman": "Renderman",
|
||||
"redshift": "Redshift",
|
||||
}
|
||||
|
||||
# not sure about the renderman image prefix
|
||||
IMAGE_PREFIXES = {
|
||||
"mentalray": "defaultRenderGlobals.imageFilePrefix",
|
||||
"vray": "vraySettings.fileNamePrefix",
|
||||
"arnold": "defaultRenderGlobals.imageFilePrefix",
|
||||
"renderman": "rmanGlobals.imageFileFormat",
|
||||
"redshift": "defaultRenderGlobals.imageFilePrefix",
|
||||
}
|
||||
|
||||
|
||||
@attr.s
|
||||
class LayerMetadata(object):
|
||||
"""Data class for Render Layer metadata."""
|
||||
frameStart = attr.ib()
|
||||
frameEnd = attr.ib()
|
||||
cameras = attr.ib()
|
||||
sceneName = attr.ib()
|
||||
layerName = attr.ib()
|
||||
renderer = attr.ib()
|
||||
defaultExt = attr.ib()
|
||||
filePrefix = attr.ib()
|
||||
enabledAOVs = attr.ib()
|
||||
frameStep = attr.ib(default=1)
|
||||
padding = attr.ib(default=4)
|
||||
|
||||
|
||||
class ExpectedFiles:
|
||||
"""Class grouping functionality for all supported renderers.
|
||||
|
||||
Attributes:
|
||||
multipart (bool): Flag if multipart exrs are used.
|
||||
|
||||
"""
|
||||
multipart = False
|
||||
|
||||
def __init__(self, render_instance):
|
||||
"""Constructor."""
|
||||
self._render_instance = render_instance
|
||||
|
||||
def get(self, renderer, layer):
|
||||
"""Get expected files for given renderer and render layer.
|
||||
|
||||
Args:
|
||||
renderer (str): Name of renderer
|
||||
layer (str): Name of render layer
|
||||
|
||||
Returns:
|
||||
dict: Expected rendered files by AOV
|
||||
|
||||
Raises:
|
||||
:exc:`UnsupportedRendererException`: If requested renderer
|
||||
is not supported. It needs to be implemented by extending
|
||||
:class:`AExpectedFiles` and added to this methods ``if``
|
||||
statement.
|
||||
|
||||
"""
|
||||
renderSetup.instance().switchToLayerUsingLegacyName(layer)
|
||||
|
||||
if renderer.lower() == "arnold":
|
||||
return self._get_files(ExpectedFilesArnold(layer,
|
||||
self._render_instance))
|
||||
if renderer.lower() == "vray":
|
||||
return self._get_files(ExpectedFilesVray(
|
||||
layer, self._render_instance))
|
||||
if renderer.lower() == "redshift":
|
||||
return self._get_files(ExpectedFilesRedshift(
|
||||
layer, self._render_instance))
|
||||
if renderer.lower() == "mentalray":
|
||||
return self._get_files(ExpectedFilesMentalray(
|
||||
layer, self._render_instance))
|
||||
if renderer.lower() == "renderman":
|
||||
return self._get_files(ExpectedFilesRenderman(
|
||||
layer, self._render_instance))
|
||||
|
||||
raise UnsupportedRendererException(
|
||||
"unsupported {}".format(renderer)
|
||||
)
|
||||
|
||||
def _get_files(self, renderer):
|
||||
# type: (AExpectedFiles) -> list
|
||||
files = renderer.get_files()
|
||||
self.multipart = renderer.multipart
|
||||
return files
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class AExpectedFiles:
|
||||
"""Abstract class with common code for all renderers.
|
||||
|
||||
Attributes:
|
||||
renderer (str): name of renderer.
|
||||
layer (str): name of render layer.
|
||||
multipart (bool): flag for multipart exrs.
|
||||
|
||||
"""
|
||||
|
||||
renderer = None
|
||||
layer = None
|
||||
multipart = False
|
||||
|
||||
def __init__(self, layer, render_instance):
|
||||
"""Constructor."""
|
||||
self.layer = layer
|
||||
self.render_instance = render_instance
|
||||
|
||||
@abstractmethod
|
||||
def get_aovs(self):
|
||||
"""To be implemented by renderer class."""
|
||||
|
||||
@staticmethod
|
||||
def sanitize_camera_name(camera):
|
||||
"""Sanitize camera name.
|
||||
|
||||
Remove Maya illegal characters from camera name.
|
||||
|
||||
Args:
|
||||
camera (str): Maya camera name.
|
||||
|
||||
Returns:
|
||||
(str): sanitized camera name
|
||||
|
||||
Example:
|
||||
>>> AExpectedFiles.sanizite_camera_name('test:camera_01')
|
||||
test_camera_01
|
||||
|
||||
"""
|
||||
return re.sub('[^0-9a-zA-Z_]+', '_', camera)
|
||||
|
||||
def get_renderer_prefix(self):
|
||||
"""Return prefix for specific renderer.
|
||||
|
||||
This is for most renderers the same and can be overridden if needed.
|
||||
|
||||
Returns:
|
||||
str: String with image prefix containing tokens
|
||||
|
||||
Raises:
|
||||
:exc:`UnsupportedRendererException`: If we requested image
|
||||
prefix for renderer we know nothing about.
|
||||
See :data:`IMAGE_PREFIXES` for mapping of renderers and
|
||||
image prefixes.
|
||||
|
||||
"""
|
||||
try:
|
||||
file_prefix = cmds.getAttr(IMAGE_PREFIXES[self.renderer])
|
||||
except KeyError:
|
||||
raise UnsupportedRendererException(
|
||||
"Unsupported renderer {}".format(self.renderer)
|
||||
)
|
||||
return file_prefix
|
||||
|
||||
def _get_layer_data(self):
|
||||
# type: () -> LayerMetadata
|
||||
# ______________________________________________
|
||||
# ____________________/ ____________________________________________/
|
||||
# 1 - get scene name /__________________/
|
||||
# ____________________/
|
||||
_, scene_basename = os.path.split(cmds.file(q=True, loc=True))
|
||||
scene_name, _ = os.path.splitext(scene_basename)
|
||||
|
||||
file_prefix = self.get_renderer_prefix()
|
||||
|
||||
if not file_prefix:
|
||||
raise RuntimeError("Image prefix not set")
|
||||
|
||||
layer_name = self.layer
|
||||
if self.layer.startswith("rs_"):
|
||||
layer_name = self.layer[3:]
|
||||
|
||||
return LayerMetadata(
|
||||
frameStart=int(self.get_render_attribute("startFrame")),
|
||||
frameEnd=int(self.get_render_attribute("endFrame")),
|
||||
frameStep=int(self.get_render_attribute("byFrameStep")),
|
||||
padding=int(self.get_render_attribute("extensionPadding")),
|
||||
# if we have <camera> token in prefix path we'll expect output for
|
||||
# every renderable camera in layer.
|
||||
cameras=self.get_renderable_cameras(),
|
||||
sceneName=scene_name,
|
||||
layerName=layer_name,
|
||||
renderer=self.renderer,
|
||||
defaultExt=cmds.getAttr("defaultRenderGlobals.imfPluginKey"),
|
||||
filePrefix=file_prefix,
|
||||
enabledAOVs=self.get_aovs()
|
||||
)
|
||||
|
||||
def _generate_single_file_sequence(
|
||||
self, layer_data, force_aov_name=None):
|
||||
# type: (LayerMetadata, str) -> list
|
||||
expected_files = []
|
||||
for cam in layer_data.cameras:
|
||||
file_prefix = layer_data.filePrefix
|
||||
mappings = (
|
||||
(R_SUBSTITUTE_SCENE_TOKEN, layer_data.sceneName),
|
||||
(R_SUBSTITUTE_LAYER_TOKEN, layer_data.layerName),
|
||||
(R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)),
|
||||
# this is required to remove unfilled aov token, for example
|
||||
# in Redshift
|
||||
(R_REMOVE_AOV_TOKEN, "") if not force_aov_name \
|
||||
else (R_SUBSTITUTE_AOV_TOKEN, force_aov_name),
|
||||
|
||||
(R_CLEAN_FRAME_TOKEN, ""),
|
||||
(R_CLEAN_EXT_TOKEN, ""),
|
||||
)
|
||||
|
||||
for regex, value in mappings:
|
||||
file_prefix = re.sub(regex, value, file_prefix)
|
||||
|
||||
for frame in range(
|
||||
int(layer_data.frameStart),
|
||||
int(layer_data.frameEnd) + 1,
|
||||
int(layer_data.frameStep),
|
||||
):
|
||||
expected_files.append(
|
||||
"{}.{}.{}".format(
|
||||
file_prefix,
|
||||
str(frame).rjust(layer_data.padding, "0"),
|
||||
layer_data.defaultExt,
|
||||
)
|
||||
)
|
||||
return expected_files
|
||||
|
||||
def _generate_aov_file_sequences(self, layer_data):
|
||||
# type: (LayerMetadata) -> list
|
||||
expected_files = []
|
||||
aov_file_list = {}
|
||||
for aov in layer_data.enabledAOVs:
|
||||
for cam in layer_data.cameras:
|
||||
file_prefix = layer_data.filePrefix
|
||||
|
||||
mappings = (
|
||||
(R_SUBSTITUTE_SCENE_TOKEN, layer_data.sceneName),
|
||||
(R_SUBSTITUTE_LAYER_TOKEN, layer_data.layerName),
|
||||
(R_SUBSTITUTE_CAMERA_TOKEN,
|
||||
self.sanitize_camera_name(cam)),
|
||||
(R_SUBSTITUTE_AOV_TOKEN, aov[0]),
|
||||
(R_CLEAN_FRAME_TOKEN, ""),
|
||||
(R_CLEAN_EXT_TOKEN, ""),
|
||||
)
|
||||
|
||||
for regex, value in mappings:
|
||||
file_prefix = re.sub(regex, value, file_prefix)
|
||||
|
||||
aov_files = []
|
||||
for frame in range(
|
||||
int(layer_data.frameStart),
|
||||
int(layer_data.frameEnd) + 1,
|
||||
int(layer_data.frameStep),
|
||||
):
|
||||
aov_files.append(
|
||||
"{}.{}.{}".format(
|
||||
file_prefix,
|
||||
str(frame).rjust(layer_data.padding, "0"),
|
||||
aov[1],
|
||||
)
|
||||
)
|
||||
|
||||
# if we have more then one renderable camera, append
|
||||
# camera name to AOV to allow per camera AOVs.
|
||||
aov_name = aov[0]
|
||||
if len(layer_data.cameras) > 1:
|
||||
aov_name = "{}_{}".format(aov[0],
|
||||
self.sanitize_camera_name(cam))
|
||||
|
||||
aov_file_list[aov_name] = aov_files
|
||||
file_prefix = layer_data.filePrefix
|
||||
|
||||
expected_files.append(aov_file_list)
|
||||
return expected_files
|
||||
|
||||
def get_files(self):
|
||||
"""Return list of expected files.
|
||||
|
||||
It will translate render token strings ('<RenderPass>', etc.) to
|
||||
their values. This task is tricky as every renderer deals with this
|
||||
differently. It depends on `get_aovs()` abstract method implemented
|
||||
for every supported renderer.
|
||||
|
||||
"""
|
||||
layer_data = self._get_layer_data()
|
||||
|
||||
expected_files = []
|
||||
if layer_data.enabledAOVs:
|
||||
return self._generate_aov_file_sequences(layer_data)
|
||||
else:
|
||||
return self._generate_single_file_sequence(layer_data)
|
||||
|
||||
def get_renderable_cameras(self):
|
||||
# type: () -> list
|
||||
"""Get all renderable cameras.
|
||||
|
||||
Returns:
|
||||
list: list of renderable cameras.
|
||||
|
||||
"""
|
||||
cam_parents = [
|
||||
cmds.listRelatives(x, ap=True)[-1] for x in cmds.ls(cameras=True)
|
||||
]
|
||||
|
||||
return [
|
||||
cam
|
||||
for cam in cam_parents
|
||||
if self.maya_is_true(cmds.getAttr("{}.renderable".format(cam)))
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def maya_is_true(attr_val):
|
||||
"""Whether a Maya attr evaluates to True.
|
||||
|
||||
When querying an attribute value from an ambiguous object the
|
||||
Maya API will return a list of values, which need to be properly
|
||||
handled to evaluate properly.
|
||||
|
||||
Args:
|
||||
attr_val (mixed): Maya attribute to be evaluated as bool.
|
||||
|
||||
Returns:
|
||||
bool: cast Maya attribute to Pythons boolean value.
|
||||
|
||||
"""
|
||||
if isinstance(attr_val, types.BooleanType):
|
||||
return attr_val
|
||||
if isinstance(attr_val, (types.ListType, types.GeneratorType)):
|
||||
return any(attr_val)
|
||||
|
||||
return bool(attr_val)
|
||||
|
||||
@staticmethod
|
||||
def get_layer_overrides(attribute):
|
||||
"""Get overrides for attribute on current render layer.
|
||||
|
||||
Args:
|
||||
attribute (str): Maya attribute name.
|
||||
|
||||
Returns:
|
||||
Value of attribute override.
|
||||
|
||||
"""
|
||||
connections = cmds.listConnections(attribute, plugs=True)
|
||||
if connections:
|
||||
for connection in connections:
|
||||
if connection:
|
||||
# node_name = connection.split(".")[0]
|
||||
|
||||
attr_name = "%s.value" % ".".join(
|
||||
connection.split(".")[:-1]
|
||||
)
|
||||
yield cmds.getAttr(attr_name)
|
||||
|
||||
def get_render_attribute(self, attribute):
|
||||
"""Get attribute from render options.
|
||||
|
||||
Args:
|
||||
attribute (str): name of attribute to be looked up.
|
||||
|
||||
Returns:
|
||||
Attribute value
|
||||
|
||||
"""
|
||||
return lib.get_attr_in_layer(
|
||||
"defaultRenderGlobals.{}".format(attribute), layer=self.layer
|
||||
)
|
||||
|
||||
|
||||
class ExpectedFilesArnold(AExpectedFiles):
|
||||
"""Expected files for Arnold renderer.
|
||||
|
||||
Attributes:
|
||||
aiDriverExtension (dict): Arnold AOV driver extension mapping.
|
||||
Is there a better way?
|
||||
renderer (str): name of renderer.
|
||||
|
||||
"""
|
||||
|
||||
aiDriverExtension = {
|
||||
"jpeg": "jpg",
|
||||
"exr": "exr",
|
||||
"deepexr": "exr",
|
||||
"png": "png",
|
||||
"tiff": "tif",
|
||||
"mtoa_shaders": "ass", # TODO: research what those last two should be
|
||||
"maya": "",
|
||||
}
|
||||
|
||||
def __init__(self, layer, render_instance):
|
||||
"""Constructor."""
|
||||
super(ExpectedFilesArnold, self).__init__(layer, render_instance)
|
||||
self.renderer = "arnold"
|
||||
|
||||
def get_aovs(self):
|
||||
"""Get all AOVs.
|
||||
|
||||
See Also:
|
||||
:func:`AExpectedFiles.get_aovs()`
|
||||
|
||||
Raises:
|
||||
:class:`AOVError`: If AOV cannot be determined.
|
||||
|
||||
"""
|
||||
enabled_aovs = []
|
||||
try:
|
||||
if not (
|
||||
cmds.getAttr("defaultArnoldRenderOptions.aovMode")
|
||||
and not cmds.getAttr("defaultArnoldDriver.mergeAOVs") # noqa: W503, E501
|
||||
):
|
||||
# AOVs are merged in mutli-channel file
|
||||
self.multipart = True
|
||||
return enabled_aovs
|
||||
except ValueError:
|
||||
# this occurs when Render Setting windows was not opened yet. In
|
||||
# such case there are no Arnold options created so query for AOVs
|
||||
# will fail. We terminate here as there are no AOVs specified then.
|
||||
# This state will most probably fail later on some Validator
|
||||
# anyway.
|
||||
return enabled_aovs
|
||||
|
||||
# AOVs are set to be rendered separately. We should expect
|
||||
# <RenderPass> token in path.
|
||||
|
||||
# handle aovs from references
|
||||
use_ref_aovs = self.render_instance.data.get(
|
||||
"useReferencedAovs", False) or False
|
||||
|
||||
ai_aovs = cmds.ls(type="aiAOV")
|
||||
if not use_ref_aovs:
|
||||
ref_aovs = cmds.ls(type="aiAOV", referencedNodes=True)
|
||||
ai_aovs = list(set(ai_aovs) - set(ref_aovs))
|
||||
|
||||
for aov in ai_aovs:
|
||||
enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov)))
|
||||
ai_driver = cmds.listConnections("{}.outputs".format(aov))[0]
|
||||
ai_translator = cmds.getAttr("{}.aiTranslator".format(ai_driver))
|
||||
try:
|
||||
aov_ext = self.aiDriverExtension[ai_translator]
|
||||
except KeyError:
|
||||
msg = (
|
||||
"Unrecognized arnold " "driver format for AOV - {}"
|
||||
).format(cmds.getAttr("{}.name".format(aov)))
|
||||
raise AOVError(msg)
|
||||
|
||||
for override in self.get_layer_overrides(
|
||||
"{}.enabled".format(aov)
|
||||
):
|
||||
enabled = self.maya_is_true(override)
|
||||
if enabled:
|
||||
# If aov RGBA is selected, arnold will translate it to `beauty`
|
||||
aov_name = cmds.getAttr("%s.name" % aov)
|
||||
if aov_name == "RGBA":
|
||||
aov_name = "beauty"
|
||||
enabled_aovs.append((aov_name, aov_ext))
|
||||
# Append 'beauty' as this is arnolds
|
||||
# default. If <RenderPass> token is specified and no AOVs are
|
||||
# defined, this will be used.
|
||||
enabled_aovs.append(
|
||||
(u"beauty", cmds.getAttr("defaultRenderGlobals.imfPluginKey"))
|
||||
)
|
||||
return enabled_aovs
|
||||
|
||||
|
||||
class ExpectedFilesVray(AExpectedFiles):
|
||||
"""Expected files for V-Ray renderer."""
|
||||
|
||||
def __init__(self, layer, render_instance):
|
||||
"""Constructor."""
|
||||
super(ExpectedFilesVray, self).__init__(layer, render_instance)
|
||||
self.renderer = "vray"
|
||||
|
||||
def get_renderer_prefix(self):
|
||||
"""Get image prefix for V-Ray.
|
||||
|
||||
This overrides :func:`AExpectedFiles.get_renderer_prefix()` as
|
||||
we must add `<aov>` token manually.
|
||||
|
||||
See also:
|
||||
:func:`AExpectedFiles.get_renderer_prefix()`
|
||||
|
||||
"""
|
||||
prefix = super(ExpectedFilesVray, self).get_renderer_prefix()
|
||||
prefix = "{}_<aov>".format(prefix)
|
||||
return prefix
|
||||
|
||||
def _get_layer_data(self):
|
||||
# type: () -> LayerMetadata
|
||||
"""Override to get vray specific extension."""
|
||||
layer_data = super(ExpectedFilesVray, self)._get_layer_data()
|
||||
default_ext = cmds.getAttr("vraySettings.imageFormatStr")
|
||||
if default_ext in ["exr (multichannel)", "exr (deep)"]:
|
||||
default_ext = "exr"
|
||||
layer_data.defaultExt = default_ext
|
||||
layer_data.padding = cmds.getAttr("vraySettings.fileNamePadding")
|
||||
return layer_data
|
||||
|
||||
def get_files(self):
|
||||
"""Get expected files.
|
||||
|
||||
This overrides :func:`AExpectedFiles.get_files()` as we
|
||||
we need to add one sequence for plain beauty if AOVs are enabled
|
||||
as vray output beauty without 'beauty' in filename.
|
||||
|
||||
"""
|
||||
expected_files = super(ExpectedFilesVray, self).get_files()
|
||||
|
||||
layer_data = self._get_layer_data()
|
||||
# remove 'beauty' from filenames as vray doesn't output it
|
||||
update = {}
|
||||
if layer_data.enabledAOVs:
|
||||
for aov, seqs in expected_files[0].items():
|
||||
if aov.startswith("beauty"):
|
||||
new_list = []
|
||||
for seq in seqs:
|
||||
new_list.append(seq.replace("_beauty", ""))
|
||||
update[aov] = new_list
|
||||
|
||||
expected_files[0].update(update)
|
||||
return expected_files
|
||||
|
||||
def get_aovs(self):
|
||||
"""Get all AOVs.
|
||||
|
||||
See Also:
|
||||
:func:`AExpectedFiles.get_aovs()`
|
||||
|
||||
"""
|
||||
enabled_aovs = []
|
||||
|
||||
try:
|
||||
# really? do we set it in vray just by selecting multichannel exr?
|
||||
if (
|
||||
cmds.getAttr("vraySettings.imageFormatStr")
|
||||
== "exr (multichannel)" # noqa: W503
|
||||
):
|
||||
# AOVs are merged in mutli-channel file
|
||||
self.multipart = True
|
||||
return enabled_aovs
|
||||
except ValueError:
|
||||
# this occurs when Render Setting windows was not opened yet. In
|
||||
# such case there are no VRay options created so query for AOVs
|
||||
# will fail. We terminate here as there are no AOVs specified then.
|
||||
# This state will most probably fail later on some Validator
|
||||
# anyway.
|
||||
return enabled_aovs
|
||||
|
||||
default_ext = cmds.getAttr("vraySettings.imageFormatStr")
|
||||
if default_ext in ["exr (multichannel)", "exr (deep)"]:
|
||||
default_ext = "exr"
|
||||
|
||||
# add beauty as default
|
||||
enabled_aovs.append(
|
||||
(u"beauty", default_ext)
|
||||
)
|
||||
|
||||
# handle aovs from references
|
||||
use_ref_aovs = self.render_instance.data.get(
|
||||
"useReferencedAovs", False) or False
|
||||
|
||||
# this will have list of all aovs no matter if they are coming from
|
||||
# reference or not.
|
||||
vr_aovs = cmds.ls(
|
||||
type=["VRayRenderElement", "VRayRenderElementSet"]) or []
|
||||
if not use_ref_aovs:
|
||||
ref_aovs = cmds.ls(
|
||||
type=["VRayRenderElement", "VRayRenderElementSet"],
|
||||
referencedNodes=True) or []
|
||||
# get difference
|
||||
vr_aovs = list(set(vr_aovs) - set(ref_aovs))
|
||||
|
||||
for aov in vr_aovs:
|
||||
enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov)))
|
||||
for override in self.get_layer_overrides(
|
||||
"{}.enabled".format(aov)
|
||||
):
|
||||
enabled = self.maya_is_true(override)
|
||||
|
||||
if enabled:
|
||||
enabled_aovs.append(
|
||||
(self._get_vray_aov_name(aov), default_ext))
|
||||
|
||||
return enabled_aovs
|
||||
|
||||
@staticmethod
|
||||
def _get_vray_aov_name(node):
|
||||
"""Get AOVs name from Vray.
|
||||
|
||||
Args:
|
||||
node (str): aov node name.
|
||||
|
||||
Returns:
|
||||
str: aov name.
|
||||
|
||||
"""
|
||||
vray_name = None
|
||||
vray_explicit_name = None
|
||||
vray_file_name = None
|
||||
for node_attr in cmds.listAttr(node):
|
||||
if node_attr.startswith("vray_filename"):
|
||||
vray_file_name = cmds.getAttr("{}.{}".format(node, node_attr))
|
||||
elif node_attr.startswith("vray_name"):
|
||||
vray_name = cmds.getAttr("{}.{}".format(node, node_attr))
|
||||
elif node_attr.startswith("vray_explicit_name"):
|
||||
vray_explicit_name = cmds.getAttr(
|
||||
"{}.{}".format(node, node_attr))
|
||||
|
||||
if vray_file_name is not None and vray_file_name != "":
|
||||
final_name = vray_file_name
|
||||
elif vray_explicit_name is not None and vray_explicit_name != "":
|
||||
final_name = vray_explicit_name
|
||||
elif vray_name is not None and vray_name != "":
|
||||
final_name = vray_name
|
||||
else:
|
||||
continue
|
||||
# special case for Material Select elements - these are named
|
||||
# based on the materia they are connected to.
|
||||
if "vray_mtl_mtlselect" in cmds.listAttr(node):
|
||||
connections = cmds.listConnections(
|
||||
"{}.vray_mtl_mtlselect".format(node))
|
||||
if connections:
|
||||
final_name += '_{}'.format(str(connections[0]))
|
||||
|
||||
return final_name
|
||||
|
||||
|
||||
class ExpectedFilesRedshift(AExpectedFiles):
|
||||
"""Expected files for Redshift renderer.
|
||||
|
||||
Attributes:
|
||||
|
||||
unmerged_aovs (list): Name of aovs that are not merged into resulting
|
||||
exr and we need them specified in expectedFiles output.
|
||||
|
||||
"""
|
||||
|
||||
unmerged_aovs = ["Cryptomatte"]
|
||||
|
||||
def __init__(self, layer, render_instance):
|
||||
"""Construtor."""
|
||||
super(ExpectedFilesRedshift, self).__init__(layer, render_instance)
|
||||
self.renderer = "redshift"
|
||||
|
||||
def get_renderer_prefix(self):
|
||||
"""Get image prefix for Redshift.
|
||||
|
||||
This overrides :func:`AExpectedFiles.get_renderer_prefix()` as
|
||||
we must add `<aov>` token manually.
|
||||
|
||||
See also:
|
||||
:func:`AExpectedFiles.get_renderer_prefix()`
|
||||
|
||||
"""
|
||||
prefix = super(ExpectedFilesRedshift, self).get_renderer_prefix()
|
||||
prefix = "{}.<aov>".format(prefix)
|
||||
return prefix
|
||||
|
||||
def get_files(self):
|
||||
"""Get expected files.
|
||||
|
||||
This overrides :func:`AExpectedFiles.get_files()` as we
|
||||
we need to add one sequence for plain beauty if AOVs are enabled
|
||||
as vray output beauty without 'beauty' in filename.
|
||||
|
||||
"""
|
||||
expected_files = super(ExpectedFilesRedshift, self).get_files()
|
||||
layer_data = self._get_layer_data()
|
||||
|
||||
# Redshift doesn't merge Cryptomatte AOV to final exr. We need to check
|
||||
# for such condition and add it to list of expected files.
|
||||
|
||||
for aov in layer_data.enabledAOVs:
|
||||
if aov[0].lower() == "cryptomatte":
|
||||
aov_name = aov[0]
|
||||
expected_files.append(
|
||||
{aov_name: self._generate_single_file_sequence(layer_data)}
|
||||
)
|
||||
|
||||
if layer_data.get("enabledAOVs"):
|
||||
# because if Beauty is added manually, it will be rendered as
|
||||
# 'Beauty_other' in file name and "standard" beauty will have
|
||||
# 'Beauty' in its name. When disabled, standard output will be
|
||||
# without `Beauty`.
|
||||
if expected_files[0].get(u"Beauty"):
|
||||
expected_files[0][u"Beauty_other"] = expected_files[0].pop(
|
||||
u"Beauty")
|
||||
new_list = [
|
||||
seq.replace(".Beauty", ".Beauty_other")
|
||||
for seq in expected_files[0][u"Beauty_other"]
|
||||
]
|
||||
|
||||
expected_files[0][u"Beauty_other"] = new_list
|
||||
expected_files[0][u"Beauty"] = self._generate_single_file_sequence( # noqa: E501
|
||||
layer_data, force_aov_name="Beauty"
|
||||
)
|
||||
else:
|
||||
expected_files[0][u"Beauty"] = self._generate_single_file_sequence( # noqa: E501
|
||||
layer_data
|
||||
)
|
||||
|
||||
return expected_files
|
||||
|
||||
def get_aovs(self):
|
||||
"""Get all AOVs.
|
||||
|
||||
See Also:
|
||||
:func:`AExpectedFiles.get_aovs()`
|
||||
|
||||
"""
|
||||
enabled_aovs = []
|
||||
|
||||
try:
|
||||
if self.maya_is_true(
|
||||
cmds.getAttr("redshiftOptions.exrForceMultilayer")
|
||||
):
|
||||
# AOVs are merged in mutli-channel file
|
||||
self.multipart = True
|
||||
return enabled_aovs
|
||||
except ValueError:
|
||||
# this occurs when Render Setting windows was not opened yet. In
|
||||
# such case there are no Redshift options created so query for AOVs
|
||||
# will fail. We terminate here as there are no AOVs specified then.
|
||||
# This state will most probably fail later on some Validator
|
||||
# anyway.
|
||||
return enabled_aovs
|
||||
|
||||
default_ext = cmds.getAttr(
|
||||
"redshiftOptions.imageFormat", asString=True)
|
||||
rs_aovs = cmds.ls(type="RedshiftAOV", referencedNodes=False)
|
||||
|
||||
for aov in rs_aovs:
|
||||
enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov)))
|
||||
for override in self.get_layer_overrides(
|
||||
"{}.enabled".format(aov)
|
||||
):
|
||||
enabled = self.maya_is_true(override)
|
||||
|
||||
if enabled:
|
||||
# If AOVs are merged into multipart exr, append AOV only if it
|
||||
# is in the list of AOVs that renderer cannot (or will not)
|
||||
# merge into final exr.
|
||||
if self.maya_is_true(
|
||||
cmds.getAttr("redshiftOptions.exrForceMultilayer")
|
||||
):
|
||||
if cmds.getAttr("%s.name" % aov) in self.unmerged_aovs:
|
||||
enabled_aovs.append(
|
||||
(cmds.getAttr("%s.name" % aov), default_ext)
|
||||
)
|
||||
else:
|
||||
enabled_aovs.append(
|
||||
(cmds.getAttr("%s.name" % aov), default_ext)
|
||||
)
|
||||
|
||||
if self.maya_is_true(
|
||||
cmds.getAttr("redshiftOptions.exrForceMultilayer")
|
||||
):
|
||||
# AOVs are merged in mutli-channel file
|
||||
self.multipart = True
|
||||
|
||||
return enabled_aovs
|
||||
|
||||
|
||||
class ExpectedFilesRenderman(AExpectedFiles):
|
||||
"""Expected files for Renderman renderer.
|
||||
|
||||
Warning:
|
||||
This is very rudimentary and needs more love and testing.
|
||||
"""
|
||||
|
||||
def __init__(self, layer, render_instance):
|
||||
"""Constructor."""
|
||||
super(ExpectedFilesRenderman, self).__init__(layer, render_instance)
|
||||
self.renderer = "renderman"
|
||||
|
||||
def get_aovs(self):
|
||||
"""Get all AOVs.
|
||||
|
||||
See Also:
|
||||
:func:`AExpectedFiles.get_aovs()`
|
||||
|
||||
"""
|
||||
enabled_aovs = []
|
||||
|
||||
default_ext = "exr"
|
||||
displays = cmds.listConnections("rmanGlobals.displays")
|
||||
for aov in displays:
|
||||
aov_name = str(aov)
|
||||
if aov_name == "rmanDefaultDisplay":
|
||||
aov_name = "beauty"
|
||||
|
||||
enabled = self.maya_is_true(cmds.getAttr("{}.enable".format(aov)))
|
||||
for override in self.get_layer_overrides(
|
||||
"{}.enable".format(aov)
|
||||
):
|
||||
enabled = self.maya_is_true(override)
|
||||
|
||||
if enabled:
|
||||
enabled_aovs.append((aov_name, default_ext))
|
||||
|
||||
return enabled_aovs
|
||||
|
||||
def get_files(self):
|
||||
"""Get expected files.
|
||||
|
||||
This overrides :func:`AExpectedFiles.get_files()` as we
|
||||
we need to add one sequence for plain beauty if AOVs are enabled
|
||||
as vray output beauty without 'beauty' in filename.
|
||||
|
||||
In renderman we hack it with prepending path. This path would
|
||||
normally be translated from `rmanGlobals.imageOutputDir`. We skip
|
||||
this and hardcode prepend path we expect. There is no place for user
|
||||
to mess around with this settings anyway and it is enforced in
|
||||
render settings validator.
|
||||
"""
|
||||
layer_data = self._get_layer_data()
|
||||
new_aovs = {}
|
||||
|
||||
expected_files = super(ExpectedFilesRenderman, self).get_files()
|
||||
# we always get beauty
|
||||
for aov, files in expected_files[0].items():
|
||||
new_files = []
|
||||
for file in files:
|
||||
new_file = "{}/{}/{}".format(
|
||||
layer_data["sceneName"], layer_data["layerName"], file
|
||||
)
|
||||
new_files.append(new_file)
|
||||
new_aovs[aov] = new_files
|
||||
|
||||
return [new_aovs]
|
||||
|
||||
|
||||
class ExpectedFilesMentalray(AExpectedFiles):
|
||||
"""Skeleton unimplemented class for Mentalray renderer."""
|
||||
|
||||
def __init__(self, layer, render_instance):
|
||||
"""Constructor.
|
||||
|
||||
Raises:
|
||||
:exc:`UnimplementedRendererException`: as it is not implemented.
|
||||
|
||||
"""
|
||||
super(ExpectedFilesMentalray, self).__init__(layer, render_instance)
|
||||
raise UnimplementedRendererException("Mentalray not implemented")
|
||||
|
||||
def get_aovs(self):
|
||||
"""Get all AOVs.
|
||||
|
||||
See Also:
|
||||
:func:`AExpectedFiles.get_aovs()`
|
||||
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
class AOVError(Exception):
|
||||
"""Custom exception for determining AOVs."""
|
||||
|
||||
|
||||
class UnsupportedRendererException(Exception):
|
||||
"""Custom exception.
|
||||
|
||||
Raised when requesting data from unsupported renderer.
|
||||
"""
|
||||
|
||||
|
||||
class UnimplementedRendererException(Exception):
|
||||
"""Custom exception.
|
||||
|
||||
Raised when requesting data from renderer that is not implemented yet.
|
||||
"""
|
||||
|
|
@ -2252,10 +2252,8 @@ def get_attr_in_layer(attr, layer):
|
|||
|
||||
try:
|
||||
if cmds.mayaHasRenderSetup():
|
||||
log.debug("lib.get_attr_in_layer is not "
|
||||
"optimized for render setup")
|
||||
with renderlayer(layer):
|
||||
return cmds.getAttr(attr)
|
||||
from . import lib_rendersetup
|
||||
return lib_rendersetup.get_attr_in_layer(attr, layer)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
|
|
|||
1039
openpype/hosts/maya/api/lib_renderproducts.py
Normal file
343
openpype/hosts/maya/api/lib_rendersetup.py
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Library for handling Render Setup in Maya."""
|
||||
from maya import cmds
|
||||
import maya.api.OpenMaya as om
|
||||
import logging
|
||||
|
||||
import maya.app.renderSetup.model.utils as utils
|
||||
from maya.app.renderSetup.model import (
|
||||
renderSetup
|
||||
)
|
||||
from maya.app.renderSetup.model.override import (
|
||||
AbsOverride,
|
||||
RelOverride,
|
||||
UniqueOverride
|
||||
)
|
||||
|
||||
ExactMatch = 0
|
||||
ParentMatch = 1
|
||||
ChildMatch = 2
|
||||
|
||||
DefaultRenderLayer = "defaultRenderLayer"
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_rendersetup_layer(layer):
|
||||
"""Return render setup layer name.
|
||||
|
||||
This also converts names from legacy renderLayer node name to render setup
|
||||
name.
|
||||
|
||||
Note: `defaultRenderLayer` is not a renderSetupLayer node but it is however
|
||||
the valid layer name for Render Setup - so we return that as is.
|
||||
|
||||
Example:
|
||||
>>> for legacy_layer in cmds.ls(type="renderLayer"):
|
||||
>>> layer = get_rendersetup_layer(legacy_layer)
|
||||
|
||||
Returns:
|
||||
str or None: Returns renderSetupLayer node name if `layer` is a valid
|
||||
layer name in legacy renderlayers or render setup layers.
|
||||
Returns None if the layer can't be found or Render Setup is
|
||||
currently disabled.
|
||||
|
||||
|
||||
"""
|
||||
if layer == DefaultRenderLayer:
|
||||
# defaultRenderLayer doesn't have a `renderSetupLayer`
|
||||
return layer
|
||||
|
||||
if not cmds.mayaHasRenderSetup():
|
||||
return None
|
||||
|
||||
if not cmds.objExists(layer):
|
||||
return None
|
||||
|
||||
if cmds.nodeType(layer) == "renderSetupLayer":
|
||||
return layer
|
||||
|
||||
# By default Render Setup renames the legacy renderlayer
|
||||
# to `rs_<layername>` but lets not rely on that as the
|
||||
# layer node can be renamed manually
|
||||
connections = cmds.listConnections(layer + ".message",
|
||||
type="renderSetupLayer",
|
||||
exactType=True,
|
||||
source=False,
|
||||
destination=True,
|
||||
plugs=True) or []
|
||||
return next((conn.split(".", 1)[0] for conn in connections
|
||||
if conn.endswith(".legacyRenderLayer")), None)
|
||||
|
||||
|
||||
def get_attr_in_layer(node_attr, layer):
|
||||
"""Return attribute value in Render Setup layer.
|
||||
|
||||
This will only work for attributes which can be
|
||||
retrieved with `maya.cmds.getAttr` and for which
|
||||
Relative and Absolute overrides are applicable.
|
||||
|
||||
Examples:
|
||||
>>> get_attr_in_layer("defaultResolution.width", layer="layer1")
|
||||
>>> get_attr_in_layer("defaultRenderGlobals.startFrame", layer="layer")
|
||||
>>> get_attr_in_layer("transform.translate", layer="layer3")
|
||||
|
||||
Args:
|
||||
attr (str): attribute name as 'node.attribute'
|
||||
layer (str): layer name
|
||||
|
||||
Returns:
|
||||
object: attribute value in layer
|
||||
|
||||
"""
|
||||
|
||||
# Delay pymel import to here because it's slow to load
|
||||
import pymel.core as pm
|
||||
|
||||
def _layer_needs_update(layer):
|
||||
"""Return whether layer needs updating."""
|
||||
# Use `getattr` as e.g. DefaultRenderLayer does not have the attribute
|
||||
return getattr(layer, "needsMembershipUpdate", False) or \
|
||||
getattr(layer, "needsApplyUpdate", False)
|
||||
|
||||
def get_default_layer_value(node_attr_):
|
||||
"""Return attribute value in defaultRenderLayer"""
|
||||
inputs = cmds.listConnections(node_attr_,
|
||||
source=True,
|
||||
destination=False,
|
||||
# We want to skip conversion nodes since
|
||||
# an override to `endFrame` could have
|
||||
# a `unitToTimeConversion` node
|
||||
# in-between
|
||||
skipConversionNodes=True,
|
||||
type="applyOverride") or []
|
||||
if inputs:
|
||||
_override = inputs[0]
|
||||
history_overrides = cmds.ls(cmds.listHistory(_override,
|
||||
pruneDagObjects=True),
|
||||
type="applyOverride")
|
||||
node = history_overrides[-1] if history_overrides else _override
|
||||
node_attr_ = node + ".original"
|
||||
|
||||
return pm.getAttr(node_attr_, asString=True)
|
||||
|
||||
layer = get_rendersetup_layer(layer)
|
||||
rs = renderSetup.instance()
|
||||
current_layer = rs.getVisibleRenderLayer()
|
||||
if current_layer.name() == layer:
|
||||
|
||||
# Ensure layer is up-to-date
|
||||
if _layer_needs_update(current_layer):
|
||||
try:
|
||||
rs.switchToLayer(current_layer)
|
||||
except RuntimeError:
|
||||
# Some cases can cause errors on switching
|
||||
# the first time with Render Setup layers
|
||||
# e.g. different overrides to compounds
|
||||
# and its children plugs. So we just force
|
||||
# it another time. If it then still fails
|
||||
# we will let it error out.
|
||||
rs.switchToLayer(current_layer)
|
||||
|
||||
return pm.getAttr(node_attr, asString=True)
|
||||
|
||||
overrides = get_attr_overrides(node_attr, layer)
|
||||
default_layer_value = get_default_layer_value(node_attr)
|
||||
if not overrides:
|
||||
return default_layer_value
|
||||
|
||||
value = default_layer_value
|
||||
for match, layer_override, index in overrides:
|
||||
if isinstance(layer_override, AbsOverride):
|
||||
# Absolute override
|
||||
value = pm.getAttr(layer_override.name() + ".attrValue")
|
||||
if match == ExactMatch:
|
||||
value = value
|
||||
if match == ParentMatch:
|
||||
value = value[index]
|
||||
if match == ChildMatch:
|
||||
value[index] = value
|
||||
|
||||
elif isinstance(layer_override, RelOverride):
|
||||
# Relative override
|
||||
# Value = Original * Multiply + Offset
|
||||
multiply = pm.getAttr(layer_override.name() + ".multiply")
|
||||
offset = pm.getAttr(layer_override.name() + ".offset")
|
||||
|
||||
if match == ExactMatch:
|
||||
value = value * multiply + offset
|
||||
if match == ParentMatch:
|
||||
value = value * multiply[index] + offset[index]
|
||||
if match == ChildMatch:
|
||||
value[index] = value[index] * multiply + offset
|
||||
|
||||
else:
|
||||
raise TypeError("Unsupported override: %s" % layer_override)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def get_attr_overrides(node_attr, layer,
|
||||
skip_disabled=True,
|
||||
skip_local_render=True,
|
||||
stop_at_absolute_override=True):
|
||||
"""Return all Overrides applicable to the attribute.
|
||||
|
||||
Overrides are returned as a 3-tuple:
|
||||
(Match, Override, Index)
|
||||
|
||||
Match:
|
||||
This is any of ExactMatch, ParentMatch, ChildMatch
|
||||
and defines whether the override is exactly on the
|
||||
plug, on the parent or on a child plug.
|
||||
|
||||
Override:
|
||||
This is the RenderSetup Override instance.
|
||||
|
||||
Index:
|
||||
This is the Plug index under the parent or for
|
||||
the child that matches. The ExactMatch index will
|
||||
always be None. For ParentMatch the index is which
|
||||
index the plug is under the parent plug. For ChildMatch
|
||||
the index is which child index matches the plug.
|
||||
|
||||
Args:
|
||||
node_attr (str): attribute name as 'node.attribute'
|
||||
layer (str): layer name
|
||||
skip_disabled (bool): exclude disabled overrides
|
||||
skip_local_render (bool): exclude overrides marked
|
||||
as local render.
|
||||
stop_at_absolute_override: exclude overrides prior
|
||||
to the last absolute override as they have
|
||||
no influence on the resulting value.
|
||||
|
||||
Returns:
|
||||
list: Ordered Overrides in order of strength
|
||||
|
||||
"""
|
||||
|
||||
def get_mplug_children(plug):
|
||||
"""Return children MPlugs of compound MPlug"""
|
||||
children = []
|
||||
if plug.isCompound:
|
||||
for i in range(plug.numChildren()):
|
||||
children.append(plug.child(i))
|
||||
return children
|
||||
|
||||
def get_mplug_names(mplug):
|
||||
"""Return long and short name of MPlug"""
|
||||
long_name = mplug.partialName(useLongNames=True)
|
||||
short_name = mplug.partialName(useLongNames=False)
|
||||
return {long_name, short_name}
|
||||
|
||||
def iter_override_targets(_override):
|
||||
try:
|
||||
for target in _override._targets():
|
||||
yield target
|
||||
except AssertionError:
|
||||
# Workaround: There is a bug where the private `_targets()` method
|
||||
# fails on some attribute plugs. For example overrides
|
||||
# to the defaultRenderGlobals.endFrame
|
||||
# (Tested in Maya 2020.2)
|
||||
log.debug("Workaround for %s" % _override)
|
||||
from maya.app.renderSetup.common.utils import findPlug
|
||||
|
||||
attr = _override.attributeName()
|
||||
if isinstance(_override, UniqueOverride):
|
||||
node = _override.targetNodeName()
|
||||
yield findPlug(node, attr)
|
||||
else:
|
||||
nodes = _override.parent().selector().nodes()
|
||||
for node in nodes:
|
||||
if cmds.attributeQuery(attr, node=node, exists=True):
|
||||
yield findPlug(node, attr)
|
||||
|
||||
# Get the MPlug for the node.attr
|
||||
sel = om.MSelectionList()
|
||||
sel.add(node_attr)
|
||||
plug = sel.getPlug(0)
|
||||
|
||||
layer = get_rendersetup_layer(layer)
|
||||
if layer == DefaultRenderLayer:
|
||||
# DefaultRenderLayer will never have overrides
|
||||
# since it's the default layer
|
||||
return []
|
||||
|
||||
rs_layer = renderSetup.instance().getRenderLayer(layer)
|
||||
if rs_layer is None:
|
||||
# Renderlayer does not exist
|
||||
return
|
||||
|
||||
# Get any parent or children plugs as we also
|
||||
# want to include them in the attribute match
|
||||
# for overrides
|
||||
parent = plug.parent() if plug.isChild else None
|
||||
parent_index = None
|
||||
if parent:
|
||||
parent_index = get_mplug_children(parent).index(plug)
|
||||
|
||||
children = get_mplug_children(plug)
|
||||
|
||||
# Create lookup for the attribute by both long
|
||||
# and short names
|
||||
attr_names = get_mplug_names(plug)
|
||||
for child in children:
|
||||
attr_names.update(get_mplug_names(child))
|
||||
if parent:
|
||||
attr_names.update(get_mplug_names(parent))
|
||||
|
||||
# Get all overrides of the layer
|
||||
# And find those that are relevant to the attribute
|
||||
plug_overrides = []
|
||||
|
||||
# Iterate over the overrides in reverse so we get the last
|
||||
# overrides first and can "break" whenever an absolute
|
||||
# override is reached
|
||||
layer_overrides = list(utils.getOverridesRecursive(rs_layer))
|
||||
for layer_override in reversed(layer_overrides):
|
||||
|
||||
if skip_disabled and not layer_override.isEnabled():
|
||||
# Ignore disabled overrides
|
||||
continue
|
||||
|
||||
if skip_local_render and layer_override.isLocalRender():
|
||||
continue
|
||||
|
||||
# The targets list can be very large so we'll do
|
||||
# a quick filter by attribute name to detect whether
|
||||
# it matches the attribute name, or its parent or child
|
||||
if layer_override.attributeName() not in attr_names:
|
||||
continue
|
||||
|
||||
override_match = None
|
||||
for override_plug in iter_override_targets(layer_override):
|
||||
|
||||
override_match = None
|
||||
if plug == override_plug:
|
||||
override_match = (ExactMatch, layer_override, None)
|
||||
|
||||
elif parent and override_plug == parent:
|
||||
override_match = (ParentMatch, layer_override, parent_index)
|
||||
|
||||
elif children and override_plug in children:
|
||||
child_index = children.index(override_plug)
|
||||
override_match = (ChildMatch, layer_override, child_index)
|
||||
|
||||
if override_match:
|
||||
plug_overrides.append(override_match)
|
||||
break
|
||||
|
||||
if (
|
||||
override_match and
|
||||
stop_at_absolute_override and
|
||||
isinstance(layer_override, AbsOverride) and
|
||||
# When the override is only on a child plug then it doesn't
|
||||
# override the entire value so we not stop at this override
|
||||
not override_match[0] == ChildMatch
|
||||
):
|
||||
# If override is absolute override, then BREAK out
|
||||
# of parent loop we don't need to look any further as
|
||||
# this is the absolute override
|
||||
break
|
||||
|
||||
return reversed(plug_overrides)
|
||||
|
|
@ -4,6 +4,8 @@ import os
|
|||
import json
|
||||
import appdirs
|
||||
import requests
|
||||
import six
|
||||
import sys
|
||||
|
||||
from maya import cmds
|
||||
import maya.app.renderSetup.model.renderSetup as renderSetup
|
||||
|
|
@ -12,7 +14,13 @@ from openpype.hosts.maya.api import (
|
|||
lib,
|
||||
plugin
|
||||
)
|
||||
from openpype.api import (get_system_settings, get_asset)
|
||||
from openpype.api import (
|
||||
get_system_settings,
|
||||
get_project_settings,
|
||||
get_asset)
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
from avalon.api import Session
|
||||
|
||||
|
||||
class CreateRender(plugin.Creator):
|
||||
|
|
@ -83,6 +91,32 @@ class CreateRender(plugin.Creator):
|
|||
def __init__(self, *args, **kwargs):
|
||||
"""Constructor."""
|
||||
super(CreateRender, self).__init__(*args, **kwargs)
|
||||
deadline_settings = get_system_settings()["modules"]["deadline"]
|
||||
if not deadline_settings["enabled"]:
|
||||
self.deadline_servers = {}
|
||||
return
|
||||
project_settings = get_project_settings(Session["AVALON_PROJECT"])
|
||||
try:
|
||||
default_servers = deadline_settings["deadline_urls"]
|
||||
project_servers = (
|
||||
project_settings["deadline"]
|
||||
["deadline_servers"]
|
||||
)
|
||||
self.deadline_servers = {
|
||||
k: default_servers[k]
|
||||
for k in project_servers
|
||||
if k in default_servers
|
||||
}
|
||||
|
||||
if not self.deadline_servers:
|
||||
self.deadline_servers = default_servers
|
||||
|
||||
except AttributeError:
|
||||
# Handle situation were we had only one url for deadline.
|
||||
manager = ModulesManager()
|
||||
deadline_module = manager.modules_by_name["deadline"]
|
||||
# get default deadline webservice url from deadline module
|
||||
self.deadline_servers = deadline_module.deadline_urls
|
||||
|
||||
def process(self):
|
||||
"""Entry point."""
|
||||
|
|
@ -94,10 +128,10 @@ class CreateRender(plugin.Creator):
|
|||
use_selection = self.options.get("useSelection")
|
||||
with lib.undo_chunk():
|
||||
self._create_render_settings()
|
||||
instance = super(CreateRender, self).process()
|
||||
self.instance = super(CreateRender, self).process()
|
||||
# create namespace with instance
|
||||
index = 1
|
||||
namespace_name = "_{}".format(str(instance))
|
||||
namespace_name = "_{}".format(str(self.instance))
|
||||
try:
|
||||
cmds.namespace(rm=namespace_name)
|
||||
except RuntimeError:
|
||||
|
|
@ -105,12 +139,20 @@ class CreateRender(plugin.Creator):
|
|||
pass
|
||||
|
||||
while cmds.namespace(exists=namespace_name):
|
||||
namespace_name = "_{}{}".format(str(instance), index)
|
||||
namespace_name = "_{}{}".format(str(self.instance), index)
|
||||
index += 1
|
||||
|
||||
namespace = cmds.namespace(add=namespace_name)
|
||||
|
||||
cmds.setAttr("{}.machineList".format(instance), lock=True)
|
||||
# add Deadline server selection list
|
||||
if self.deadline_servers:
|
||||
cmds.scriptJob(
|
||||
attributeChange=[
|
||||
"{}.deadlineServers".format(self.instance),
|
||||
self._deadline_webservice_changed
|
||||
])
|
||||
|
||||
cmds.setAttr("{}.machineList".format(self.instance), lock=True)
|
||||
self._rs = renderSetup.instance()
|
||||
layers = self._rs.getRenderLayers()
|
||||
if use_selection:
|
||||
|
|
@ -122,7 +164,7 @@ class CreateRender(plugin.Creator):
|
|||
render_set = cmds.sets(
|
||||
n="{}:{}".format(namespace, layer.name()))
|
||||
sets.append(render_set)
|
||||
cmds.sets(sets, forceElement=instance)
|
||||
cmds.sets(sets, forceElement=self.instance)
|
||||
|
||||
# if no render layers are present, create default one with
|
||||
# asterisk selector
|
||||
|
|
@ -138,62 +180,61 @@ class CreateRender(plugin.Creator):
|
|||
renderer = 'renderman'
|
||||
|
||||
self._set_default_renderer_settings(renderer)
|
||||
return self.instance
|
||||
|
||||
def _deadline_webservice_changed(self):
|
||||
"""Refresh Deadline server dependent options."""
|
||||
# get selected server
|
||||
from maya import cmds
|
||||
webservice = self.deadline_servers[
|
||||
self.server_aliases[
|
||||
cmds.getAttr("{}.deadlineServers".format(self.instance))
|
||||
]
|
||||
]
|
||||
pools = self._get_deadline_pools(webservice)
|
||||
cmds.deleteAttr("{}.primaryPool".format(self.instance))
|
||||
cmds.deleteAttr("{}.secondaryPool".format(self.instance))
|
||||
cmds.addAttr(self.instance, longName="primaryPool",
|
||||
attributeType="enum",
|
||||
enumName=":".join(pools))
|
||||
cmds.addAttr(self.instance, longName="secondaryPool",
|
||||
attributeType="enum",
|
||||
enumName=":".join(["-"] + pools))
|
||||
|
||||
def _get_deadline_pools(self, webservice):
|
||||
# type: (str) -> list
|
||||
"""Get pools from Deadline.
|
||||
Args:
|
||||
webservice (str): Server url.
|
||||
Returns:
|
||||
list: Pools.
|
||||
Throws:
|
||||
RuntimeError: If deadline webservice is unreachable.
|
||||
|
||||
"""
|
||||
argument = "{}/api/pools?NamesOnly=true".format(webservice)
|
||||
try:
|
||||
response = self._requests_get(argument)
|
||||
except requests.exceptions.ConnectionError as exc:
|
||||
msg = 'Cannot connect to deadline web service'
|
||||
self.log.error(msg)
|
||||
six.reraise(
|
||||
RuntimeError,
|
||||
RuntimeError('{} - {}'.format(msg, exc)),
|
||||
sys.exc_info()[2])
|
||||
if not response.ok:
|
||||
self.log.warning("No pools retrieved")
|
||||
return []
|
||||
|
||||
return response.json()
|
||||
|
||||
def _create_render_settings(self):
|
||||
"""Create instance settings."""
|
||||
# get pools
|
||||
pools = []
|
||||
|
||||
system_settings = get_system_settings()["modules"]
|
||||
|
||||
deadline_enabled = system_settings["deadline"]["enabled"]
|
||||
muster_enabled = system_settings["muster"]["enabled"]
|
||||
deadline_url = system_settings["deadline"]["DEADLINE_REST_URL"]
|
||||
muster_url = system_settings["muster"]["MUSTER_REST_URL"]
|
||||
|
||||
if deadline_enabled and muster_enabled:
|
||||
self.log.error(
|
||||
"Both Deadline and Muster are enabled. " "Cannot support both."
|
||||
)
|
||||
raise RuntimeError("Both Deadline and Muster are enabled")
|
||||
|
||||
if deadline_enabled:
|
||||
argument = "{}/api/pools?NamesOnly=true".format(deadline_url)
|
||||
try:
|
||||
response = self._requests_get(argument)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
msg = 'Cannot connect to deadline web service'
|
||||
self.log.error(msg)
|
||||
raise RuntimeError('{} - {}'.format(msg, e))
|
||||
if not response.ok:
|
||||
self.log.warning("No pools retrieved")
|
||||
else:
|
||||
pools = response.json()
|
||||
self.data["primaryPool"] = pools
|
||||
# We add a string "-" to allow the user to not
|
||||
# set any secondary pools
|
||||
self.data["secondaryPool"] = ["-"] + pools
|
||||
|
||||
if muster_enabled:
|
||||
self.log.info(">>> Loading Muster credentials ...")
|
||||
self._load_credentials()
|
||||
self.log.info(">>> Getting pools ...")
|
||||
try:
|
||||
pools = self._get_muster_pools()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.startswith("401"):
|
||||
self.log.warning("access token expired")
|
||||
self._show_login()
|
||||
raise RuntimeError("Access token expired")
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.log.error("Cannot connect to Muster API endpoint.")
|
||||
raise RuntimeError("Cannot connect to {}".format(muster_url))
|
||||
pool_names = []
|
||||
for pool in pools:
|
||||
self.log.info(" - pool: {}".format(pool["name"]))
|
||||
pool_names.append(pool["name"])
|
||||
|
||||
self.data["primaryPool"] = pool_names
|
||||
pool_names = []
|
||||
|
||||
self.server_aliases = self.deadline_servers.keys()
|
||||
self.data["deadlineServers"] = self.server_aliases
|
||||
self.data["suspendPublishJob"] = False
|
||||
self.data["review"] = True
|
||||
self.data["extendFrames"] = False
|
||||
|
|
@ -212,6 +253,54 @@ class CreateRender(plugin.Creator):
|
|||
# Disable for now as this feature is not working yet
|
||||
# self.data["assScene"] = False
|
||||
|
||||
system_settings = get_system_settings()["modules"]
|
||||
|
||||
deadline_enabled = system_settings["deadline"]["enabled"]
|
||||
muster_enabled = system_settings["muster"]["enabled"]
|
||||
muster_url = system_settings["muster"]["MUSTER_REST_URL"]
|
||||
|
||||
if deadline_enabled and muster_enabled:
|
||||
self.log.error(
|
||||
"Both Deadline and Muster are enabled. " "Cannot support both."
|
||||
)
|
||||
raise RuntimeError("Both Deadline and Muster are enabled")
|
||||
|
||||
if deadline_enabled:
|
||||
# if default server is not between selected, use first one for
|
||||
# initial list of pools.
|
||||
try:
|
||||
deadline_url = self.deadline_servers["default"]
|
||||
except KeyError:
|
||||
deadline_url = [
|
||||
self.deadline_servers[k]
|
||||
for k in self.deadline_servers.keys()
|
||||
][0]
|
||||
|
||||
pool_names = self._get_deadline_pools(deadline_url)
|
||||
|
||||
if muster_enabled:
|
||||
self.log.info(">>> Loading Muster credentials ...")
|
||||
self._load_credentials()
|
||||
self.log.info(">>> Getting pools ...")
|
||||
pools = []
|
||||
try:
|
||||
pools = self._get_muster_pools()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.startswith("401"):
|
||||
self.log.warning("access token expired")
|
||||
self._show_login()
|
||||
raise RuntimeError("Access token expired")
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.log.error("Cannot connect to Muster API endpoint.")
|
||||
raise RuntimeError("Cannot connect to {}".format(muster_url))
|
||||
for pool in pools:
|
||||
self.log.info(" - pool: {}".format(pool["name"]))
|
||||
pool_names.append(pool["name"])
|
||||
|
||||
self.data["primaryPool"] = pool_names
|
||||
# We add a string "-" to allow the user to not
|
||||
# set any secondary pools
|
||||
self.data["secondaryPool"] = ["-"] + pool_names
|
||||
self.options = {"useSelection": False} # Force no content
|
||||
|
||||
def _load_credentials(self):
|
||||
|
|
@ -293,9 +382,7 @@ class CreateRender(plugin.Creator):
|
|||
|
||||
"""
|
||||
if "verify" not in kwargs:
|
||||
kwargs["verify"] = (
|
||||
False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True
|
||||
) # noqa
|
||||
kwargs["verify"] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True)
|
||||
return requests.post(*args, **kwargs)
|
||||
|
||||
def _requests_get(self, *args, **kwargs):
|
||||
|
|
@ -312,9 +399,7 @@ class CreateRender(plugin.Creator):
|
|||
|
||||
"""
|
||||
if "verify" not in kwargs:
|
||||
kwargs["verify"] = (
|
||||
False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True
|
||||
) # noqa
|
||||
kwargs["verify"] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True)
|
||||
return requests.get(*args, **kwargs)
|
||||
|
||||
def _set_default_renderer_settings(self, renderer):
|
||||
|
|
@ -332,14 +417,10 @@ class CreateRender(plugin.Creator):
|
|||
|
||||
if renderer == "arnold":
|
||||
# set format to exr
|
||||
|
||||
cmds.setAttr(
|
||||
"defaultArnoldDriver.ai_translator", "exr", type="string")
|
||||
# enable animation
|
||||
cmds.setAttr("defaultRenderGlobals.outFormatControl", 0)
|
||||
cmds.setAttr("defaultRenderGlobals.animation", 1)
|
||||
cmds.setAttr("defaultRenderGlobals.putFrameBeforeExt", 1)
|
||||
cmds.setAttr("defaultRenderGlobals.extensionPadding", 4)
|
||||
|
||||
self._set_global_output_settings()
|
||||
# resolution
|
||||
cmds.setAttr(
|
||||
"defaultResolution.width",
|
||||
|
|
@ -349,43 +430,12 @@ class CreateRender(plugin.Creator):
|
|||
asset["data"].get("resolutionHeight"))
|
||||
|
||||
if renderer == "vray":
|
||||
vray_settings = cmds.ls(type="VRaySettingsNode")
|
||||
if not vray_settings:
|
||||
node = cmds.createNode("VRaySettingsNode")
|
||||
else:
|
||||
node = vray_settings[0]
|
||||
|
||||
# set underscore as element separator instead of default `.`
|
||||
cmds.setAttr(
|
||||
"{}.fileNameRenderElementSeparator".format(
|
||||
node),
|
||||
"_"
|
||||
)
|
||||
# set format to exr
|
||||
cmds.setAttr(
|
||||
"{}.imageFormatStr".format(node), 5)
|
||||
|
||||
# animType
|
||||
cmds.setAttr(
|
||||
"{}.animType".format(node), 1)
|
||||
|
||||
# resolution
|
||||
cmds.setAttr(
|
||||
"{}.width".format(node),
|
||||
asset["data"].get("resolutionWidth"))
|
||||
cmds.setAttr(
|
||||
"{}.height".format(node),
|
||||
asset["data"].get("resolutionHeight"))
|
||||
|
||||
self._set_vray_settings(asset)
|
||||
if renderer == "redshift":
|
||||
redshift_settings = cmds.ls(type="RedshiftOptions")
|
||||
if not redshift_settings:
|
||||
node = cmds.createNode("RedshiftOptions")
|
||||
else:
|
||||
node = redshift_settings[0]
|
||||
_ = self._set_renderer_option(
|
||||
"RedshiftOptions", "{}.imageFormat", 1
|
||||
)
|
||||
|
||||
# set exr
|
||||
cmds.setAttr("{}.imageFormat".format(node), 1)
|
||||
# resolution
|
||||
cmds.setAttr(
|
||||
"defaultResolution.width",
|
||||
|
|
@ -394,8 +444,56 @@ class CreateRender(plugin.Creator):
|
|||
"defaultResolution.height",
|
||||
asset["data"].get("resolutionHeight"))
|
||||
|
||||
# enable animation
|
||||
cmds.setAttr("defaultRenderGlobals.outFormatControl", 0)
|
||||
cmds.setAttr("defaultRenderGlobals.animation", 1)
|
||||
cmds.setAttr("defaultRenderGlobals.putFrameBeforeExt", 1)
|
||||
cmds.setAttr("defaultRenderGlobals.extensionPadding", 4)
|
||||
self._set_global_output_settings()
|
||||
|
||||
@staticmethod
|
||||
def _set_renderer_option(renderer_node, arg=None, value=None):
|
||||
# type: (str, str, str) -> str
|
||||
"""Set option on renderer node.
|
||||
|
||||
If renderer settings node doesn't exists, it is created first.
|
||||
|
||||
Args:
|
||||
renderer_node (str): Renderer name.
|
||||
arg (str, optional): Argument name.
|
||||
value (str, optional): Argument value.
|
||||
|
||||
Returns:
|
||||
str: Renderer settings node.
|
||||
|
||||
"""
|
||||
settings = cmds.ls(type=renderer_node)
|
||||
result = settings[0] if settings else cmds.createNode(renderer_node)
|
||||
cmds.setAttr(arg.format(result), value)
|
||||
return result
|
||||
|
||||
def _set_vray_settings(self, asset):
|
||||
# type: (dict) -> None
|
||||
"""Sets important settings for Vray."""
|
||||
node = self._set_renderer_option(
|
||||
"VRaySettingsNode", "{}.fileNameRenderElementSeparator", "_"
|
||||
)
|
||||
|
||||
# set format to exr
|
||||
cmds.setAttr(
|
||||
"{}.imageFormatStr".format(node), 5)
|
||||
|
||||
# animType
|
||||
cmds.setAttr(
|
||||
"{}.animType".format(node), 1)
|
||||
|
||||
# resolution
|
||||
cmds.setAttr(
|
||||
"{}.width".format(node),
|
||||
asset["data"].get("resolutionWidth"))
|
||||
cmds.setAttr(
|
||||
"{}.height".format(node),
|
||||
asset["data"].get("resolutionHeight"))
|
||||
|
||||
@staticmethod
|
||||
def _set_global_output_settings():
|
||||
# enable animation
|
||||
cmds.setAttr("defaultRenderGlobals.outFormatControl", 0)
|
||||
cmds.setAttr("defaultRenderGlobals.animation", 1)
|
||||
cmds.setAttr("defaultRenderGlobals.putFrameBeforeExt", 1)
|
||||
cmds.setAttr("defaultRenderGlobals.extensionPadding", 4)
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ import maya.app.renderSetup.model.renderSetup as renderSetup
|
|||
import pyblish.api
|
||||
|
||||
from avalon import maya, api
|
||||
from openpype.hosts.maya.api.expected_files import ExpectedFiles
|
||||
from openpype.hosts.maya.api.lib_renderproducts import get as get_layer_render_products # noqa: E501
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
||||
|
|
@ -64,6 +64,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
def process(self, context):
|
||||
"""Entry point to collector."""
|
||||
render_instance = None
|
||||
deadline_url = None
|
||||
|
||||
for instance in context:
|
||||
if "rendering" in instance.data["families"]:
|
||||
render_instance = instance
|
||||
|
|
@ -86,6 +88,15 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
asset = api.Session["AVALON_ASSET"]
|
||||
workspace = context.data["workspaceDir"]
|
||||
|
||||
deadline_settings = (
|
||||
context.data
|
||||
["system_settings"]
|
||||
["modules"]
|
||||
["deadline"]
|
||||
)
|
||||
|
||||
if deadline_settings["enabled"]:
|
||||
deadline_url = render_instance.data.get("deadlineUrl")
|
||||
self._rs = renderSetup.instance()
|
||||
current_layer = self._rs.getVisibleRenderLayer()
|
||||
maya_render_layers = {
|
||||
|
|
@ -157,10 +168,21 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
|
||||
# return all expected files for all cameras and aovs in given
|
||||
# frame range
|
||||
ef = ExpectedFiles(render_instance)
|
||||
exp_files = ef.get(renderer, layer_name)
|
||||
self.log.info("multipart: {}".format(ef.multipart))
|
||||
layer_render_products = get_layer_render_products(
|
||||
layer_name, render_instance)
|
||||
render_products = layer_render_products.layer_data.products
|
||||
assert render_products, "no render products generated"
|
||||
exp_files = []
|
||||
for product in render_products:
|
||||
for camera in layer_render_products.layer_data.cameras:
|
||||
exp_files.append(
|
||||
{product.productName: layer_render_products.get_files(
|
||||
product, camera)})
|
||||
|
||||
self.log.info("multipart: {}".format(
|
||||
layer_render_products.multipart))
|
||||
assert exp_files, "no file names were generated, this is bug"
|
||||
self.log.info(exp_files)
|
||||
|
||||
# if we want to attach render to subset, check if we have AOV's
|
||||
# in expectedFiles. If so, raise error as we cannot attach AOV
|
||||
|
|
@ -175,24 +197,15 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
full_exp_files = []
|
||||
aov_dict = {}
|
||||
|
||||
# we either get AOVs or just list of files. List of files can
|
||||
# mean two things - there are no AOVs enabled or multipass EXR
|
||||
# is produced. In either case we treat those as `beauty`.
|
||||
if isinstance(exp_files[0], dict):
|
||||
for aov, files in exp_files[0].items():
|
||||
full_paths = []
|
||||
for e in files:
|
||||
full_path = os.path.join(workspace, "renders", e)
|
||||
full_path = full_path.replace("\\", "/")
|
||||
full_paths.append(full_path)
|
||||
aov_dict[aov] = full_paths
|
||||
else:
|
||||
# replace relative paths with absolute. Render products are
|
||||
# returned as list of dictionaries.
|
||||
for aov in exp_files:
|
||||
full_paths = []
|
||||
for e in exp_files:
|
||||
full_path = os.path.join(workspace, "renders", e)
|
||||
for file in aov[aov.keys()[0]]:
|
||||
full_path = os.path.join(workspace, "renders", file)
|
||||
full_path = full_path.replace("\\", "/")
|
||||
full_paths.append(full_path)
|
||||
aov_dict["beauty"] = full_paths
|
||||
aov_dict[aov.keys()[0]] = full_paths
|
||||
|
||||
frame_start_render = int(self.get_render_attribute(
|
||||
"startFrame", layer=layer_name))
|
||||
|
|
@ -224,7 +237,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
"subset": expected_layer_name,
|
||||
"attachTo": attach_to,
|
||||
"setMembers": layer_name,
|
||||
"multipartExr": ef.multipart,
|
||||
"multipartExr": layer_render_products.multipart,
|
||||
"review": render_instance.data.get("review") or False,
|
||||
"publish": True,
|
||||
|
||||
|
|
@ -263,6 +276,9 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
"vrayUseReferencedAovs") or False
|
||||
}
|
||||
|
||||
if deadline_url:
|
||||
data["deadlineUrl"] = deadline_url
|
||||
|
||||
if self.sync_workfile_version:
|
||||
data["version"] = context.data["version"]
|
||||
|
||||
|
|
@ -306,10 +322,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
instance.data.update(data)
|
||||
self.log.debug("data: {}".format(json.dumps(data, indent=4)))
|
||||
|
||||
# Restore current layer.
|
||||
self.log.info("Restoring to {}".format(current_layer.name()))
|
||||
self._rs.switchToLayer(current_layer)
|
||||
|
||||
def parse_options(self, render_globals):
|
||||
"""Get all overrides with a value, skip those without.
|
||||
|
||||
|
|
@ -392,11 +404,13 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
rset = self.maya_layers[layer].renderSettingsCollectionInstance()
|
||||
return rset.getOverrides()
|
||||
|
||||
def get_render_attribute(self, attr, layer):
|
||||
@staticmethod
|
||||
def get_render_attribute(attr, layer):
|
||||
"""Get attribute from render options.
|
||||
|
||||
Args:
|
||||
attr (str): name of attribute to be looked up.
|
||||
attr (str): name of attribute to be looked up
|
||||
layer (str): name of render layer
|
||||
|
||||
Returns:
|
||||
Attribute value
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ class ExtractReviewDataLut(openpype.api.Extractor):
|
|||
|
||||
if "render.farm" in families:
|
||||
instance.data["families"].remove("review")
|
||||
instance.data["families"].remove("ftrack")
|
||||
|
||||
self.log.debug(
|
||||
"_ lutPath: {}".format(instance.data["lutPath"]))
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ class ExtractReviewDataMov(openpype.api.Extractor):
|
|||
|
||||
if "render.farm" in families:
|
||||
instance.data["families"].remove("review")
|
||||
instance.data["families"].remove("ftrack")
|
||||
data = exporter.generate_mov(farm=True)
|
||||
|
||||
self.log.debug(
|
||||
|
|
|
|||
|
|
@ -415,13 +415,11 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
"""Plugin entry point."""
|
||||
self._instance = instance
|
||||
context = instance.context
|
||||
self._deadline_url = (
|
||||
context.data["system_settings"]
|
||||
["modules"]
|
||||
["deadline"]
|
||||
["DEADLINE_REST_URL"]
|
||||
)
|
||||
assert self._deadline_url, "Requires DEADLINE_REST_URL"
|
||||
self._deadline_url = context.data.get("defaultDeadline")
|
||||
self._deadline_url = instance.data.get(
|
||||
"deadlineUrl", self._deadline_url)
|
||||
|
||||
assert self._deadline_url, "Requires Deadline Webservice URL"
|
||||
|
||||
file_path = None
|
||||
if self.use_published:
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ class PypeStreamHandler(logging.StreamHandler):
|
|||
msg = self.format(record)
|
||||
msg = Terminal.log(msg)
|
||||
stream = self.stream
|
||||
if stream is None:
|
||||
return
|
||||
fs = "%s\n"
|
||||
# if no unicode support...
|
||||
if not USE_UNICODE:
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ from .muster import MusterModule
|
|||
from .deadline import DeadlineModule
|
||||
from .project_manager_action import ProjectManagerAction
|
||||
from .standalonepublish_action import StandAlonePublishAction
|
||||
from .python_console_interpreter import PythonInterpreterAction
|
||||
from .sync_server import SyncServerModule
|
||||
from .slack import SlackIntegrationModule
|
||||
|
||||
|
|
@ -77,6 +78,7 @@ __all__ = (
|
|||
"DeadlineModule",
|
||||
"ProjectManagerAction",
|
||||
"StandAlonePublishAction",
|
||||
"PythonInterpreterAction",
|
||||
|
||||
"SyncServerModule",
|
||||
|
||||
|
|
|
|||
|
|
@ -6,17 +6,25 @@ from openpype.modules import (
|
|||
class DeadlineModule(PypeModule, IPluginPaths):
|
||||
name = "deadline"
|
||||
|
||||
def __init__(self, manager, settings):
|
||||
self.deadline_urls = {}
|
||||
super(DeadlineModule, self).__init__(manager, settings)
|
||||
|
||||
def initialize(self, modules_settings):
|
||||
# This module is always enabled
|
||||
deadline_settings = modules_settings[self.name]
|
||||
self.enabled = deadline_settings["enabled"]
|
||||
self.deadline_url = deadline_settings["DEADLINE_REST_URL"]
|
||||
deadline_url = deadline_settings.get("DEADLINE_REST_URL")
|
||||
if deadline_url:
|
||||
self.deadline_urls = {"default": deadline_url}
|
||||
else:
|
||||
self.deadline_urls = deadline_settings.get("deadline_urls") # noqa: E501
|
||||
|
||||
def get_global_environments(self):
|
||||
"""Deadline global environments for OpenPype implementation."""
|
||||
return {
|
||||
"DEADLINE_REST_URL": self.deadline_url
|
||||
}
|
||||
if not self.deadline_urls:
|
||||
self.enabled = False
|
||||
self.log.warning(("default Deadline Webservice URL "
|
||||
"not specified. Disabling module."))
|
||||
return
|
||||
|
||||
def connect_with_modules(self, *_a, **_kw):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect Deadline servers from instance.
|
||||
|
||||
This is resolving index of server lists stored in `deadlineServers` instance
|
||||
attribute or using default server if that attribute doesn't exists.
|
||||
|
||||
"""
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin):
|
||||
"""Collect Deadline Webservice URL from instance."""
|
||||
|
||||
order = pyblish.api.CollectorOrder
|
||||
label = "Deadline Webservice from the Instance"
|
||||
families = ["rendering"]
|
||||
|
||||
def process(self, instance):
|
||||
instance.data["deadlineUrl"] = self._collect_deadline_url(instance)
|
||||
self.log.info(
|
||||
"Using {} for submission.".format(instance.data["deadlineUrl"]))
|
||||
|
||||
@staticmethod
|
||||
def _collect_deadline_url(render_instance):
|
||||
# type: (pyblish.api.Instance) -> str
|
||||
"""Get Deadline Webservice URL from render instance.
|
||||
|
||||
This will get all configured Deadline Webservice URLs and create
|
||||
subset of them based upon project configuration. It will then take
|
||||
`deadlineServers` from render instance that is now basically `int`
|
||||
index of that list.
|
||||
|
||||
Args:
|
||||
render_instance (pyblish.api.Instance): Render instance created
|
||||
by Creator in Maya.
|
||||
|
||||
Returns:
|
||||
str: Selected Deadline Webservice URL.
|
||||
|
||||
"""
|
||||
|
||||
deadline_settings = (
|
||||
render_instance.context.data
|
||||
["system_settings"]
|
||||
["modules"]
|
||||
["deadline"]
|
||||
)
|
||||
|
||||
try:
|
||||
default_servers = deadline_settings["deadline_urls"]
|
||||
project_servers = (
|
||||
render_instance.context.data
|
||||
["project_settings"]
|
||||
["deadline"]
|
||||
["deadline_servers"]
|
||||
)
|
||||
deadline_servers = {
|
||||
k: default_servers[k]
|
||||
for k in project_servers
|
||||
if k in default_servers
|
||||
}
|
||||
|
||||
except AttributeError:
|
||||
# Handle situation were we had only one url for deadline.
|
||||
return render_instance.context.data["defaultDeadline"]
|
||||
|
||||
return deadline_servers[
|
||||
list(deadline_servers.keys())[
|
||||
int(render_instance.data.get("deadlineServers"))
|
||||
]
|
||||
]
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect default Deadline server."""
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin):
|
||||
"""Collect default Deadline Webservice URL."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.01
|
||||
label = "Default Deadline Webservice"
|
||||
|
||||
def process(self, context):
|
||||
try:
|
||||
deadline_module = context.data.get("openPypeModules")["deadline"]
|
||||
except AttributeError:
|
||||
self.log.error("Cannot get OpenPype Deadline module.")
|
||||
raise AssertionError("OpenPype Deadline module not found.")
|
||||
|
||||
# get default deadline webservice url from deadline module
|
||||
self.log.debug(deadline_module.deadline_urls)
|
||||
context.data["defaultDeadline"] = deadline_module.deadline_urls["default"] # noqa: E501
|
||||
|
|
@ -199,7 +199,7 @@ def get_renderer_variables(renderlayer, root):
|
|||
if extension is None:
|
||||
extension = "png"
|
||||
|
||||
if extension == "exr (multichannel)" or extension == "exr (deep)":
|
||||
if extension in ["exr (multichannel)", "exr (deep)"]:
|
||||
extension = "exr"
|
||||
|
||||
prefix_attr = "vraySettings.fileNamePrefix"
|
||||
|
|
@ -264,12 +264,13 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
|
||||
self._instance = instance
|
||||
self.payload_skeleton = copy.deepcopy(payload_skeleton_template)
|
||||
self._deadline_url = (
|
||||
context.data["system_settings"]
|
||||
["modules"]
|
||||
["deadline"]
|
||||
["DEADLINE_REST_URL"]
|
||||
)
|
||||
|
||||
# get default deadline webservice url from deadline module
|
||||
self.deadline_url = instance.context.data.get("defaultDeadline")
|
||||
# if custom one is set in instance, use that
|
||||
if instance.data.get("deadlineUrl"):
|
||||
self.deadline_url = instance.data.get("deadlineUrl")
|
||||
assert self.deadline_url, "Requires Deadline Webservice URL"
|
||||
|
||||
self._job_info = (
|
||||
context.data["project_settings"].get(
|
||||
|
|
@ -287,65 +288,76 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
"pluginInfo", {})
|
||||
)
|
||||
|
||||
assert self._deadline_url, "Requires DEADLINE_REST_URL"
|
||||
|
||||
context = instance.context
|
||||
workspace = context.data["workspaceDir"]
|
||||
anatomy = context.data['anatomy']
|
||||
instance.data["toBeRenderedOn"] = "deadline"
|
||||
|
||||
filepath = None
|
||||
patches = (
|
||||
context.data["project_settings"].get(
|
||||
"deadline", {}).get(
|
||||
"publish", {}).get(
|
||||
"MayaSubmitDeadline", {}).get(
|
||||
"scene_patches", {})
|
||||
)
|
||||
|
||||
# Handle render/export from published scene or not ------------------
|
||||
if self.use_published:
|
||||
patched_files = []
|
||||
for i in context:
|
||||
if "workfile" in i.data["families"]:
|
||||
assert i.data["publish"] is True, (
|
||||
"Workfile (scene) must be published along")
|
||||
template_data = i.data.get("anatomyData")
|
||||
rep = i.data.get("representations")[0].get("name")
|
||||
template_data["representation"] = rep
|
||||
template_data["ext"] = rep
|
||||
template_data["comment"] = None
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
template_filled = anatomy_filled["publish"]["path"]
|
||||
filepath = os.path.normpath(template_filled)
|
||||
self.log.info("Using published scene for render {}".format(
|
||||
filepath))
|
||||
if "workfile" not in i.data["families"]:
|
||||
continue
|
||||
assert i.data["publish"] is True, (
|
||||
"Workfile (scene) must be published along")
|
||||
template_data = i.data.get("anatomyData")
|
||||
rep = i.data.get("representations")[0].get("name")
|
||||
template_data["representation"] = rep
|
||||
template_data["ext"] = rep
|
||||
template_data["comment"] = None
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
template_filled = anatomy_filled["publish"]["path"]
|
||||
filepath = os.path.normpath(template_filled)
|
||||
self.log.info("Using published scene for render {}".format(
|
||||
filepath))
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
self.log.error("published scene does not exist!")
|
||||
raise
|
||||
# now we need to switch scene in expected files
|
||||
# because <scene> token will now point to published
|
||||
# scene file and that might differ from current one
|
||||
new_scene = os.path.splitext(
|
||||
os.path.basename(filepath))[0]
|
||||
orig_scene = os.path.splitext(
|
||||
os.path.basename(context.data["currentFile"]))[0]
|
||||
exp = instance.data.get("expectedFiles")
|
||||
if not os.path.exists(filepath):
|
||||
self.log.error("published scene does not exist!")
|
||||
raise
|
||||
# now we need to switch scene in expected files
|
||||
# because <scene> token will now point to published
|
||||
# scene file and that might differ from current one
|
||||
new_scene = os.path.splitext(
|
||||
os.path.basename(filepath))[0]
|
||||
orig_scene = os.path.splitext(
|
||||
os.path.basename(context.data["currentFile"]))[0]
|
||||
exp = instance.data.get("expectedFiles")
|
||||
|
||||
if isinstance(exp[0], dict):
|
||||
# we have aovs and we need to iterate over them
|
||||
new_exp = {}
|
||||
for aov, files in exp[0].items():
|
||||
replaced_files = []
|
||||
for f in files:
|
||||
replaced_files.append(
|
||||
f.replace(orig_scene, new_scene)
|
||||
)
|
||||
new_exp[aov] = replaced_files
|
||||
instance.data["expectedFiles"] = [new_exp]
|
||||
else:
|
||||
new_exp = []
|
||||
for f in exp:
|
||||
new_exp.append(
|
||||
if isinstance(exp[0], dict):
|
||||
# we have aovs and we need to iterate over them
|
||||
new_exp = {}
|
||||
for aov, files in exp[0].items():
|
||||
replaced_files = []
|
||||
for f in files:
|
||||
replaced_files.append(
|
||||
f.replace(orig_scene, new_scene)
|
||||
)
|
||||
instance.data["expectedFiles"] = [new_exp]
|
||||
self.log.info("Scene name was switched {} -> {}".format(
|
||||
orig_scene, new_scene
|
||||
))
|
||||
new_exp[aov] = replaced_files
|
||||
instance.data["expectedFiles"] = [new_exp]
|
||||
else:
|
||||
new_exp = []
|
||||
for f in exp:
|
||||
new_exp.append(
|
||||
f.replace(orig_scene, new_scene)
|
||||
)
|
||||
instance.data["expectedFiles"] = [new_exp]
|
||||
self.log.info("Scene name was switched {} -> {}".format(
|
||||
orig_scene, new_scene
|
||||
))
|
||||
# patch workfile is needed
|
||||
if filepath not in patched_files:
|
||||
patched_file = self._patch_workfile(filepath, patches)
|
||||
patched_files.append(patched_file)
|
||||
|
||||
all_instances = []
|
||||
for result in context.data["results"]:
|
||||
|
|
@ -670,7 +682,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
self.log.info(
|
||||
"Submitting tile job(s) [{}] ...".format(len(frame_payloads)))
|
||||
|
||||
url = "{}/api/jobs".format(self._deadline_url)
|
||||
url = "{}/api/jobs".format(self.deadline_url)
|
||||
tiles_count = instance.data.get("tilesX") * instance.data.get("tilesY") # noqa: E501
|
||||
|
||||
for tile_job in frame_payloads:
|
||||
|
|
@ -754,7 +766,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
self.log.debug(json.dumps(payload, indent=4, sort_keys=True))
|
||||
|
||||
# E.g. http://192.168.0.1:8082/api/jobs
|
||||
url = "{}/api/jobs".format(self._deadline_url)
|
||||
url = "{}/api/jobs".format(self.deadline_url)
|
||||
response = self._requests_post(url, json=payload)
|
||||
if not response.ok:
|
||||
raise Exception(response.text)
|
||||
|
|
@ -868,10 +880,11 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
payload["JobInfo"].update(job_info_ext)
|
||||
payload["PluginInfo"].update(plugin_info_ext)
|
||||
|
||||
envs = []
|
||||
for k, v in payload["JobInfo"].items():
|
||||
if k.startswith("EnvironmentKeyValue"):
|
||||
envs.append(v)
|
||||
envs = [
|
||||
v
|
||||
for k, v in payload["JobInfo"].items()
|
||||
if k.startswith("EnvironmentKeyValue")
|
||||
]
|
||||
|
||||
# add app name to environment
|
||||
envs.append(
|
||||
|
|
@ -892,11 +905,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
envs.append(
|
||||
"OPENPYPE_ASS_EXPORT_STEP={}".format(1))
|
||||
|
||||
i = 0
|
||||
for e in envs:
|
||||
for i, e in enumerate(envs):
|
||||
payload["JobInfo"]["EnvironmentKeyValue{}".format(i)] = e
|
||||
i += 1
|
||||
|
||||
return payload
|
||||
|
||||
def _get_vray_render_payload(self, data):
|
||||
|
|
@ -964,7 +974,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
payload = self._get_arnold_export_payload(data)
|
||||
self.log.info("Submitting ass export job.")
|
||||
|
||||
url = "{}/api/jobs".format(self._deadline_url)
|
||||
url = "{}/api/jobs".format(self.deadline_url)
|
||||
response = self._requests_post(url, json=payload)
|
||||
if not response.ok:
|
||||
self.log.error("Submition failed!")
|
||||
|
|
@ -1003,7 +1013,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
|
||||
"""
|
||||
if 'verify' not in kwargs:
|
||||
kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa
|
||||
kwargs['verify'] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True)
|
||||
# add 10sec timeout before bailing out
|
||||
kwargs['timeout'] = 10
|
||||
return requests.post(*args, **kwargs)
|
||||
|
|
@ -1022,7 +1032,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
|
||||
"""
|
||||
if 'verify' not in kwargs:
|
||||
kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa
|
||||
kwargs['verify'] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True)
|
||||
# add 10sec timeout before bailing out
|
||||
kwargs['timeout'] = 10
|
||||
return requests.get(*args, **kwargs)
|
||||
|
|
@ -1069,3 +1079,43 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
result = filename_zero.replace("\\", "/")
|
||||
|
||||
return result
|
||||
|
||||
def _patch_workfile(self, file, patches):
|
||||
# type: (str, dict) -> [str, None]
|
||||
"""Patch Maya scene.
|
||||
|
||||
This will take list of patches (lines to add) and apply them to
|
||||
*published* Maya scene file (that is used later for rendering).
|
||||
|
||||
Patches are dict with following structure::
|
||||
{
|
||||
"name": "Name of patch",
|
||||
"regex": "regex of line before patch",
|
||||
"line": "line to insert"
|
||||
}
|
||||
|
||||
Args:
|
||||
file (str): File to patch.
|
||||
patches (dict): Dictionary defining patches.
|
||||
|
||||
Returns:
|
||||
str: Patched file path or None
|
||||
|
||||
"""
|
||||
if os.path.splitext(file)[1].lower() != ".ma" or not patches:
|
||||
return None
|
||||
|
||||
compiled_regex = [re.compile(p["regex"]) for p in patches]
|
||||
with open(file, "r+") as pf:
|
||||
scene_data = pf.readlines()
|
||||
for ln, line in enumerate(scene_data):
|
||||
for i, r in enumerate(compiled_regex):
|
||||
if re.match(r, line):
|
||||
scene_data.insert(ln + 1, patches[i]["line"])
|
||||
pf.seek(0)
|
||||
pf.writelines(scene_data)
|
||||
pf.truncate()
|
||||
self.log.info(
|
||||
"Applied {} patch to scene.".format(
|
||||
patches[i]["name"]))
|
||||
return file
|
||||
|
|
|
|||
|
|
@ -42,13 +42,12 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
node = instance[0]
|
||||
context = instance.context
|
||||
|
||||
deadline_url = (
|
||||
context.data["system_settings"]
|
||||
["modules"]
|
||||
["deadline"]
|
||||
["DEADLINE_REST_URL"]
|
||||
)
|
||||
assert deadline_url, "Requires DEADLINE_REST_URL"
|
||||
# get default deadline webservice url from deadline module
|
||||
deadline_url = instance.context.data["defaultDeadline"]
|
||||
# if custom one is set in instance, use that
|
||||
if instance.data.get("deadlineUrl"):
|
||||
deadline_url = instance.data.get("deadlineUrl")
|
||||
assert deadline_url, "Requires Deadline Webservice URL"
|
||||
|
||||
self.deadline_url = "{}/api/jobs".format(deadline_url)
|
||||
self._comment = context.data.get("comment", "")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import os
|
|||
import json
|
||||
import re
|
||||
from copy import copy, deepcopy
|
||||
import sys
|
||||
import openpype.api
|
||||
|
||||
from avalon import api, io
|
||||
|
|
@ -615,14 +614,16 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
instance["families"] = families
|
||||
|
||||
def process(self, instance):
|
||||
# type: (pyblish.api.Instance) -> None
|
||||
"""Process plugin.
|
||||
|
||||
Detect type of renderfarm submission and create and post dependend job
|
||||
in case of Deadline. It creates json file with metadata needed for
|
||||
publishing in directory of render.
|
||||
|
||||
:param instance: Instance data
|
||||
:type instance: dict
|
||||
Args:
|
||||
instance (pyblish.api.Instance): Instance data.
|
||||
|
||||
"""
|
||||
data = instance.data.copy()
|
||||
context = instance.context
|
||||
|
|
@ -908,13 +909,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
}
|
||||
|
||||
if submission_type == "deadline":
|
||||
self.deadline_url = (
|
||||
context.data["system_settings"]
|
||||
["modules"]
|
||||
["deadline"]
|
||||
["DEADLINE_REST_URL"]
|
||||
)
|
||||
assert self.deadline_url, "Requires DEADLINE_REST_URL"
|
||||
# get default deadline webservice url from deadline module
|
||||
self.deadline_url = instance.context.data["defaultDeadline"]
|
||||
# if custom one is set in instance, use that
|
||||
if instance.data.get("deadlineUrl"):
|
||||
self.deadline_url = instance.data.get("deadlineUrl")
|
||||
assert self.deadline_url, "Requires Deadline Webservice URL"
|
||||
|
||||
self._submit_deadline_post_job(instance, render_job, instances)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import pyblish.api
|
||||
|
||||
from avalon.vendor import requests
|
||||
from openpype.plugin import contextplugin_should_run
|
||||
import os
|
||||
|
||||
|
||||
class ValidateDeadlineConnection(pyblish.api.ContextPlugin):
|
||||
class ValidateDeadlineConnection(pyblish.api.InstancePlugin):
|
||||
"""Validate Deadline Web Service is running"""
|
||||
|
||||
label = "Validate Deadline Web Service"
|
||||
|
|
@ -13,18 +12,16 @@ class ValidateDeadlineConnection(pyblish.api.ContextPlugin):
|
|||
hosts = ["maya", "nuke"]
|
||||
families = ["renderlayer"]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
# Workaround bug pyblish-base#250
|
||||
if not contextplugin_should_run(self, context):
|
||||
return
|
||||
|
||||
deadline_url = (
|
||||
context.data["system_settings"]
|
||||
["modules"]
|
||||
["deadline"]
|
||||
["DEADLINE_REST_URL"]
|
||||
)
|
||||
def process(self, instance):
|
||||
# get default deadline webservice url from deadline module
|
||||
deadline_url = instance.context.data["defaultDeadline"]
|
||||
# if custom one is set in instance, use that
|
||||
if instance.data.get("deadlineUrl"):
|
||||
deadline_url = instance.data.get("deadlineUrl")
|
||||
self.log.info(
|
||||
"We have deadline URL on instance {}".format(
|
||||
deadline_url))
|
||||
assert deadline_url, "Requires Deadline Webservice URL"
|
||||
|
||||
# Check response
|
||||
response = self._requests_get(deadline_url)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import pyblish.api
|
|||
|
||||
from avalon.vendor import requests
|
||||
|
||||
from openpype.api import get_system_settings
|
||||
from openpype.lib.abstract_submit_deadline import requests_get
|
||||
from openpype.lib.delivery import collect_frames
|
||||
|
||||
|
|
@ -22,6 +21,7 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin):
|
|||
allow_user_override = True
|
||||
|
||||
def process(self, instance):
|
||||
self.instance = instance
|
||||
frame_list = self._get_frame_list(instance.data["render_job_id"])
|
||||
|
||||
for repre in instance.data["representations"]:
|
||||
|
|
@ -129,13 +129,12 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin):
|
|||
Might be different than job info saved in metadata.json if user
|
||||
manually changes job pre/during rendering.
|
||||
"""
|
||||
deadline_url = (
|
||||
get_system_settings()
|
||||
["modules"]
|
||||
["deadline"]
|
||||
["DEADLINE_REST_URL"]
|
||||
)
|
||||
assert deadline_url, "Requires DEADLINE_REST_URL"
|
||||
# get default deadline webservice url from deadline module
|
||||
deadline_url = self.instance.context.data["defaultDeadline"]
|
||||
# if custom one is set in instance, use that
|
||||
if self.instance.data.get("deadlineUrl"):
|
||||
deadline_url = self.instance.data.get("deadlineUrl")
|
||||
assert deadline_url, "Requires Deadline Webservice URL"
|
||||
|
||||
url = "{}/api/jobs?JobID={}".format(deadline_url, job_id)
|
||||
try:
|
||||
|
|
@ -181,6 +180,10 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin):
|
|||
"""Returns set of file names from metadata.json"""
|
||||
expected_files = set()
|
||||
|
||||
for file_name in repre["files"]:
|
||||
files = repre["files"]
|
||||
if not isinstance(files, list):
|
||||
files = [files]
|
||||
|
||||
for file_name in files:
|
||||
expected_files.add(file_name)
|
||||
return expected_files
|
||||
|
|
|
|||
|
|
@ -63,8 +63,9 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin):
|
|||
self.log.debug("Adding ftrack family for '{}'".
|
||||
format(instance.data.get("family")))
|
||||
|
||||
if families and "ftrack" not in families:
|
||||
instance.data["families"].append("ftrack")
|
||||
if families:
|
||||
if "ftrack" not in families:
|
||||
instance.data["families"].append("ftrack")
|
||||
else:
|
||||
instance.data["families"] = ["ftrack"]
|
||||
else:
|
||||
|
|
|
|||
8
openpype/modules/python_console_interpreter/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from .module import (
|
||||
PythonInterpreterAction
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PythonInterpreterAction",
|
||||
)
|
||||
45
openpype/modules/python_console_interpreter/module.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from .. import PypeModule, ITrayAction
|
||||
|
||||
|
||||
class PythonInterpreterAction(PypeModule, ITrayAction):
|
||||
label = "Console"
|
||||
name = "python_interpreter"
|
||||
admin_action = True
|
||||
|
||||
def initialize(self, modules_settings):
|
||||
self.enabled = True
|
||||
self._interpreter_window = None
|
||||
|
||||
def tray_init(self):
|
||||
self.create_interpreter_window()
|
||||
|
||||
def tray_exit(self):
|
||||
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:
|
||||
return
|
||||
|
||||
from openpype.modules.python_console_interpreter.window import (
|
||||
PythonInterpreterWidget
|
||||
)
|
||||
|
||||
self._interpreter_window = PythonInterpreterWidget()
|
||||
|
||||
def on_action_trigger(self):
|
||||
self.show_interpreter_window()
|
||||
|
||||
def show_interpreter_window(self):
|
||||
self.create_interpreter_window()
|
||||
|
||||
if self._interpreter_window.isVisible():
|
||||
self._interpreter_window.activateWindow()
|
||||
self._interpreter_window.raise_()
|
||||
return
|
||||
|
||||
self._interpreter_window.show()
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from .widgets import (
|
||||
PythonInterpreterWidget
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PythonInterpreterWidget",
|
||||
)
|
||||
583
openpype/modules/python_console_interpreter/window/widgets.py
Normal file
|
|
@ -0,0 +1,583 @@
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
import collections
|
||||
from code import InteractiveInterpreter
|
||||
|
||||
import appdirs
|
||||
from Qt import QtCore, QtWidgets, QtGui
|
||||
|
||||
from openpype import resources
|
||||
from openpype.style import load_stylesheet
|
||||
from openpype.lib import JSONSettingRegistry
|
||||
|
||||
|
||||
openpype_art = """
|
||||
. . .. . ..
|
||||
_oOOP3OPP3Op_. .
|
||||
.PPpo~. .. ~2p. .. .... . .
|
||||
.Ppo . .pPO3Op.. . O:. . . .
|
||||
.3Pp . oP3'. 'P33. . 4 .. . . . .. . . .
|
||||
.~OP 3PO. .Op3 : . .. _____ _____ _____
|
||||
.P3O . oP3oP3O3P' . . . . / /./ /./ /
|
||||
O3:. O3p~ . .:. . ./____/./____/ /____/
|
||||
'P . 3p3. oP3~. ..P:. . . .. . . .. . . .
|
||||
. ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . .
|
||||
. '_ .. . . _OP3.. . .https://openpype.io.. .
|
||||
~P3.OPPPO3OP~ . .. .
|
||||
. ' '. . .. . . . .. .
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class PythonInterpreterRegistry(JSONSettingRegistry):
|
||||
"""Class handling OpenPype general settings registry.
|
||||
|
||||
Attributes:
|
||||
vendor (str): Name used for path construction.
|
||||
product (str): Additional name used for path construction.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.vendor = "pypeclub"
|
||||
self.product = "openpype"
|
||||
name = "python_interpreter_tool"
|
||||
path = appdirs.user_data_dir(self.product, self.vendor)
|
||||
super(PythonInterpreterRegistry, self).__init__(name, path)
|
||||
|
||||
|
||||
class StdOEWrap:
|
||||
def __init__(self):
|
||||
self._origin_stdout_write = None
|
||||
self._origin_stderr_write = None
|
||||
self._listening = False
|
||||
self.lines = collections.deque()
|
||||
|
||||
if not sys.stdout:
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
|
||||
if not sys.stderr:
|
||||
sys.stderr = open(os.devnull, "w")
|
||||
|
||||
if self._origin_stdout_write is None:
|
||||
self._origin_stdout_write = sys.stdout.write
|
||||
|
||||
if self._origin_stderr_write is None:
|
||||
self._origin_stderr_write = sys.stderr.write
|
||||
|
||||
self._listening = True
|
||||
sys.stdout.write = self._stdout_listener
|
||||
sys.stderr.write = self._stderr_listener
|
||||
|
||||
def stop_listen(self):
|
||||
self._listening = False
|
||||
|
||||
def _stdout_listener(self, text):
|
||||
if self._listening:
|
||||
self.lines.append(text)
|
||||
if self._origin_stdout_write is not None:
|
||||
self._origin_stdout_write(text)
|
||||
|
||||
def _stderr_listener(self, text):
|
||||
if self._listening:
|
||||
self.lines.append(text)
|
||||
if self._origin_stderr_write is not None:
|
||||
self._origin_stderr_write(text)
|
||||
|
||||
|
||||
class PythonCodeEditor(QtWidgets.QPlainTextEdit):
|
||||
execute_requested = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(PythonCodeEditor, self).__init__(parent)
|
||||
|
||||
self.setObjectName("PythonCodeEditor")
|
||||
|
||||
self._indent = 4
|
||||
|
||||
def _tab_shift_right(self):
|
||||
cursor = self.textCursor()
|
||||
selected_text = cursor.selectedText()
|
||||
if not selected_text:
|
||||
cursor.insertText(" " * self._indent)
|
||||
return
|
||||
|
||||
sel_start = cursor.selectionStart()
|
||||
sel_end = cursor.selectionEnd()
|
||||
cursor.setPosition(sel_end)
|
||||
end_line = cursor.blockNumber()
|
||||
cursor.setPosition(sel_start)
|
||||
while True:
|
||||
cursor.movePosition(QtGui.QTextCursor.StartOfLine)
|
||||
text = cursor.block().text()
|
||||
spaces = len(text) - len(text.lstrip(" "))
|
||||
new_spaces = spaces % self._indent
|
||||
if not new_spaces:
|
||||
new_spaces = self._indent
|
||||
|
||||
cursor.insertText(" " * new_spaces)
|
||||
if cursor.blockNumber() == end_line:
|
||||
break
|
||||
|
||||
cursor.movePosition(QtGui.QTextCursor.NextBlock)
|
||||
|
||||
def _tab_shift_left(self):
|
||||
tmp_cursor = self.textCursor()
|
||||
sel_start = tmp_cursor.selectionStart()
|
||||
sel_end = tmp_cursor.selectionEnd()
|
||||
|
||||
cursor = QtGui.QTextCursor(self.document())
|
||||
cursor.setPosition(sel_end)
|
||||
end_line = cursor.blockNumber()
|
||||
cursor.setPosition(sel_start)
|
||||
while True:
|
||||
cursor.movePosition(QtGui.QTextCursor.StartOfLine)
|
||||
text = cursor.block().text()
|
||||
spaces = len(text) - len(text.lstrip(" "))
|
||||
if spaces:
|
||||
spaces_to_remove = (spaces % self._indent) or self._indent
|
||||
if spaces_to_remove > spaces:
|
||||
spaces_to_remove = spaces
|
||||
|
||||
cursor.setPosition(
|
||||
cursor.position() + spaces_to_remove,
|
||||
QtGui.QTextCursor.KeepAnchor
|
||||
)
|
||||
cursor.removeSelectedText()
|
||||
|
||||
if cursor.blockNumber() == end_line:
|
||||
break
|
||||
|
||||
cursor.movePosition(QtGui.QTextCursor.NextBlock)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == QtCore.Qt.Key_Backtab:
|
||||
self._tab_shift_left()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if event.key() == QtCore.Qt.Key_Tab:
|
||||
if event.modifiers() == QtCore.Qt.NoModifier:
|
||||
self._tab_shift_right()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if (
|
||||
event.key() == QtCore.Qt.Key_Return
|
||||
and event.modifiers() == QtCore.Qt.ControlModifier
|
||||
):
|
||||
self.execute_requested.emit()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
super(PythonCodeEditor, self).keyPressEvent(event)
|
||||
|
||||
|
||||
class PythonTabWidget(QtWidgets.QWidget):
|
||||
before_execute = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, parent):
|
||||
super(PythonTabWidget, self).__init__(parent)
|
||||
|
||||
code_input = PythonCodeEditor(self)
|
||||
|
||||
self.setFocusProxy(code_input)
|
||||
|
||||
execute_btn = QtWidgets.QPushButton("Execute", self)
|
||||
execute_btn.setToolTip("Execute command (Ctrl + Enter)")
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout()
|
||||
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(execute_btn)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(code_input, 1)
|
||||
layout.addLayout(btns_layout, 0)
|
||||
|
||||
execute_btn.clicked.connect(self._on_execute_clicked)
|
||||
code_input.execute_requested.connect(self.execute)
|
||||
|
||||
self._code_input = code_input
|
||||
self._interpreter = InteractiveInterpreter()
|
||||
|
||||
def _on_execute_clicked(self):
|
||||
self.execute()
|
||||
|
||||
def get_code(self):
|
||||
return self._code_input.toPlainText()
|
||||
|
||||
def set_code(self, code_text):
|
||||
self._code_input.setPlainText(code_text)
|
||||
|
||||
def execute(self):
|
||||
code_text = self._code_input.toPlainText()
|
||||
self.before_execute.emit(code_text)
|
||||
self._interpreter.runcode(code_text)
|
||||
|
||||
|
||||
class TabNameDialog(QtWidgets.QDialog):
|
||||
default_width = 330
|
||||
default_height = 85
|
||||
|
||||
def __init__(self, parent):
|
||||
super(TabNameDialog, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("Enter tab name")
|
||||
|
||||
name_label = QtWidgets.QLabel("Tab name:", self)
|
||||
name_input = QtWidgets.QLineEdit(self)
|
||||
|
||||
inputs_layout = QtWidgets.QHBoxLayout()
|
||||
inputs_layout.addWidget(name_label)
|
||||
inputs_layout.addWidget(name_input)
|
||||
|
||||
ok_btn = QtWidgets.QPushButton("Ok", self)
|
||||
cancel_btn = QtWidgets.QPushButton("Cancel", self)
|
||||
btns_layout = QtWidgets.QHBoxLayout()
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(ok_btn)
|
||||
btns_layout.addWidget(cancel_btn)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addLayout(inputs_layout)
|
||||
layout.addStretch(1)
|
||||
layout.addLayout(btns_layout)
|
||||
|
||||
ok_btn.clicked.connect(self._on_ok_clicked)
|
||||
cancel_btn.clicked.connect(self._on_cancel_clicked)
|
||||
|
||||
self._name_input = name_input
|
||||
self._ok_btn = ok_btn
|
||||
self._cancel_btn = cancel_btn
|
||||
|
||||
self._result = None
|
||||
|
||||
self.resize(self.default_width, self.default_height)
|
||||
|
||||
def set_tab_name(self, name):
|
||||
self._name_input.setText(name)
|
||||
|
||||
def result(self):
|
||||
return self._result
|
||||
|
||||
def showEvent(self, event):
|
||||
super(TabNameDialog, self).showEvent(event)
|
||||
btns_width = max(
|
||||
self._ok_btn.width(),
|
||||
self._cancel_btn.width()
|
||||
)
|
||||
|
||||
self._ok_btn.setMinimumWidth(btns_width)
|
||||
self._cancel_btn.setMinimumWidth(btns_width)
|
||||
|
||||
def _on_ok_clicked(self):
|
||||
self._result = self._name_input.text()
|
||||
self.accept()
|
||||
|
||||
def _on_cancel_clicked(self):
|
||||
self._result = None
|
||||
self.reject()
|
||||
|
||||
|
||||
class OutputTextWidget(QtWidgets.QTextEdit):
|
||||
v_max_offset = 4
|
||||
|
||||
def vertical_scroll_at_max(self):
|
||||
v_scroll = self.verticalScrollBar()
|
||||
return v_scroll.value() > v_scroll.maximum() - self.v_max_offset
|
||||
|
||||
def scroll_to_bottom(self):
|
||||
v_scroll = self.verticalScrollBar()
|
||||
return v_scroll.setValue(v_scroll.maximum())
|
||||
|
||||
|
||||
class EnhancedTabBar(QtWidgets.QTabBar):
|
||||
double_clicked = QtCore.Signal(QtCore.QPoint)
|
||||
right_clicked = QtCore.Signal(QtCore.QPoint)
|
||||
mid_clicked = QtCore.Signal(QtCore.QPoint)
|
||||
|
||||
def __init__(self, parent):
|
||||
super(EnhancedTabBar, self).__init__(parent)
|
||||
|
||||
self.setDrawBase(False)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
self.double_clicked.emit(event.globalPos())
|
||||
event.accept()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == QtCore.Qt.RightButton:
|
||||
self.right_clicked.emit(event.globalPos())
|
||||
event.accept()
|
||||
return
|
||||
|
||||
elif event.button() == QtCore.Qt.MidButton:
|
||||
self.mid_clicked.emit(event.globalPos())
|
||||
event.accept()
|
||||
|
||||
else:
|
||||
super(EnhancedTabBar, self).mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class PythonInterpreterWidget(QtWidgets.QWidget):
|
||||
default_width = 1000
|
||||
default_height = 600
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(PythonInterpreterWidget, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("OpenPype Console")
|
||||
self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath()))
|
||||
|
||||
self.ansi_escape = re.compile(
|
||||
r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]"
|
||||
)
|
||||
|
||||
self._tabs = []
|
||||
|
||||
self._stdout_err_wrapper = StdOEWrap()
|
||||
|
||||
output_widget = OutputTextWidget(self)
|
||||
output_widget.setObjectName("PythonInterpreterOutput")
|
||||
output_widget.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
|
||||
output_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
|
||||
|
||||
tab_widget = QtWidgets.QTabWidget(self)
|
||||
tab_bar = EnhancedTabBar(tab_widget)
|
||||
tab_widget.setTabBar(tab_bar)
|
||||
tab_widget.setTabsClosable(False)
|
||||
tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
|
||||
add_tab_btn = QtWidgets.QPushButton("+", tab_widget)
|
||||
tab_widget.setCornerWidget(add_tab_btn, QtCore.Qt.TopLeftCorner)
|
||||
|
||||
widgets_splitter = QtWidgets.QSplitter(self)
|
||||
widgets_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
widgets_splitter.addWidget(output_widget)
|
||||
widgets_splitter.addWidget(tab_widget)
|
||||
widgets_splitter.setStretchFactor(0, 1)
|
||||
widgets_splitter.setStretchFactor(1, 1)
|
||||
height = int(self.default_height / 2)
|
||||
widgets_splitter.setSizes([height, self.default_height - height])
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(widgets_splitter)
|
||||
|
||||
line_check_timer = QtCore.QTimer()
|
||||
line_check_timer.setInterval(200)
|
||||
|
||||
line_check_timer.timeout.connect(self._on_timer_timeout)
|
||||
add_tab_btn.clicked.connect(self._on_add_clicked)
|
||||
tab_bar.right_clicked.connect(self._on_tab_right_click)
|
||||
tab_bar.double_clicked.connect(self._on_tab_double_click)
|
||||
tab_bar.mid_clicked.connect(self._on_tab_mid_click)
|
||||
tab_widget.tabCloseRequested.connect(self._on_tab_close_req)
|
||||
|
||||
self._widgets_splitter = widgets_splitter
|
||||
self._add_tab_btn = add_tab_btn
|
||||
self._output_widget = output_widget
|
||||
self._tab_widget = tab_widget
|
||||
self._line_check_timer = line_check_timer
|
||||
|
||||
self._append_lines([openpype_art])
|
||||
|
||||
self.setStyleSheet(load_stylesheet())
|
||||
|
||||
self.resize(self.default_width, self.default_height)
|
||||
|
||||
self._init_from_registry()
|
||||
|
||||
if self._tab_widget.count() < 1:
|
||||
self.add_tab("Python")
|
||||
|
||||
def _init_from_registry(self):
|
||||
setting_registry = PythonInterpreterRegistry()
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
sizes = setting_registry.get_item("splitter_sizes")
|
||||
if len(sizes) == len(self._widgets_splitter.sizes()):
|
||||
self._widgets_splitter.setSizes(sizes)
|
||||
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
tab_defs = setting_registry.get_item("tabs") or []
|
||||
for tab_def in tab_defs:
|
||||
widget = self.add_tab(tab_def["name"])
|
||||
widget.set_code(tab_def["code"])
|
||||
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def save_registry(self):
|
||||
setting_registry = PythonInterpreterRegistry()
|
||||
|
||||
setting_registry.set_item("width", self.width())
|
||||
setting_registry.set_item("height", self.height())
|
||||
|
||||
setting_registry.set_item(
|
||||
"splitter_sizes", self._widgets_splitter.sizes()
|
||||
)
|
||||
|
||||
tabs = []
|
||||
for tab_idx in range(self._tab_widget.count()):
|
||||
widget = self._tab_widget.widget(tab_idx)
|
||||
tab_code = widget.get_code()
|
||||
tab_name = self._tab_widget.tabText(tab_idx)
|
||||
tabs.append({
|
||||
"name": tab_name,
|
||||
"code": tab_code
|
||||
})
|
||||
|
||||
setting_registry.set_item("tabs", tabs)
|
||||
|
||||
def _on_tab_right_click(self, global_point):
|
||||
point = self._tab_widget.mapFromGlobal(global_point)
|
||||
tab_bar = self._tab_widget.tabBar()
|
||||
tab_idx = tab_bar.tabAt(point)
|
||||
last_index = tab_bar.count() - 1
|
||||
if tab_idx < 0 or tab_idx > last_index:
|
||||
return
|
||||
|
||||
menu = QtWidgets.QMenu(self._tab_widget)
|
||||
menu.addAction("Rename")
|
||||
result = menu.exec_(global_point)
|
||||
if result is None:
|
||||
return
|
||||
|
||||
if result.text() == "Rename":
|
||||
self._rename_tab_req(tab_idx)
|
||||
|
||||
def _rename_tab_req(self, tab_idx):
|
||||
dialog = TabNameDialog(self)
|
||||
dialog.set_tab_name(self._tab_widget.tabText(tab_idx))
|
||||
dialog.exec_()
|
||||
tab_name = dialog.result()
|
||||
if tab_name:
|
||||
self._tab_widget.setTabText(tab_idx, tab_name)
|
||||
|
||||
def _on_tab_mid_click(self, global_point):
|
||||
point = self._tab_widget.mapFromGlobal(global_point)
|
||||
tab_bar = self._tab_widget.tabBar()
|
||||
tab_idx = tab_bar.tabAt(point)
|
||||
last_index = tab_bar.count() - 1
|
||||
if tab_idx < 0 or tab_idx > last_index:
|
||||
return
|
||||
|
||||
self._on_tab_close_req(tab_idx)
|
||||
|
||||
def _on_tab_double_click(self, global_point):
|
||||
point = self._tab_widget.mapFromGlobal(global_point)
|
||||
tab_bar = self._tab_widget.tabBar()
|
||||
tab_idx = tab_bar.tabAt(point)
|
||||
last_index = tab_bar.count() - 1
|
||||
if tab_idx < 0 or tab_idx > last_index:
|
||||
return
|
||||
|
||||
self._rename_tab_req(tab_idx)
|
||||
|
||||
def _on_tab_close_req(self, tab_index):
|
||||
if self._tab_widget.count() == 1:
|
||||
return
|
||||
|
||||
widget = self._tab_widget.widget(tab_index)
|
||||
if widget in self._tabs:
|
||||
self._tabs.remove(widget)
|
||||
self._tab_widget.removeTab(tab_index)
|
||||
|
||||
if self._tab_widget.count() == 1:
|
||||
self._tab_widget.setTabsClosable(False)
|
||||
|
||||
def _append_lines(self, lines):
|
||||
at_max = self._output_widget.vertical_scroll_at_max()
|
||||
tmp_cursor = QtGui.QTextCursor(self._output_widget.document())
|
||||
tmp_cursor.movePosition(QtGui.QTextCursor.End)
|
||||
for line in lines:
|
||||
tmp_cursor.insertText(line)
|
||||
|
||||
if at_max:
|
||||
self._output_widget.scroll_to_bottom()
|
||||
|
||||
def _on_timer_timeout(self):
|
||||
if self._stdout_err_wrapper.lines:
|
||||
lines = []
|
||||
while self._stdout_err_wrapper.lines:
|
||||
line = self._stdout_err_wrapper.lines.popleft()
|
||||
lines.append(self.ansi_escape.sub("", line))
|
||||
self._append_lines(lines)
|
||||
|
||||
def _on_add_clicked(self):
|
||||
dialog = TabNameDialog(self)
|
||||
dialog.exec_()
|
||||
tab_name = dialog.result()
|
||||
if tab_name:
|
||||
self.add_tab(tab_name)
|
||||
|
||||
def _on_before_execute(self, code_text):
|
||||
at_max = self._output_widget.vertical_scroll_at_max()
|
||||
document = self._output_widget.document()
|
||||
tmp_cursor = QtGui.QTextCursor(document)
|
||||
tmp_cursor.movePosition(QtGui.QTextCursor.End)
|
||||
tmp_cursor.insertText("{}\nExecuting command:\n".format(20 * "-"))
|
||||
|
||||
code_block_format = QtGui.QTextFrameFormat()
|
||||
code_block_format.setBackground(QtGui.QColor(27, 27, 27))
|
||||
code_block_format.setPadding(4)
|
||||
|
||||
tmp_cursor.insertFrame(code_block_format)
|
||||
char_format = tmp_cursor.charFormat()
|
||||
char_format.setForeground(
|
||||
QtGui.QBrush(QtGui.QColor(114, 224, 198))
|
||||
)
|
||||
tmp_cursor.setCharFormat(char_format)
|
||||
tmp_cursor.insertText(code_text)
|
||||
|
||||
# Create new cursor
|
||||
tmp_cursor = QtGui.QTextCursor(document)
|
||||
tmp_cursor.movePosition(QtGui.QTextCursor.End)
|
||||
tmp_cursor.insertText("{}\n".format(20 * "-"))
|
||||
|
||||
if at_max:
|
||||
self._output_widget.scroll_to_bottom()
|
||||
|
||||
def add_tab(self, tab_name, index=None):
|
||||
widget = PythonTabWidget(self)
|
||||
widget.before_execute.connect(self._on_before_execute)
|
||||
if index is None:
|
||||
if self._tab_widget.count() > 0:
|
||||
index = self._tab_widget.currentIndex() + 1
|
||||
else:
|
||||
index = 0
|
||||
|
||||
self._tabs.append(widget)
|
||||
self._tab_widget.insertTab(index, widget, tab_name)
|
||||
self._tab_widget.setCurrentIndex(index)
|
||||
|
||||
if self._tab_widget.count() > 1:
|
||||
self._tab_widget.setTabsClosable(True)
|
||||
widget.setFocus()
|
||||
return widget
|
||||
|
||||
def showEvent(self, event):
|
||||
self._line_check_timer.start()
|
||||
super(PythonInterpreterWidget, self).showEvent(event)
|
||||
self._output_widget.scroll_to_bottom()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.save_registry()
|
||||
super(PythonInterpreterWidget, self).closeEvent(event)
|
||||
self._line_check_timer.stop()
|
||||
15
openpype/plugins/publish/collect_modules.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect OpenPype modules."""
|
||||
from openpype.modules import ModulesManager
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectModules(pyblish.api.ContextPlugin):
|
||||
"""Collect OpenPype modules."""
|
||||
|
||||
order = pyblish.api.CollectorOrder
|
||||
label = "OpenPype Modules"
|
||||
|
||||
def process(self, context):
|
||||
manager = ModulesManager()
|
||||
context.data["openPypeModules"] = manager.modules_by_name
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"deadline_servers": [],
|
||||
"publish": {
|
||||
"ValidateExpectedFiles": {
|
||||
"enabled": true,
|
||||
|
|
@ -45,7 +46,8 @@
|
|||
"group": "none",
|
||||
"limit": [],
|
||||
"jobInfo": {},
|
||||
"pluginInfo": {}
|
||||
"pluginInfo": {},
|
||||
"scene_patches": []
|
||||
},
|
||||
"NukeSubmitDeadline": {
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -304,7 +304,8 @@
|
|||
"aftereffects"
|
||||
],
|
||||
"families": [
|
||||
"render"
|
||||
"render",
|
||||
"workfile"
|
||||
],
|
||||
"tasks": [],
|
||||
"add_ftrack_family": true,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,19 @@
|
|||
"workfile": "ma",
|
||||
"yetiRig": "ma"
|
||||
},
|
||||
"maya-dirmap": {
|
||||
"enabled": true,
|
||||
"paths": {
|
||||
"source-path": [
|
||||
"foo1",
|
||||
"foo2"
|
||||
],
|
||||
"destination-path": [
|
||||
"bar1",
|
||||
"bar2"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scriptsmenu": {
|
||||
"name": "OpenPype Tools",
|
||||
"definition": [
|
||||
|
|
@ -31,6 +44,12 @@
|
|||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateRender": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateAnimation": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
|
|
@ -81,12 +100,6 @@
|
|||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateRender": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateRenderSetup": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"project_setup": {
|
||||
"dev_mode": true,
|
||||
"install_unreal_python_engine": false
|
||||
"dev_mode": true
|
||||
}
|
||||
}
|
||||
|
|
@ -140,7 +140,9 @@
|
|||
},
|
||||
"deadline": {
|
||||
"enabled": true,
|
||||
"DEADLINE_REST_URL": "http://localhost:8082"
|
||||
"deadline_urls": {
|
||||
"default": "http://127.0.0.1:8082"
|
||||
}
|
||||
},
|
||||
"muster": {
|
||||
"enabled": false,
|
||||
|
|
|
|||
|
|
@ -105,7 +105,8 @@ from .enum_entity import (
|
|||
AppsEnumEntity,
|
||||
ToolsEnumEntity,
|
||||
TaskTypeEnumEntity,
|
||||
ProvidersEnum
|
||||
ProvidersEnum,
|
||||
DeadlineUrlEnumEntity
|
||||
)
|
||||
|
||||
from .list_entity import ListEntity
|
||||
|
|
@ -160,6 +161,7 @@ __all__ = (
|
|||
"ToolsEnumEntity",
|
||||
"TaskTypeEnumEntity",
|
||||
"ProvidersEnum",
|
||||
"DeadlineUrlEnumEntity",
|
||||
|
||||
"ListEntity",
|
||||
|
||||
|
|
|
|||
|
|
@ -174,6 +174,14 @@ class BaseItemEntity(BaseEntity):
|
|||
roles = [roles]
|
||||
self.roles = roles
|
||||
|
||||
@abstractmethod
|
||||
def collect_static_entities_by_path(self):
|
||||
"""Collect all paths of all static path entities.
|
||||
|
||||
Static path is entity which is not dynamic or under dynamic entity.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def require_restart_on_change(self):
|
||||
return self._require_restart_on_change
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ class DictConditionalEntity(ItemEntity):
|
|||
self.enum_key = self.schema_data.get("enum_key")
|
||||
self.enum_label = self.schema_data.get("enum_label")
|
||||
self.enum_children = self.schema_data.get("enum_children")
|
||||
self.enum_default = self.schema_data.get("enum_default")
|
||||
|
||||
self.enum_entity = None
|
||||
|
||||
|
|
@ -277,15 +278,22 @@ class DictConditionalEntity(ItemEntity):
|
|||
if isinstance(item, dict) and "key" in item:
|
||||
valid_enum_items.append(item)
|
||||
|
||||
enum_keys = []
|
||||
enum_items = []
|
||||
for item in valid_enum_items:
|
||||
item_key = item["key"]
|
||||
enum_keys.append(item_key)
|
||||
item_label = item.get("label") or item_key
|
||||
enum_items.append({item_key: item_label})
|
||||
|
||||
if not enum_items:
|
||||
return
|
||||
|
||||
if self.enum_default in enum_keys:
|
||||
default_key = self.enum_default
|
||||
else:
|
||||
default_key = enum_keys[0]
|
||||
|
||||
# Create Enum child first
|
||||
enum_key = self.enum_key or "invalid"
|
||||
enum_schema = {
|
||||
|
|
@ -293,7 +301,8 @@ class DictConditionalEntity(ItemEntity):
|
|||
"multiselection": False,
|
||||
"enum_items": enum_items,
|
||||
"key": enum_key,
|
||||
"label": self.enum_label
|
||||
"label": self.enum_label,
|
||||
"default": default_key
|
||||
}
|
||||
|
||||
enum_entity = self.create_schema_object(enum_schema, self)
|
||||
|
|
@ -318,6 +327,11 @@ class DictConditionalEntity(ItemEntity):
|
|||
|
||||
self.non_gui_children[item_key][child_obj.key] = child_obj
|
||||
|
||||
def collect_static_entities_by_path(self):
|
||||
if self.is_dynamic_item or self.is_in_dynamic_item:
|
||||
return {}
|
||||
return {self.path: self}
|
||||
|
||||
def get_child_path(self, child_obj):
|
||||
"""Get hierarchical path of child entity.
|
||||
|
||||
|
|
|
|||
|
|
@ -203,6 +203,18 @@ class DictImmutableKeysEntity(ItemEntity):
|
|||
)
|
||||
self.show_borders = self.schema_data.get("show_borders", True)
|
||||
|
||||
def collect_static_entities_by_path(self):
|
||||
output = {}
|
||||
if self.is_dynamic_item or self.is_in_dynamic_item:
|
||||
return output
|
||||
|
||||
output[self.path] = self
|
||||
for children in self.non_gui_children.values():
|
||||
result = children.collect_static_entities_by_path()
|
||||
if result:
|
||||
output.update(result)
|
||||
return output
|
||||
|
||||
def get_child_path(self, child_obj):
|
||||
"""Get hierarchical path of child entity.
|
||||
|
||||
|
|
|
|||
|
|
@ -73,21 +73,41 @@ class EnumEntity(BaseEnumEntity):
|
|||
def _item_initalization(self):
|
||||
self.multiselection = self.schema_data.get("multiselection", False)
|
||||
self.enum_items = self.schema_data.get("enum_items")
|
||||
# Default is optional and non breaking attribute
|
||||
enum_default = self.schema_data.get("default")
|
||||
|
||||
valid_keys = set()
|
||||
all_keys = []
|
||||
for item in self.enum_items or []:
|
||||
valid_keys.add(tuple(item.keys())[0])
|
||||
key = tuple(item.keys())[0]
|
||||
all_keys.append(key)
|
||||
|
||||
self.valid_keys = valid_keys
|
||||
self.valid_keys = set(all_keys)
|
||||
|
||||
if self.multiselection:
|
||||
self.valid_value_types = (list, )
|
||||
self.value_on_not_set = []
|
||||
value_on_not_set = []
|
||||
if enum_default:
|
||||
if not isinstance(enum_default, list):
|
||||
enum_default = [enum_default]
|
||||
|
||||
for item in enum_default:
|
||||
if item in all_keys:
|
||||
value_on_not_set.append(item)
|
||||
|
||||
self.value_on_not_set = value_on_not_set
|
||||
|
||||
else:
|
||||
for key in valid_keys:
|
||||
if self.value_on_not_set is NOT_SET:
|
||||
self.value_on_not_set = key
|
||||
break
|
||||
if isinstance(enum_default, list) and enum_default:
|
||||
enum_default = enum_default[0]
|
||||
|
||||
if enum_default in self.valid_keys:
|
||||
self.value_on_not_set = enum_default
|
||||
|
||||
else:
|
||||
for key in all_keys:
|
||||
if self.value_on_not_set is NOT_SET:
|
||||
self.value_on_not_set = key
|
||||
break
|
||||
|
||||
self.valid_value_types = (STRING_TYPE, )
|
||||
|
||||
|
|
@ -423,3 +443,54 @@ class ProvidersEnum(BaseEnumEntity):
|
|||
self._current_value = value_on_not_set
|
||||
|
||||
self.value_on_not_set = value_on_not_set
|
||||
|
||||
|
||||
class DeadlineUrlEnumEntity(BaseEnumEntity):
|
||||
schema_types = ["deadline_url-enum"]
|
||||
|
||||
def _item_initalization(self):
|
||||
self.multiselection = self.schema_data.get("multiselection", True)
|
||||
|
||||
self.enum_items = []
|
||||
self.valid_keys = set()
|
||||
|
||||
if self.multiselection:
|
||||
self.valid_value_types = (list,)
|
||||
self.value_on_not_set = []
|
||||
else:
|
||||
for key in self.valid_keys:
|
||||
if self.value_on_not_set is NOT_SET:
|
||||
self.value_on_not_set = key
|
||||
break
|
||||
|
||||
self.valid_value_types = (STRING_TYPE,)
|
||||
|
||||
# GUI attribute
|
||||
self.placeholder = self.schema_data.get("placeholder")
|
||||
|
||||
def _get_enum_values(self):
|
||||
system_settings_entity = self.get_entity_from_path("system_settings")
|
||||
|
||||
valid_keys = set()
|
||||
enum_items_list = []
|
||||
deadline_urls_entity = (
|
||||
system_settings_entity
|
||||
["modules"]
|
||||
["deadline"]
|
||||
["deadline_urls"]
|
||||
)
|
||||
for server_name, url_entity in deadline_urls_entity.items():
|
||||
enum_items_list.append(
|
||||
{server_name: "{}: {}".format(server_name, url_entity.value)})
|
||||
valid_keys.add(server_name)
|
||||
return enum_items_list, valid_keys
|
||||
|
||||
def set_override_state(self, *args, **kwargs):
|
||||
super(DeadlineUrlEnumEntity, self).set_override_state(*args, **kwargs)
|
||||
|
||||
self.enum_items, self.valid_keys = self._get_enum_values()
|
||||
new_value = []
|
||||
for key in self._current_value:
|
||||
if key in self.valid_keys:
|
||||
new_value.append(key)
|
||||
self._current_value = new_value
|
||||
|
|
|
|||
|
|
@ -53,6 +53,11 @@ class EndpointEntity(ItemEntity):
|
|||
def _settings_value(self):
|
||||
pass
|
||||
|
||||
def collect_static_entities_by_path(self):
|
||||
if self.is_dynamic_item or self.is_in_dynamic_item:
|
||||
return {}
|
||||
return {self.path: self}
|
||||
|
||||
def settings_value(self):
|
||||
if self._override_state is OverrideState.NOT_DEFINED:
|
||||
return NOT_SET
|
||||
|
|
|
|||
|
|
@ -106,6 +106,9 @@ class PathEntity(ItemEntity):
|
|||
self.valid_value_types = valid_value_types
|
||||
self.child_obj = self.create_schema_object(item_schema, self)
|
||||
|
||||
def collect_static_entities_by_path(self):
|
||||
return self.child_obj.collect_static_entities_by_path()
|
||||
|
||||
def get_child_path(self, _child_obj):
|
||||
return self.path
|
||||
|
||||
|
|
@ -192,6 +195,24 @@ class PathEntity(ItemEntity):
|
|||
class ListStrictEntity(ItemEntity):
|
||||
schema_types = ["list-strict"]
|
||||
|
||||
def __getitem__(self, idx):
|
||||
if not isinstance(idx, int):
|
||||
idx = int(idx)
|
||||
return self.children[idx]
|
||||
|
||||
def __setitem__(self, idx, value):
|
||||
if not isinstance(idx, int):
|
||||
idx = int(idx)
|
||||
self.children[idx].set(value)
|
||||
|
||||
def get(self, idx, default=None):
|
||||
if not isinstance(idx, int):
|
||||
idx = int(idx)
|
||||
|
||||
if idx < len(self.children):
|
||||
return self.children[idx]
|
||||
return default
|
||||
|
||||
def _item_initalization(self):
|
||||
self.valid_value_types = (list, )
|
||||
self.require_key = True
|
||||
|
|
@ -222,6 +243,18 @@ class ListStrictEntity(ItemEntity):
|
|||
|
||||
super(ListStrictEntity, self).schema_validations()
|
||||
|
||||
def collect_static_entities_by_path(self):
|
||||
output = {}
|
||||
if self.is_dynamic_item or self.is_in_dynamic_item:
|
||||
return output
|
||||
|
||||
output[self.path] = self
|
||||
for child_obj in self.children:
|
||||
result = child_obj.collect_static_entities_by_path()
|
||||
if result:
|
||||
output.update(result)
|
||||
return output
|
||||
|
||||
def get_child_path(self, child_obj):
|
||||
result_idx = None
|
||||
for idx, _child_obj in enumerate(self.children):
|
||||
|
|
|
|||
|
|
@ -45,6 +45,24 @@ class ListEntity(EndpointEntity):
|
|||
return True
|
||||
return False
|
||||
|
||||
def __getitem__(self, idx):
|
||||
if not isinstance(idx, int):
|
||||
idx = int(idx)
|
||||
return self.children[idx]
|
||||
|
||||
def __setitem__(self, idx, value):
|
||||
if not isinstance(idx, int):
|
||||
idx = int(idx)
|
||||
self.children[idx].set(value)
|
||||
|
||||
def get(self, idx, default=None):
|
||||
if not isinstance(idx, int):
|
||||
idx = int(idx)
|
||||
|
||||
if idx < len(self.children):
|
||||
return self.children[idx]
|
||||
return default
|
||||
|
||||
def index(self, item):
|
||||
if isinstance(item, BaseEntity):
|
||||
for idx, child_entity in enumerate(self.children):
|
||||
|
|
|
|||
|
|
@ -242,6 +242,14 @@ class RootEntity(BaseItemEntity):
|
|||
"""Whan any children has changed."""
|
||||
self.on_change()
|
||||
|
||||
def collect_static_entities_by_path(self):
|
||||
output = {}
|
||||
for child_obj in self.non_gui_children.values():
|
||||
result = child_obj.collect_static_entities_by_path()
|
||||
if result:
|
||||
output.update(result)
|
||||
return output
|
||||
|
||||
def get_child_path(self, child_entity):
|
||||
"""Return path of children entity"""
|
||||
for key, _child_entity in self.non_gui_children.items():
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@
|
|||
- all items in `enum_children` must have at least `key` key which represents value stored under `enum_key`
|
||||
- items can define `label` for UI purposes
|
||||
- most important part is that item can define `children` key where are definitions of it's children (`children` value works the same way as in `dict`)
|
||||
- to set default value for `enum_key` set it with `enum_default`
|
||||
- entity must have defined `"label"` if is not used as widget
|
||||
- is set as group if any parent is not group
|
||||
- if `"label"` is entetered there which will be shown in GUI
|
||||
|
|
@ -359,6 +360,9 @@ How output of the schema could look like on save:
|
|||
- values are defined under value of key `"enum_items"` as list
|
||||
- each item in list is simple dictionary where value is label and key is value which will be stored
|
||||
- should be possible to enter single dictionary if order of items doesn't matter
|
||||
- it is possible to set default selected value/s with `default` attribute
|
||||
- it is recommended to use this option only in single selection mode
|
||||
- at the end this option is used only when defying default settings value or in dynamic items
|
||||
|
||||
```
|
||||
{
|
||||
|
|
@ -371,7 +375,7 @@ How output of the schema could look like on save:
|
|||
{"ftrackreview": "Add to Ftrack"},
|
||||
{"delete": "Delete output"},
|
||||
{"slate-frame": "Add slate frame"},
|
||||
{"no-hnadles": "Skip handle frames"}
|
||||
{"no-handles": "Skip handle frames"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@
|
|||
"collapsible": true,
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "deadline_url-enum",
|
||||
"key": "deadline_servers",
|
||||
"label": "Deadline Webservice URLs",
|
||||
"multiselect": true
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
@ -151,7 +157,7 @@
|
|||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "MayaSubmitDeadline",
|
||||
"label": "Submit maya job to deadline",
|
||||
"label": "Submit Maya job to Deadline",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
|
|
@ -213,6 +219,31 @@
|
|||
"type": "raw-json",
|
||||
"key": "pluginInfo",
|
||||
"label": "Additional PluginInfo data"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "scene_patches",
|
||||
"label": "Scene patches",
|
||||
"required_keys": ["name", "regex", "line"],
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"key": "name",
|
||||
"label": "Patch name",
|
||||
"type": "text"
|
||||
}, {
|
||||
"key": "regex",
|
||||
"label": "Patch regex",
|
||||
"type": "text"
|
||||
}, {
|
||||
"key": "line",
|
||||
"label": "Patch line",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,6 +14,39 @@
|
|||
"type": "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"key": "maya-dirmap",
|
||||
"label": "Maya Directory Mapping",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "paths",
|
||||
"children": [
|
||||
{
|
||||
"type": "list",
|
||||
"object_type": "text",
|
||||
"key": "source-path",
|
||||
"label": "Source Path"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"object_type": "text",
|
||||
"key": "destination-path",
|
||||
"label": "Destination Path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_maya_scriptsmenu"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,26 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "CreateRender",
|
||||
"label": "Create Render",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "defaults",
|
||||
"label": "Default Subsets",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_create_plugin",
|
||||
|
|
@ -65,10 +85,6 @@
|
|||
"key": "CreatePointCache",
|
||||
"label": "Create Cache"
|
||||
},
|
||||
{
|
||||
"key": "CreateRender",
|
||||
"label": "Create Render"
|
||||
},
|
||||
{
|
||||
"key": "CreateRenderSetup",
|
||||
"label": "Create Render Setup"
|
||||
|
|
|
|||
|
|
@ -130,9 +130,11 @@
|
|||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "DEADLINE_REST_URL",
|
||||
"label": "Deadline Resl URL"
|
||||
"type": "dict-modifiable",
|
||||
"object_type": "text",
|
||||
"key": "deadline_urls",
|
||||
"required_keys": ["default"],
|
||||
"label": "Deadline Webservice URLs"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ def _load_font():
|
|||
font_dirs = []
|
||||
font_dirs.append(os.path.join(fonts_dirpath, "Montserrat"))
|
||||
font_dirs.append(os.path.join(fonts_dirpath, "Spartan"))
|
||||
font_dirs.append(os.path.join(fonts_dirpath, "RobotoMono", "static"))
|
||||
|
||||
loaded_fonts = []
|
||||
for font_dir in font_dirs:
|
||||
|
|
|
|||
202
openpype/style/fonts/RobotoMono/LICENSE.txt
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
77
openpype/style/fonts/RobotoMono/README.txt
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
Roboto Mono Variable Font
|
||||
=========================
|
||||
|
||||
This download contains Roboto Mono as both variable fonts and static fonts.
|
||||
|
||||
Roboto Mono is a variable font with this axis:
|
||||
wght
|
||||
|
||||
This means all the styles are contained in these files:
|
||||
RobotoMono-VariableFont_wght.ttf
|
||||
RobotoMono-Italic-VariableFont_wght.ttf
|
||||
|
||||
If your app fully supports variable fonts, you can now pick intermediate styles
|
||||
that aren’t available as static fonts. Not all apps support variable fonts, and
|
||||
in those cases you can use the static font files for Roboto Mono:
|
||||
static/RobotoMono-Thin.ttf
|
||||
static/RobotoMono-ExtraLight.ttf
|
||||
static/RobotoMono-Light.ttf
|
||||
static/RobotoMono-Regular.ttf
|
||||
static/RobotoMono-Medium.ttf
|
||||
static/RobotoMono-SemiBold.ttf
|
||||
static/RobotoMono-Bold.ttf
|
||||
static/RobotoMono-ThinItalic.ttf
|
||||
static/RobotoMono-ExtraLightItalic.ttf
|
||||
static/RobotoMono-LightItalic.ttf
|
||||
static/RobotoMono-Italic.ttf
|
||||
static/RobotoMono-MediumItalic.ttf
|
||||
static/RobotoMono-SemiBoldItalic.ttf
|
||||
static/RobotoMono-BoldItalic.ttf
|
||||
|
||||
Get started
|
||||
-----------
|
||||
|
||||
1. Install the font files you want to use
|
||||
|
||||
2. Use your app's font picker to view the font family and all the
|
||||
available styles
|
||||
|
||||
Learn more about variable fonts
|
||||
-------------------------------
|
||||
|
||||
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
|
||||
https://variablefonts.typenetwork.com
|
||||
https://medium.com/variable-fonts
|
||||
|
||||
In desktop apps
|
||||
|
||||
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
|
||||
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
|
||||
|
||||
Online
|
||||
|
||||
https://developers.google.com/fonts/docs/getting_started
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
|
||||
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
|
||||
|
||||
Installing fonts
|
||||
|
||||
MacOS: https://support.apple.com/en-us/HT201749
|
||||
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
|
||||
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
|
||||
|
||||
Android Apps
|
||||
|
||||
https://developers.google.com/fonts/docs/android
|
||||
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
|
||||
|
||||
License
|
||||
-------
|
||||
Please read the full license text (LICENSE.txt) to understand the permissions,
|
||||
restrictions and requirements for usage, redistribution, and modification.
|
||||
|
||||
You can use them freely in your products & projects - print or digital,
|
||||
commercial or otherwise.
|
||||
|
||||
This isn't legal advice, please consider consulting a lawyer and see the full
|
||||
license for all details.
|
||||
BIN
openpype/style/fonts/RobotoMono/RobotoMono-VariableFont_wght.ttf
Normal file
BIN
openpype/style/fonts/RobotoMono/static/RobotoMono-Bold.ttf
Normal file
BIN
openpype/style/fonts/RobotoMono/static/RobotoMono-BoldItalic.ttf
Normal file
BIN
openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLight.ttf
Normal file
BIN
openpype/style/fonts/RobotoMono/static/RobotoMono-Italic.ttf
Normal file
BIN
openpype/style/fonts/RobotoMono/static/RobotoMono-Light.ttf
Normal file
BIN
openpype/style/fonts/RobotoMono/static/RobotoMono-Medium.ttf
Normal file
BIN
openpype/style/fonts/RobotoMono/static/RobotoMono-Regular.ttf
Normal file
BIN
openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBold.ttf
Normal file
BIN
openpype/style/fonts/RobotoMono/static/RobotoMono-Thin.ttf
Normal file
BIN
openpype/style/fonts/RobotoMono/static/RobotoMono-ThinItalic.ttf
Normal file
|
|
@ -271,37 +271,38 @@ QTabWidget::tab-bar {
|
|||
}
|
||||
|
||||
QTabBar::tab {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 5px;
|
||||
|
||||
border-left: 3px solid transparent;
|
||||
border-top: 1px solid {color:border};
|
||||
border-right: 1px solid {color:border};
|
||||
background: qlineargradient(
|
||||
x1: 0, y1: 1, x2: 0, y2: 0,
|
||||
stop: 0.5 {color:bg}, stop: 1.0 {color:bg-inputs}
|
||||
);
|
||||
}
|
||||
|
||||
QTabBar::tab:selected {
|
||||
background: {color:grey-lighter};
|
||||
/* background: qradialgradient(
|
||||
cx:0.5, cy:0.5, radius: 2,
|
||||
fx:0.5, fy:1,
|
||||
stop:0.3 {color:bg}, stop:1 white
|
||||
) */
|
||||
/* background: qlineargradient(
|
||||
x1: 0, y1: 0, x2: 0, y2: 1,
|
||||
stop: 0 {color:bg-inputs}, stop: 1.0 {color:bg}
|
||||
); */
|
||||
border-left: 3px solid {color:border-focus};
|
||||
background: qlineargradient(
|
||||
x1: 0, y1: 1, x2: 0, y2: 0,
|
||||
stop: 0.5 {color:bg}, stop: 1.0 {color:border}
|
||||
);
|
||||
}
|
||||
|
||||
QTabBar::tab:!selected {
|
||||
/* Make it smaller*/
|
||||
margin-top: 3px;
|
||||
background: {color:grey-light};
|
||||
}
|
||||
|
||||
QTabBar::tab:!selected:hover {
|
||||
background: {color:grey-lighter};
|
||||
}
|
||||
|
||||
QTabBar::tab:first {
|
||||
border-left: 1px solid {color:border};
|
||||
}
|
||||
QTabBar::tab:first:selected {
|
||||
margin-left: 0;
|
||||
border-left: 3px solid {color:border-focus};
|
||||
}
|
||||
|
||||
QTabBar::tab:last:selected {
|
||||
|
|
@ -623,3 +624,8 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
|||
border: 1px solid {color:border};
|
||||
border-radius: 0.1em;
|
||||
}
|
||||
|
||||
/* Python console interpreter */
|
||||
#PythonInterpreterOutput, #PythonCodeEditor {
|
||||
font-family: "Roboto Mono";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,38 @@ class BaseWidget(QtWidgets.QWidget):
|
|||
self.label_widget = None
|
||||
self.create_ui()
|
||||
|
||||
def scroll_to(self, widget):
|
||||
self.category_widget.scroll_to(widget)
|
||||
|
||||
def set_path(self, path):
|
||||
self.category_widget.set_path(path)
|
||||
|
||||
def set_focus(self, scroll_to=False):
|
||||
"""Set focus of a widget.
|
||||
|
||||
Args:
|
||||
scroll_to(bool): Also scroll to widget in category widget.
|
||||
"""
|
||||
if scroll_to:
|
||||
self.scroll_to(self)
|
||||
self.setFocus()
|
||||
|
||||
def make_sure_is_visible(self, path, scroll_to):
|
||||
"""Make a widget of entity visible by it's path.
|
||||
|
||||
Args:
|
||||
path(str): Path to entity.
|
||||
scroll_to(bool): Should be scrolled to entity.
|
||||
|
||||
Returns:
|
||||
bool: Entity with path was found.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"{} not implemented `make_sure_is_visible`".format(
|
||||
self.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def trigger_hierarchical_style_update(self):
|
||||
self.category_widget.hierarchical_style_update()
|
||||
|
||||
|
|
@ -277,11 +309,23 @@ class BaseWidget(QtWidgets.QWidget):
|
|||
if to_run:
|
||||
to_run()
|
||||
|
||||
def focused_in(self):
|
||||
if self.entity is not None:
|
||||
self.set_path(self.entity.path)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.allow_actions and event.button() == QtCore.Qt.RightButton:
|
||||
return self.show_actions_menu()
|
||||
|
||||
return super(BaseWidget, self).mouseReleaseEvent(event)
|
||||
focused_in = False
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
focused_in = True
|
||||
self.focused_in()
|
||||
|
||||
result = super(BaseWidget, self).mouseReleaseEvent(event)
|
||||
if focused_in and not event.isAccepted():
|
||||
event.accept()
|
||||
return result
|
||||
|
||||
|
||||
class InputWidget(BaseWidget):
|
||||
|
|
@ -337,6 +381,14 @@ class InputWidget(BaseWidget):
|
|||
)
|
||||
)
|
||||
|
||||
def make_sure_is_visible(self, path, scroll_to):
|
||||
if path:
|
||||
entity_path = self.entity.path
|
||||
if entity_path == path:
|
||||
self.set_focus(scroll_to)
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_style(self):
|
||||
has_unsaved_changes = self.entity.has_unsaved_changes
|
||||
if not has_unsaved_changes and self.entity.group_item:
|
||||
|
|
@ -422,11 +474,20 @@ class GUIWidget(BaseWidget):
|
|||
layout.addWidget(splitter_item)
|
||||
|
||||
def set_entity_value(self):
|
||||
return
|
||||
pass
|
||||
|
||||
def hierarchical_style_update(self):
|
||||
pass
|
||||
|
||||
def make_sure_is_visible(self, *args, **kwargs):
|
||||
return False
|
||||
|
||||
def focused_in(self):
|
||||
pass
|
||||
|
||||
def set_path(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def get_invalid(self):
|
||||
return []
|
||||
|
||||
|
|
|
|||
492
openpype/tools/settings/settings/breadcrumbs_widget.py
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
from Qt import QtWidgets, QtGui, QtCore
|
||||
|
||||
PREFIX_ROLE = QtCore.Qt.UserRole + 1
|
||||
LAST_SEGMENT_ROLE = QtCore.Qt.UserRole + 2
|
||||
|
||||
|
||||
class BreadcrumbItem(QtGui.QStandardItem):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._display_value = None
|
||||
self._edit_value = None
|
||||
super(BreadcrumbItem, self).__init__(*args, **kwargs)
|
||||
|
||||
def data(self, role=None):
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
return self._display_value
|
||||
|
||||
if role == QtCore.Qt.EditRole:
|
||||
return self._edit_value
|
||||
|
||||
if role is None:
|
||||
args = tuple()
|
||||
else:
|
||||
args = (role, )
|
||||
return super(BreadcrumbItem, self).data(*args)
|
||||
|
||||
def setData(self, value, role):
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
self._display_value = value
|
||||
return True
|
||||
|
||||
if role == QtCore.Qt.EditRole:
|
||||
self._edit_value = value
|
||||
return True
|
||||
|
||||
if role is None:
|
||||
args = (value, )
|
||||
else:
|
||||
args = (value, role)
|
||||
return super(BreadcrumbItem, self).setData(*args)
|
||||
|
||||
|
||||
class BreadcrumbsModel(QtGui.QStandardItemModel):
|
||||
def __init__(self):
|
||||
super(BreadcrumbsModel, self).__init__()
|
||||
self.current_path = ""
|
||||
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
return
|
||||
|
||||
|
||||
class SettingsBreadcrumbs(BreadcrumbsModel):
|
||||
def __init__(self):
|
||||
self.entity = None
|
||||
|
||||
self.entities_by_path = {}
|
||||
self.dynamic_paths = set()
|
||||
|
||||
super(SettingsBreadcrumbs, self).__init__()
|
||||
|
||||
def set_entity(self, entity):
|
||||
self.entities_by_path = {}
|
||||
self.dynamic_paths = set()
|
||||
self.entity = entity
|
||||
self.reset()
|
||||
|
||||
def has_children(self, path):
|
||||
for key in self.entities_by_path.keys():
|
||||
if key.startswith(path):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_valid_path(self, path):
|
||||
if not path:
|
||||
return True
|
||||
|
||||
path_items = path.split("/")
|
||||
try:
|
||||
entity = self.entity
|
||||
for item in path_items:
|
||||
entity = entity[item]
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class SystemSettingsBreadcrumbs(SettingsBreadcrumbs):
|
||||
def reset(self):
|
||||
root_item = self.invisibleRootItem()
|
||||
rows = root_item.rowCount()
|
||||
if rows > 0:
|
||||
root_item.removeRows(0, rows)
|
||||
|
||||
if self.entity is None:
|
||||
return
|
||||
|
||||
entities_by_path = self.entity.collect_static_entities_by_path()
|
||||
self.entities_by_path = entities_by_path
|
||||
items = []
|
||||
for path in entities_by_path.keys():
|
||||
if not path:
|
||||
continue
|
||||
path_items = path.split("/")
|
||||
value = path
|
||||
label = path_items.pop(-1)
|
||||
prefix = "/".join(path_items)
|
||||
if prefix:
|
||||
prefix += "/"
|
||||
|
||||
item = QtGui.QStandardItem(value)
|
||||
item.setData(label, LAST_SEGMENT_ROLE)
|
||||
item.setData(prefix, PREFIX_ROLE)
|
||||
|
||||
items.append(item)
|
||||
|
||||
root_item.appendRows(items)
|
||||
|
||||
|
||||
class ProjectSettingsBreadcrumbs(SettingsBreadcrumbs):
|
||||
def reset(self):
|
||||
root_item = self.invisibleRootItem()
|
||||
rows = root_item.rowCount()
|
||||
if rows > 0:
|
||||
root_item.removeRows(0, rows)
|
||||
|
||||
if self.entity is None:
|
||||
return
|
||||
|
||||
entities_by_path = self.entity.collect_static_entities_by_path()
|
||||
self.entities_by_path = entities_by_path
|
||||
items = []
|
||||
for path in entities_by_path.keys():
|
||||
if not path:
|
||||
continue
|
||||
path_items = path.split("/")
|
||||
value = path
|
||||
label = path_items.pop(-1)
|
||||
prefix = "/".join(path_items)
|
||||
if prefix:
|
||||
prefix += "/"
|
||||
|
||||
item = QtGui.QStandardItem(value)
|
||||
item.setData(label, LAST_SEGMENT_ROLE)
|
||||
item.setData(prefix, PREFIX_ROLE)
|
||||
|
||||
items.append(item)
|
||||
|
||||
root_item.appendRows(items)
|
||||
|
||||
|
||||
class BreadcrumbsProxy(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BreadcrumbsProxy, self).__init__(*args, **kwargs)
|
||||
|
||||
self._current_path = ""
|
||||
|
||||
def set_path_prefix(self, prefix):
|
||||
path = prefix
|
||||
if not prefix.endswith("/"):
|
||||
path_items = path.split("/")
|
||||
if len(path_items) == 1:
|
||||
path = ""
|
||||
else:
|
||||
path_items.pop(-1)
|
||||
path = "/".join(path_items) + "/"
|
||||
|
||||
if path == self._current_path:
|
||||
return
|
||||
|
||||
self._current_path = prefix
|
||||
|
||||
self.invalidateFilter()
|
||||
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
index = self.sourceModel().index(row, 0, parent)
|
||||
prefix_path = index.data(PREFIX_ROLE)
|
||||
return prefix_path == self._current_path
|
||||
|
||||
|
||||
class BreadcrumbsHintMenu(QtWidgets.QMenu):
|
||||
def __init__(self, model, path_prefix, parent):
|
||||
super(BreadcrumbsHintMenu, self).__init__(parent)
|
||||
|
||||
self._path_prefix = path_prefix
|
||||
self._model = model
|
||||
|
||||
def showEvent(self, event):
|
||||
self.clear()
|
||||
|
||||
self._model.set_path_prefix(self._path_prefix)
|
||||
|
||||
row_count = self._model.rowCount()
|
||||
if row_count == 0:
|
||||
action = self.addAction("* Nothing")
|
||||
action.setData(".")
|
||||
else:
|
||||
for row in range(self._model.rowCount()):
|
||||
index = self._model.index(row, 0)
|
||||
label = index.data(LAST_SEGMENT_ROLE)
|
||||
value = index.data(QtCore.Qt.EditRole)
|
||||
action = self.addAction(label)
|
||||
action.setData(value)
|
||||
|
||||
super(BreadcrumbsHintMenu, self).showEvent(event)
|
||||
|
||||
|
||||
class ClickableWidget(QtWidgets.QWidget):
|
||||
clicked = QtCore.Signal()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.clicked.emit()
|
||||
super(ClickableWidget, self).mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class BreadcrumbsPathInput(QtWidgets.QLineEdit):
|
||||
cancelled = QtCore.Signal()
|
||||
confirmed = QtCore.Signal()
|
||||
|
||||
def __init__(self, model, proxy_model, parent):
|
||||
super(BreadcrumbsPathInput, self).__init__(parent)
|
||||
|
||||
self.setObjectName("BreadcrumbsPathInput")
|
||||
|
||||
self.setFrame(False)
|
||||
|
||||
completer = QtWidgets.QCompleter(self)
|
||||
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
completer.setModel(proxy_model)
|
||||
|
||||
popup = completer.popup()
|
||||
popup.setUniformItemSizes(True)
|
||||
popup.setLayoutMode(QtWidgets.QListView.Batched)
|
||||
|
||||
self.setCompleter(completer)
|
||||
|
||||
completer.activated.connect(self._on_completer_activated)
|
||||
self.textEdited.connect(self._on_text_change)
|
||||
|
||||
self._completer = completer
|
||||
self._model = model
|
||||
self._proxy_model = proxy_model
|
||||
|
||||
self._context_menu_visible = False
|
||||
|
||||
def set_model(self, model):
|
||||
self._model = model
|
||||
|
||||
def event(self, event):
|
||||
if (
|
||||
event.type() == QtCore.QEvent.KeyPress
|
||||
and event.key() == QtCore.Qt.Key_Tab
|
||||
):
|
||||
if self._model:
|
||||
find_value = self.text() + "/"
|
||||
if self._model.has_children(find_value):
|
||||
self.insert("/")
|
||||
else:
|
||||
self._completer.popup().hide()
|
||||
event.accept()
|
||||
return True
|
||||
|
||||
return super(BreadcrumbsPathInput, self).event(event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == QtCore.Qt.Key_Escape:
|
||||
self.cancelled.emit()
|
||||
return
|
||||
|
||||
if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
|
||||
self.confirmed.emit()
|
||||
return
|
||||
|
||||
super(BreadcrumbsPathInput, self).keyPressEvent(event)
|
||||
|
||||
def focusOutEvent(self, event):
|
||||
if not self._context_menu_visible:
|
||||
self.cancelled.emit()
|
||||
|
||||
self._context_menu_visible = False
|
||||
super(BreadcrumbsPathInput, self).focusOutEvent(event)
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
self._context_menu_visible = True
|
||||
super(BreadcrumbsPathInput, self).contextMenuEvent(event)
|
||||
|
||||
def _on_completer_activated(self, path):
|
||||
self.confirmed.emit()
|
||||
|
||||
def _on_text_change(self, path):
|
||||
self._proxy_model.set_path_prefix(path)
|
||||
|
||||
|
||||
class BreadcrumbsButton(QtWidgets.QToolButton):
|
||||
path_selected = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, path, model, parent):
|
||||
super(BreadcrumbsButton, self).__init__(parent)
|
||||
|
||||
self.setObjectName("BreadcrumbsButton")
|
||||
|
||||
path_prefix = path
|
||||
if path:
|
||||
path_prefix += "/"
|
||||
|
||||
self.setAutoRaise(True)
|
||||
self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
|
||||
|
||||
self.setMouseTracking(True)
|
||||
|
||||
if path:
|
||||
self.setText(path.split("/")[-1])
|
||||
else:
|
||||
self.setProperty("empty", "1")
|
||||
|
||||
menu = BreadcrumbsHintMenu(model, path_prefix, self)
|
||||
|
||||
self.setMenu(menu)
|
||||
|
||||
# fixed size breadcrumbs
|
||||
self.setMinimumSize(self.minimumSizeHint())
|
||||
size_policy = self.sizePolicy()
|
||||
size_policy.setVerticalPolicy(size_policy.Minimum)
|
||||
self.setSizePolicy(size_policy)
|
||||
|
||||
menu.triggered.connect(self._on_menu_click)
|
||||
self.clicked.connect(self._on_click)
|
||||
|
||||
self._path = path
|
||||
self._path_prefix = path_prefix
|
||||
self._model = model
|
||||
self._menu = menu
|
||||
|
||||
def _on_click(self):
|
||||
self.path_selected.emit(self._path)
|
||||
|
||||
def _on_menu_click(self, action):
|
||||
item = action.data()
|
||||
self.path_selected.emit(item)
|
||||
|
||||
|
||||
class BreadcrumbsAddressBar(QtWidgets.QFrame):
|
||||
"Windows Explorer-like address bar"
|
||||
path_changed = QtCore.Signal(str)
|
||||
path_edited = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(BreadcrumbsAddressBar, self).__init__(parent)
|
||||
|
||||
self.setAutoFillBackground(True)
|
||||
self.setFrameShape(self.StyledPanel)
|
||||
|
||||
# Edit presented path textually
|
||||
proxy_model = BreadcrumbsProxy()
|
||||
path_input = BreadcrumbsPathInput(None, proxy_model, self)
|
||||
path_input.setVisible(False)
|
||||
|
||||
path_input.cancelled.connect(self._on_input_cancel)
|
||||
path_input.confirmed.connect(self._on_input_confirm)
|
||||
|
||||
# Container for `crumbs_panel`
|
||||
crumbs_container = QtWidgets.QWidget(self)
|
||||
|
||||
# Container for breadcrumbs
|
||||
crumbs_panel = QtWidgets.QWidget(crumbs_container)
|
||||
crumbs_panel.setObjectName("BreadcrumbsPanel")
|
||||
|
||||
crumbs_layout = QtWidgets.QHBoxLayout()
|
||||
crumbs_layout.setContentsMargins(0, 0, 0, 0)
|
||||
crumbs_layout.setSpacing(0)
|
||||
|
||||
crumbs_cont_layout = QtWidgets.QHBoxLayout(crumbs_container)
|
||||
crumbs_cont_layout.setContentsMargins(0, 0, 0, 0)
|
||||
crumbs_cont_layout.setSpacing(0)
|
||||
crumbs_cont_layout.addWidget(crumbs_panel)
|
||||
|
||||
# Clicking on empty space to the right puts the bar into edit mode
|
||||
switch_space = ClickableWidget(self)
|
||||
|
||||
crumb_panel_layout = QtWidgets.QHBoxLayout(crumbs_panel)
|
||||
crumb_panel_layout.setContentsMargins(0, 0, 0, 0)
|
||||
crumb_panel_layout.setSpacing(0)
|
||||
crumb_panel_layout.addLayout(crumbs_layout, 0)
|
||||
crumb_panel_layout.addWidget(switch_space, 1)
|
||||
|
||||
switch_space.clicked.connect(self.switch_space_mouse_up)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(path_input)
|
||||
layout.addWidget(crumbs_container)
|
||||
|
||||
self.setMaximumHeight(path_input.height())
|
||||
|
||||
self.crumbs_layout = crumbs_layout
|
||||
self.crumbs_panel = crumbs_panel
|
||||
self.switch_space = switch_space
|
||||
self.path_input = path_input
|
||||
self.crumbs_container = crumbs_container
|
||||
|
||||
self._model = None
|
||||
self._proxy_model = proxy_model
|
||||
|
||||
self._current_path = None
|
||||
|
||||
def set_model(self, model):
|
||||
self._model = model
|
||||
self.path_input.set_model(model)
|
||||
self._proxy_model.setSourceModel(model)
|
||||
|
||||
def _on_input_confirm(self):
|
||||
self.change_path(self.path_input.text())
|
||||
|
||||
def _on_input_cancel(self):
|
||||
self._cancel_edit()
|
||||
|
||||
def _clear_crumbs(self):
|
||||
while self.crumbs_layout.count():
|
||||
widget = self.crumbs_layout.takeAt(0).widget()
|
||||
if widget:
|
||||
widget.deleteLater()
|
||||
|
||||
def _insert_crumb(self, path):
|
||||
btn = BreadcrumbsButton(path, self._proxy_model, self.crumbs_panel)
|
||||
|
||||
self.crumbs_layout.insertWidget(0, btn)
|
||||
|
||||
btn.path_selected.connect(self._on_crumb_clicked)
|
||||
|
||||
def _on_crumb_clicked(self, path):
|
||||
"Breadcrumb was clicked"
|
||||
self.change_path(path)
|
||||
|
||||
def change_path(self, path):
|
||||
if self._model and not self._model.is_valid_path(path):
|
||||
self._show_address_field()
|
||||
else:
|
||||
self.set_path(path)
|
||||
self.path_edited.emit(path)
|
||||
|
||||
def set_path(self, path):
|
||||
if path is None or path == ".":
|
||||
path = self._current_path
|
||||
|
||||
# exit edit mode
|
||||
self._cancel_edit()
|
||||
|
||||
self._clear_crumbs()
|
||||
self._current_path = path
|
||||
self.path_input.setText(path)
|
||||
path_items = [
|
||||
item
|
||||
for item in path.split("/")
|
||||
if item
|
||||
]
|
||||
while path_items:
|
||||
item = "/".join(path_items)
|
||||
self._insert_crumb(item)
|
||||
path_items.pop(-1)
|
||||
self._insert_crumb("")
|
||||
|
||||
self.path_changed.emit(self._current_path)
|
||||
|
||||
def _cancel_edit(self):
|
||||
"Set edit line text back to current path and switch to view mode"
|
||||
# revert path
|
||||
self.path_input.setText(self.path())
|
||||
# switch back to breadcrumbs view
|
||||
self._show_address_field(False)
|
||||
|
||||
def path(self):
|
||||
"Get path displayed in this BreadcrumbsAddressBar"
|
||||
return self._current_path
|
||||
|
||||
def switch_space_mouse_up(self):
|
||||
"EVENT: switch_space mouse clicked"
|
||||
self._show_address_field(True)
|
||||
|
||||
def _show_address_field(self, show=True):
|
||||
"Show text address field"
|
||||
self.crumbs_container.setVisible(not show)
|
||||
self.path_input.setVisible(show)
|
||||
if show:
|
||||
self.path_input.setFocus()
|
||||
self.path_input.selectAll()
|
||||
|
||||
def minimumSizeHint(self):
|
||||
result = super(BreadcrumbsAddressBar, self).minimumSizeHint()
|
||||
result.setHeight(self.path_input.minimumSizeHint().height())
|
||||
return result
|
||||
|
|
@ -31,6 +31,11 @@ from openpype.settings.entities import (
|
|||
|
||||
from openpype.settings import SaveWarningExc
|
||||
from .widgets import ProjectListWidget
|
||||
from .breadcrumbs_widget import (
|
||||
BreadcrumbsAddressBar,
|
||||
SystemSettingsBreadcrumbs,
|
||||
ProjectSettingsBreadcrumbs
|
||||
)
|
||||
|
||||
from .base import GUIWidget
|
||||
from .list_item_widget import ListWidget
|
||||
|
|
@ -175,6 +180,16 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
scroll_widget = QtWidgets.QScrollArea(self)
|
||||
scroll_widget.setObjectName("GroupWidget")
|
||||
content_widget = QtWidgets.QWidget(scroll_widget)
|
||||
|
||||
breadcrumbs_label = QtWidgets.QLabel("Path:", content_widget)
|
||||
breadcrumbs_widget = BreadcrumbsAddressBar(content_widget)
|
||||
|
||||
breadcrumbs_layout = QtWidgets.QHBoxLayout()
|
||||
breadcrumbs_layout.setContentsMargins(5, 5, 5, 5)
|
||||
breadcrumbs_layout.setSpacing(5)
|
||||
breadcrumbs_layout.addWidget(breadcrumbs_label)
|
||||
breadcrumbs_layout.addWidget(breadcrumbs_widget)
|
||||
|
||||
content_layout = QtWidgets.QVBoxLayout(content_widget)
|
||||
content_layout.setContentsMargins(3, 3, 3, 3)
|
||||
content_layout.setSpacing(5)
|
||||
|
|
@ -183,40 +198,43 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
scroll_widget.setWidgetResizable(True)
|
||||
scroll_widget.setWidget(content_widget)
|
||||
|
||||
configurations_widget = QtWidgets.QWidget(self)
|
||||
|
||||
footer_widget = QtWidgets.QWidget(configurations_widget)
|
||||
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
|
||||
refresh_icon = qtawesome.icon("fa.refresh", color="white")
|
||||
refresh_btn = QtWidgets.QPushButton(footer_widget)
|
||||
refresh_btn = QtWidgets.QPushButton(self)
|
||||
refresh_btn.setIcon(refresh_icon)
|
||||
|
||||
footer_layout.addWidget(refresh_btn, 0)
|
||||
|
||||
footer_layout = QtWidgets.QHBoxLayout()
|
||||
if self.user_role == "developer":
|
||||
self._add_developer_ui(footer_layout)
|
||||
|
||||
save_btn = QtWidgets.QPushButton("Save", footer_widget)
|
||||
require_restart_label = QtWidgets.QLabel(footer_widget)
|
||||
save_btn = QtWidgets.QPushButton("Save", self)
|
||||
require_restart_label = QtWidgets.QLabel(self)
|
||||
require_restart_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
footer_layout.addWidget(refresh_btn, 0)
|
||||
footer_layout.addWidget(require_restart_label, 1)
|
||||
footer_layout.addWidget(save_btn, 0)
|
||||
|
||||
configurations_layout = QtWidgets.QVBoxLayout(configurations_widget)
|
||||
configurations_layout = QtWidgets.QVBoxLayout()
|
||||
configurations_layout.setContentsMargins(0, 0, 0, 0)
|
||||
configurations_layout.setSpacing(0)
|
||||
|
||||
configurations_layout.addWidget(scroll_widget, 1)
|
||||
configurations_layout.addWidget(footer_widget, 0)
|
||||
configurations_layout.addLayout(footer_layout, 0)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
conf_wrapper_layout = QtWidgets.QHBoxLayout()
|
||||
conf_wrapper_layout.setContentsMargins(0, 0, 0, 0)
|
||||
conf_wrapper_layout.setSpacing(0)
|
||||
conf_wrapper_layout.addLayout(configurations_layout, 1)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
main_layout.addWidget(configurations_widget, 1)
|
||||
main_layout.addLayout(breadcrumbs_layout, 0)
|
||||
main_layout.addLayout(conf_wrapper_layout, 1)
|
||||
|
||||
save_btn.clicked.connect(self._save)
|
||||
refresh_btn.clicked.connect(self._on_refresh)
|
||||
breadcrumbs_widget.path_edited.connect(self._on_path_edit)
|
||||
|
||||
self.save_btn = save_btn
|
||||
self.refresh_btn = refresh_btn
|
||||
|
|
@ -224,7 +242,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
self.scroll_widget = scroll_widget
|
||||
self.content_layout = content_layout
|
||||
self.content_widget = content_widget
|
||||
self.configurations_widget = configurations_widget
|
||||
self.breadcrumbs_widget = breadcrumbs_widget
|
||||
self.breadcrumbs_model = None
|
||||
self.conf_wrapper_layout = conf_wrapper_layout
|
||||
self.main_layout = main_layout
|
||||
|
||||
self.ui_tweaks()
|
||||
|
|
@ -232,6 +252,23 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
def ui_tweaks(self):
|
||||
return
|
||||
|
||||
def _on_path_edit(self, path):
|
||||
for input_field in self.input_fields:
|
||||
if input_field.make_sure_is_visible(path, True):
|
||||
break
|
||||
|
||||
def scroll_to(self, widget):
|
||||
if widget:
|
||||
# Process events which happened before ensurence
|
||||
# - that is because some widgets could be not visible before
|
||||
# this method was called and have incorrect size
|
||||
QtWidgets.QApplication.processEvents()
|
||||
# Scroll to widget
|
||||
self.scroll_widget.ensureWidgetVisible(widget)
|
||||
|
||||
def set_path(self, path):
|
||||
self.breadcrumbs_widget.set_path(path)
|
||||
|
||||
def _add_developer_ui(self, footer_layout):
|
||||
modify_defaults_widget = QtWidgets.QWidget()
|
||||
modify_defaults_checkbox = QtWidgets.QCheckBox(modify_defaults_widget)
|
||||
|
|
@ -427,10 +464,19 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
def _on_reset_crash(self):
|
||||
self.save_btn.setEnabled(False)
|
||||
|
||||
if self.breadcrumbs_model is not None:
|
||||
self.breadcrumbs_model.set_entity(None)
|
||||
|
||||
def _on_reset_success(self):
|
||||
if not self.save_btn.isEnabled():
|
||||
self.save_btn.setEnabled(True)
|
||||
|
||||
if self.breadcrumbs_model is not None:
|
||||
path = self.breadcrumbs_widget.path()
|
||||
self.breadcrumbs_widget.set_path("")
|
||||
self.breadcrumbs_model.set_entity(self.entity)
|
||||
self.breadcrumbs_widget.change_path(path)
|
||||
|
||||
def add_children_gui(self):
|
||||
for child_obj in self.entity.children:
|
||||
item = self.create_ui_for_entity(self, child_obj, self)
|
||||
|
|
@ -521,6 +567,10 @@ class SystemWidget(SettingsCategoryWidget):
|
|||
self.modify_defaults_checkbox.setChecked(True)
|
||||
self.modify_defaults_checkbox.setEnabled(False)
|
||||
|
||||
def ui_tweaks(self):
|
||||
self.breadcrumbs_model = SystemSettingsBreadcrumbs()
|
||||
self.breadcrumbs_widget.set_model(self.breadcrumbs_model)
|
||||
|
||||
def _on_modify_defaults(self):
|
||||
if self.modify_defaults_checkbox.isChecked():
|
||||
if not self.entity.is_in_defaults_state():
|
||||
|
|
@ -535,9 +585,12 @@ class ProjectWidget(SettingsCategoryWidget):
|
|||
self.project_name = None
|
||||
|
||||
def ui_tweaks(self):
|
||||
self.breadcrumbs_model = ProjectSettingsBreadcrumbs()
|
||||
self.breadcrumbs_widget.set_model(self.breadcrumbs_model)
|
||||
|
||||
project_list_widget = ProjectListWidget(self)
|
||||
|
||||
self.main_layout.insertWidget(0, project_list_widget, 0)
|
||||
self.conf_wrapper_layout.insertWidget(0, project_list_widget, 0)
|
||||
|
||||
project_list_widget.project_changed.connect(self._on_project_change)
|
||||
|
||||
|
|
|
|||
|
|
@ -213,6 +213,26 @@ class DictConditionalWidget(BaseWidget):
|
|||
else:
|
||||
body_widget.hide_toolbox(hide_content=False)
|
||||
|
||||
def make_sure_is_visible(self, path, scroll_to):
|
||||
if not path:
|
||||
return False
|
||||
|
||||
entity_path = self.entity.path
|
||||
if entity_path == path:
|
||||
self.set_focus(scroll_to)
|
||||
return True
|
||||
|
||||
if not path.startswith(entity_path):
|
||||
return False
|
||||
|
||||
if self.body_widget and not self.body_widget.is_expanded():
|
||||
self.body_widget.toggle_content(True)
|
||||
|
||||
for input_field in self.input_fields:
|
||||
if input_field.make_sure_is_visible(path, scroll_to):
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_widget_to_layout(self, widget, label=None):
|
||||
if not widget.entity:
|
||||
map_id = widget.id
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
from uuid import uuid4
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from .base import BaseWidget
|
||||
from .widgets import (
|
||||
ExpandingWidget,
|
||||
IconButton,
|
||||
SpacerWidget
|
||||
IconButton
|
||||
)
|
||||
from openpype.tools.settings import (
|
||||
BTN_FIXED_SIZE,
|
||||
|
|
@ -15,6 +14,69 @@ from openpype.tools.settings import (
|
|||
from openpype.settings.constants import KEY_REGEX
|
||||
|
||||
|
||||
KEY_INPUT_TOOLTIP = (
|
||||
"Keys can't be duplicated and may contain alphabetical character (a-Z)"
|
||||
"\nnumerical characters (0-9) dash (\"-\") or underscore (\"_\")."
|
||||
)
|
||||
|
||||
|
||||
class PaintHelper:
|
||||
cached_icons = {}
|
||||
|
||||
@classmethod
|
||||
def _draw_image(cls, width, height, brush):
|
||||
image = QtGui.QPixmap(width, height)
|
||||
image.fill(QtCore.Qt.transparent)
|
||||
|
||||
icon_path_stroker = QtGui.QPainterPathStroker()
|
||||
icon_path_stroker.setCapStyle(QtCore.Qt.RoundCap)
|
||||
icon_path_stroker.setJoinStyle(QtCore.Qt.RoundJoin)
|
||||
icon_path_stroker.setWidth(height / 5)
|
||||
|
||||
painter = QtGui.QPainter(image)
|
||||
painter.setPen(QtCore.Qt.transparent)
|
||||
painter.setBrush(brush)
|
||||
rect = QtCore.QRect(0, 0, image.width(), image.height())
|
||||
fifteenth = rect.height() / 15
|
||||
# Left point
|
||||
p1 = QtCore.QPoint(
|
||||
rect.x() + (5 * fifteenth),
|
||||
rect.y() + (9 * fifteenth)
|
||||
)
|
||||
# Middle bottom point
|
||||
p2 = QtCore.QPoint(
|
||||
rect.center().x(),
|
||||
rect.y() + (11 * fifteenth)
|
||||
)
|
||||
# Top right point
|
||||
p3 = QtCore.QPoint(
|
||||
rect.x() + (10 * fifteenth),
|
||||
rect.y() + (5 * fifteenth)
|
||||
)
|
||||
|
||||
path = QtGui.QPainterPath(p1)
|
||||
path.lineTo(p2)
|
||||
path.lineTo(p3)
|
||||
|
||||
stroked_path = icon_path_stroker.createStroke(path)
|
||||
painter.drawPath(stroked_path)
|
||||
|
||||
painter.end()
|
||||
|
||||
return image
|
||||
|
||||
@classmethod
|
||||
def get_confirm_icon(cls, width, height):
|
||||
key = "{}x{}-confirm_image".format(width, height)
|
||||
icon = cls.cached_icons.get(key)
|
||||
|
||||
if icon is None:
|
||||
image = cls._draw_image(width, height, QtCore.Qt.white)
|
||||
icon = QtGui.QIcon(image)
|
||||
cls.cached_icons[key] = icon
|
||||
return icon
|
||||
|
||||
|
||||
def create_add_btn(parent):
|
||||
add_btn = QtWidgets.QPushButton("+", parent)
|
||||
add_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
|
||||
|
|
@ -31,6 +93,19 @@ def create_remove_btn(parent):
|
|||
return remove_btn
|
||||
|
||||
|
||||
def create_confirm_btn(parent):
|
||||
confirm_btn = QtWidgets.QPushButton(parent)
|
||||
|
||||
icon = PaintHelper.get_confirm_icon(
|
||||
BTN_FIXED_SIZE, BTN_FIXED_SIZE
|
||||
)
|
||||
confirm_btn.setIcon(icon)
|
||||
confirm_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
|
||||
confirm_btn.setProperty("btn-type", "tool-item")
|
||||
confirm_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE)
|
||||
return confirm_btn
|
||||
|
||||
|
||||
class ModifiableDictEmptyItem(QtWidgets.QWidget):
|
||||
def __init__(self, entity_widget, store_as_list, parent):
|
||||
super(ModifiableDictEmptyItem, self).__init__(parent)
|
||||
|
|
@ -42,6 +117,8 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
|
|||
self.is_duplicated = False
|
||||
self.key_is_valid = store_as_list
|
||||
|
||||
self.confirm_btn = None
|
||||
|
||||
if self.collapsible_key:
|
||||
self.create_collapsible_ui()
|
||||
else:
|
||||
|
|
@ -61,7 +138,6 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
|
|||
def create_addible_ui(self):
|
||||
add_btn = create_add_btn(self)
|
||||
remove_btn = create_remove_btn(self)
|
||||
spacer_widget = SpacerWidget(self)
|
||||
|
||||
remove_btn.setEnabled(False)
|
||||
|
||||
|
|
@ -70,13 +146,12 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
|
|||
layout.setSpacing(3)
|
||||
layout.addWidget(add_btn, 0)
|
||||
layout.addWidget(remove_btn, 0)
|
||||
layout.addWidget(spacer_widget, 1)
|
||||
layout.addStretch(1)
|
||||
|
||||
add_btn.clicked.connect(self._on_add_clicked)
|
||||
|
||||
self.add_btn = add_btn
|
||||
self.remove_btn = remove_btn
|
||||
self.spacer_widget = spacer_widget
|
||||
|
||||
def _on_focus_lose(self):
|
||||
if self.key_input.hasFocus() or self.key_label_input.hasFocus():
|
||||
|
|
@ -111,7 +186,16 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
|
|||
self.is_duplicated = self.entity_widget.is_key_duplicated(key)
|
||||
key_input_state = ""
|
||||
# Collapsible key and empty key are not invalid
|
||||
if self.collapsible_key and self.key_input.text() == "":
|
||||
key_value = self.key_input.text()
|
||||
if self.confirm_btn is not None:
|
||||
conf_disabled = (
|
||||
key_value == ""
|
||||
or not self.key_is_valid
|
||||
or self.is_duplicated
|
||||
)
|
||||
self.confirm_btn.setEnabled(not conf_disabled)
|
||||
|
||||
if self.collapsible_key and key_value == "":
|
||||
pass
|
||||
elif self.is_duplicated or not self.key_is_valid:
|
||||
key_input_state = "invalid"
|
||||
|
|
@ -124,6 +208,7 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
|
|||
def create_collapsible_ui(self):
|
||||
key_input = QtWidgets.QLineEdit(self)
|
||||
key_input.setObjectName("DictKey")
|
||||
key_input.setToolTip(KEY_INPUT_TOOLTIP)
|
||||
|
||||
key_label_input = QtWidgets.QLineEdit(self)
|
||||
|
||||
|
|
@ -141,11 +226,15 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
|
|||
key_input_label_widget = QtWidgets.QLabel("Key:", self)
|
||||
key_label_input_label_widget = QtWidgets.QLabel("Label:", self)
|
||||
|
||||
confirm_btn = create_confirm_btn(self)
|
||||
confirm_btn.setEnabled(False)
|
||||
|
||||
wrapper_widget = ExpandingWidget("", self)
|
||||
wrapper_widget.add_widget_after_label(key_input_label_widget)
|
||||
wrapper_widget.add_widget_after_label(key_input)
|
||||
wrapper_widget.add_widget_after_label(key_label_input_label_widget)
|
||||
wrapper_widget.add_widget_after_label(key_label_input)
|
||||
wrapper_widget.add_widget_after_label(confirm_btn)
|
||||
wrapper_widget.hide_toolbox()
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
|
@ -157,9 +246,12 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
|
|||
key_input.returnPressed.connect(self._on_enter_press)
|
||||
key_label_input.returnPressed.connect(self._on_enter_press)
|
||||
|
||||
confirm_btn.clicked.connect(self._on_enter_press)
|
||||
|
||||
self.key_input = key_input
|
||||
self.key_label_input = key_label_input
|
||||
self.wrapper_widget = wrapper_widget
|
||||
self.confirm_btn = confirm_btn
|
||||
|
||||
|
||||
class ModifiableDictItem(QtWidgets.QWidget):
|
||||
|
|
@ -190,10 +282,14 @@ class ModifiableDictItem(QtWidgets.QWidget):
|
|||
|
||||
self.key_label_input = None
|
||||
|
||||
self.confirm_btn = None
|
||||
|
||||
if collapsible_key:
|
||||
self.create_collapsible_ui()
|
||||
else:
|
||||
self.create_addible_ui()
|
||||
|
||||
self.key_input.setToolTip(KEY_INPUT_TOOLTIP)
|
||||
self.update_style()
|
||||
|
||||
@property
|
||||
|
|
@ -277,6 +373,9 @@ class ModifiableDictItem(QtWidgets.QWidget):
|
|||
edit_btn.setProperty("btn-type", "tool-item-icon")
|
||||
edit_btn.setFixedHeight(BTN_FIXED_SIZE)
|
||||
|
||||
confirm_btn = create_confirm_btn(self)
|
||||
confirm_btn.setVisible(False)
|
||||
|
||||
remove_btn = create_remove_btn(self)
|
||||
|
||||
key_input_label_widget = QtWidgets.QLabel("Key:")
|
||||
|
|
@ -286,6 +385,7 @@ class ModifiableDictItem(QtWidgets.QWidget):
|
|||
wrapper_widget.add_widget_after_label(key_input)
|
||||
wrapper_widget.add_widget_after_label(key_label_input_label_widget)
|
||||
wrapper_widget.add_widget_after_label(key_label_input)
|
||||
wrapper_widget.add_widget_after_label(confirm_btn)
|
||||
wrapper_widget.add_widget_after_label(remove_btn)
|
||||
|
||||
key_input.textChanged.connect(self._on_key_change)
|
||||
|
|
@ -295,6 +395,7 @@ class ModifiableDictItem(QtWidgets.QWidget):
|
|||
key_label_input.returnPressed.connect(self._on_enter_press)
|
||||
|
||||
edit_btn.clicked.connect(self.on_edit_pressed)
|
||||
confirm_btn.clicked.connect(self._on_enter_press)
|
||||
remove_btn.clicked.connect(self.on_remove_clicked)
|
||||
|
||||
# Hide edit inputs
|
||||
|
|
@ -310,6 +411,7 @@ class ModifiableDictItem(QtWidgets.QWidget):
|
|||
self.key_label_input_label_widget = key_label_input_label_widget
|
||||
self.wrapper_widget = wrapper_widget
|
||||
self.edit_btn = edit_btn
|
||||
self.confirm_btn = confirm_btn
|
||||
self.remove_btn = remove_btn
|
||||
|
||||
self.content_widget = content_widget
|
||||
|
|
@ -319,6 +421,9 @@ class ModifiableDictItem(QtWidgets.QWidget):
|
|||
self.category_widget, self.entity, self
|
||||
)
|
||||
|
||||
def make_sure_is_visible(self, *args, **kwargs):
|
||||
return self.input_field.make_sure_is_visible(*args, **kwargs)
|
||||
|
||||
def get_style_state(self):
|
||||
if self.is_invalid:
|
||||
return "invalid"
|
||||
|
|
@ -415,6 +520,14 @@ class ModifiableDictItem(QtWidgets.QWidget):
|
|||
self.temp_key, key, self
|
||||
)
|
||||
self.temp_key = key
|
||||
if self.confirm_btn is not None:
|
||||
conf_disabled = (
|
||||
key == ""
|
||||
or not self.key_is_valid
|
||||
or is_key_duplicated
|
||||
)
|
||||
self.confirm_btn.setEnabled(not conf_disabled)
|
||||
|
||||
if is_key_duplicated or not self.key_is_valid:
|
||||
return
|
||||
|
||||
|
|
@ -434,7 +547,7 @@ class ModifiableDictItem(QtWidgets.QWidget):
|
|||
key_value = self.key_input.text()
|
||||
key_label_value = self.key_label_input.text()
|
||||
if key_label_value:
|
||||
label = "{} ({})".format(key_label_value, key_value)
|
||||
label = "{} ({})".format(key_value, key_label_value)
|
||||
else:
|
||||
label = key_value
|
||||
self.wrapper_widget.label_widget.setText(label)
|
||||
|
|
@ -457,6 +570,7 @@ class ModifiableDictItem(QtWidgets.QWidget):
|
|||
self.key_input.setVisible(enabled)
|
||||
self.key_input_label_widget.setVisible(enabled)
|
||||
self.key_label_input.setVisible(enabled)
|
||||
self.confirm_btn.setVisible(enabled)
|
||||
if not self.is_required:
|
||||
self.remove_btn.setVisible(enabled)
|
||||
if enabled:
|
||||
|
|
@ -681,10 +795,6 @@ class DictMutableKeysWidget(BaseWidget):
|
|||
def remove_key(self, widget):
|
||||
key = self.entity.get_child_key(widget.entity)
|
||||
self.entity.pop(key)
|
||||
# Poping of key from entity should remove the entity and input field.
|
||||
# this is kept for testing purposes.
|
||||
if widget in self.input_fields:
|
||||
self.remove_row(widget)
|
||||
|
||||
def change_key(self, new_key, widget):
|
||||
if not new_key or widget.is_key_duplicated:
|
||||
|
|
@ -751,6 +861,11 @@ class DictMutableKeysWidget(BaseWidget):
|
|||
return input_field
|
||||
|
||||
def remove_row(self, widget):
|
||||
if widget.is_key_duplicated:
|
||||
new_key = widget.uuid_key
|
||||
if new_key is None:
|
||||
new_key = str(uuid4())
|
||||
self.validate_key_duplication(widget.temp_key, new_key, widget)
|
||||
self.input_fields.remove(widget)
|
||||
self.content_layout.removeWidget(widget)
|
||||
widget.deleteLater()
|
||||
|
|
@ -834,7 +949,10 @@ class DictMutableKeysWidget(BaseWidget):
|
|||
_input_field.set_entity_value()
|
||||
|
||||
else:
|
||||
if input_field.key_value() != key:
|
||||
if (
|
||||
not input_field.is_key_duplicated
|
||||
and input_field.key_value() != key
|
||||
):
|
||||
changed = True
|
||||
input_field.set_key(key)
|
||||
|
||||
|
|
@ -846,6 +964,26 @@ class DictMutableKeysWidget(BaseWidget):
|
|||
if changed:
|
||||
self.on_shuffle()
|
||||
|
||||
def make_sure_is_visible(self, path, scroll_to):
|
||||
if not path:
|
||||
return False
|
||||
|
||||
entity_path = self.entity.path
|
||||
if entity_path == path:
|
||||
self.set_focus(scroll_to)
|
||||
return True
|
||||
|
||||
if not path.startswith(entity_path):
|
||||
return False
|
||||
|
||||
if self.body_widget and not self.body_widget.is_expanded():
|
||||
self.body_widget.toggle_content(True)
|
||||
|
||||
for input_field in self.input_fields:
|
||||
if input_field.make_sure_is_visible(path, scroll_to):
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_entity_value(self):
|
||||
while self.input_fields:
|
||||
self.remove_row(self.input_fields[0])
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ from .widgets import (
|
|||
ExpandingWidget,
|
||||
NumberSpinBox,
|
||||
GridLabelWidget,
|
||||
ComboBox,
|
||||
NiceCheckbox
|
||||
SettingsComboBox,
|
||||
NiceCheckbox,
|
||||
SettingsPlainTextEdit,
|
||||
SettingsLineEdit
|
||||
)
|
||||
from .multiselection_combobox import MultiSelectionComboBox
|
||||
from .wrapper_widgets import (
|
||||
|
|
@ -46,6 +48,7 @@ class DictImmutableKeysWidget(BaseWidget):
|
|||
self._ui_item_base()
|
||||
label = self.entity.label
|
||||
|
||||
self._direct_children_widgets = []
|
||||
self._parent_widget_by_entity_id = {}
|
||||
self._added_wrapper_ids = set()
|
||||
self._prepare_entity_layouts(
|
||||
|
|
@ -154,9 +157,41 @@ class DictImmutableKeysWidget(BaseWidget):
|
|||
else:
|
||||
body_widget.hide_toolbox(hide_content=False)
|
||||
|
||||
def make_sure_is_visible(self, path, scroll_to):
|
||||
if not path:
|
||||
return False
|
||||
|
||||
entity_path = self.entity.path
|
||||
if entity_path == path:
|
||||
self.set_focus(scroll_to)
|
||||
return True
|
||||
|
||||
if not path.startswith(entity_path):
|
||||
return False
|
||||
|
||||
is_checkbox_child = False
|
||||
changed = False
|
||||
for direct_child in self._direct_children_widgets:
|
||||
if direct_child.make_sure_is_visible(path, scroll_to):
|
||||
changed = True
|
||||
if direct_child.entity is self.checkbox_child:
|
||||
is_checkbox_child = True
|
||||
break
|
||||
|
||||
# Change scroll to this widget
|
||||
if is_checkbox_child:
|
||||
self.scroll_to(self)
|
||||
|
||||
elif self.body_widget and not self.body_widget.is_expanded():
|
||||
# Expand widget if is callapsible
|
||||
self.body_widget.toggle_content(True)
|
||||
|
||||
return changed
|
||||
|
||||
def add_widget_to_layout(self, widget, label=None):
|
||||
if self.checkbox_child and widget.entity is self.checkbox_child:
|
||||
self.body_widget.add_widget_before_label(widget)
|
||||
self._direct_children_widgets.append(widget)
|
||||
return
|
||||
|
||||
if not widget.entity:
|
||||
|
|
@ -172,6 +207,8 @@ class DictImmutableKeysWidget(BaseWidget):
|
|||
self._added_wrapper_ids.add(wrapper.id)
|
||||
return
|
||||
|
||||
self._direct_children_widgets.append(widget)
|
||||
|
||||
row = self.content_layout.rowCount()
|
||||
if not label or isinstance(widget, WrapperWidget):
|
||||
self.content_layout.addWidget(widget, row, 0, 1, 2)
|
||||
|
|
@ -270,11 +307,8 @@ class BoolWidget(InputWidget):
|
|||
height=checkbox_height, parent=self.content_widget
|
||||
)
|
||||
|
||||
spacer = QtWidgets.QWidget(self.content_widget)
|
||||
spacer.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
self.content_layout.addWidget(self.input_field, 0)
|
||||
self.content_layout.addWidget(spacer, 1)
|
||||
self.content_layout.addStretch(1)
|
||||
|
||||
self.setFocusProxy(self.input_field)
|
||||
|
||||
|
|
@ -297,9 +331,9 @@ class TextWidget(InputWidget):
|
|||
def _add_inputs_to_layout(self):
|
||||
multiline = self.entity.multiline
|
||||
if multiline:
|
||||
self.input_field = QtWidgets.QPlainTextEdit(self.content_widget)
|
||||
self.input_field = SettingsPlainTextEdit(self.content_widget)
|
||||
else:
|
||||
self.input_field = QtWidgets.QLineEdit(self.content_widget)
|
||||
self.input_field = SettingsLineEdit(self.content_widget)
|
||||
|
||||
placeholder_text = self.entity.placeholder_text
|
||||
if placeholder_text:
|
||||
|
|
@ -313,8 +347,12 @@ class TextWidget(InputWidget):
|
|||
|
||||
self.content_layout.addWidget(self.input_field, 1, **layout_kwargs)
|
||||
|
||||
self.input_field.focused_in.connect(self._on_input_focus)
|
||||
self.input_field.textChanged.connect(self._on_value_change)
|
||||
|
||||
def _on_input_focus(self):
|
||||
self.focused_in()
|
||||
|
||||
def _on_entity_change(self):
|
||||
if self.entity.value != self.input_value():
|
||||
self.set_entity_value()
|
||||
|
|
@ -352,6 +390,10 @@ class NumberWidget(InputWidget):
|
|||
self.content_layout.addWidget(self.input_field, 1)
|
||||
|
||||
self.input_field.valueChanged.connect(self._on_value_change)
|
||||
self.input_field.focused_in.connect(self._on_input_focus)
|
||||
|
||||
def _on_input_focus(self):
|
||||
self.focused_in()
|
||||
|
||||
def _on_entity_change(self):
|
||||
if self.entity.value != self.input_field.value():
|
||||
|
|
@ -366,7 +408,7 @@ class NumberWidget(InputWidget):
|
|||
self.entity.set(self.input_field.value())
|
||||
|
||||
|
||||
class RawJsonInput(QtWidgets.QPlainTextEdit):
|
||||
class RawJsonInput(SettingsPlainTextEdit):
|
||||
tab_length = 4
|
||||
|
||||
def __init__(self, valid_type, *args, **kwargs):
|
||||
|
|
@ -428,15 +470,18 @@ class RawJsonWidget(InputWidget):
|
|||
QtWidgets.QSizePolicy.Minimum,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding
|
||||
)
|
||||
|
||||
self.setFocusProxy(self.input_field)
|
||||
|
||||
self.content_layout.addWidget(
|
||||
self.input_field, 1, alignment=QtCore.Qt.AlignTop
|
||||
)
|
||||
|
||||
self.input_field.focused_in.connect(self._on_input_focus)
|
||||
self.input_field.textChanged.connect(self._on_value_change)
|
||||
|
||||
def _on_input_focus(self):
|
||||
self.focused_in()
|
||||
|
||||
def set_entity_value(self):
|
||||
self.input_field.set_value(self.entity.value)
|
||||
self._is_invalid = self.input_field.has_invalid_value()
|
||||
|
|
@ -470,7 +515,7 @@ class EnumeratorWidget(InputWidget):
|
|||
)
|
||||
|
||||
else:
|
||||
self.input_field = ComboBox(self.content_widget)
|
||||
self.input_field = SettingsComboBox(self.content_widget)
|
||||
|
||||
for enum_item in self.entity.enum_items:
|
||||
for value, label in enum_item.items():
|
||||
|
|
@ -480,8 +525,12 @@ class EnumeratorWidget(InputWidget):
|
|||
|
||||
self.setFocusProxy(self.input_field)
|
||||
|
||||
self.input_field.focused_in.connect(self._on_input_focus)
|
||||
self.input_field.value_changed.connect(self._on_value_change)
|
||||
|
||||
def _on_input_focus(self):
|
||||
self.focused_in()
|
||||
|
||||
def _on_entity_change(self):
|
||||
if self.entity.value != self.input_field.value():
|
||||
self.set_entity_value()
|
||||
|
|
@ -562,6 +611,9 @@ class PathWidget(BaseWidget):
|
|||
def set_entity_value(self):
|
||||
self.input_field.set_entity_value()
|
||||
|
||||
def make_sure_is_visible(self, *args, **kwargs):
|
||||
return self.input_field.make_sure_is_visible(*args, **kwargs)
|
||||
|
||||
def hierarchical_style_update(self):
|
||||
self.update_style()
|
||||
self.input_field.hierarchical_style_update()
|
||||
|
|
@ -632,14 +684,19 @@ class PathWidget(BaseWidget):
|
|||
|
||||
class PathInputWidget(InputWidget):
|
||||
def _add_inputs_to_layout(self):
|
||||
self.input_field = QtWidgets.QLineEdit(self.content_widget)
|
||||
self.input_field = SettingsLineEdit(self.content_widget)
|
||||
placeholder = self.entity.placeholder_text
|
||||
if placeholder:
|
||||
self.input_field.setPlaceholderText(placeholder)
|
||||
|
||||
self.setFocusProxy(self.input_field)
|
||||
self.content_layout.addWidget(self.input_field)
|
||||
|
||||
self.input_field.textChanged.connect(self._on_value_change)
|
||||
self.input_field.focused_in.connect(self._on_input_focus)
|
||||
|
||||
def _on_input_focus(self):
|
||||
self.focused_in()
|
||||
|
||||
def _on_entity_change(self):
|
||||
if self.entity.value != self.input_value():
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ class EmptyListItem(QtWidgets.QWidget):
|
|||
|
||||
add_btn = QtWidgets.QPushButton("+", self)
|
||||
remove_btn = QtWidgets.QPushButton("-", self)
|
||||
spacer_widget = QtWidgets.QWidget(self)
|
||||
spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
add_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
|
||||
remove_btn.setEnabled(False)
|
||||
|
|
@ -35,13 +33,12 @@ class EmptyListItem(QtWidgets.QWidget):
|
|||
layout.setSpacing(3)
|
||||
layout.addWidget(add_btn, 0)
|
||||
layout.addWidget(remove_btn, 0)
|
||||
layout.addWidget(spacer_widget, 1)
|
||||
layout.addStretch(1)
|
||||
|
||||
add_btn.clicked.connect(self._on_add_clicked)
|
||||
|
||||
self.add_btn = add_btn
|
||||
self.remove_btn = remove_btn
|
||||
self.spacer_widget = spacer_widget
|
||||
|
||||
def _on_add_clicked(self):
|
||||
self.entity_widget.add_new_item()
|
||||
|
|
@ -101,12 +98,6 @@ class ListItem(QtWidgets.QWidget):
|
|||
self.category_widget, self.entity, self
|
||||
)
|
||||
|
||||
spacer_widget = QtWidgets.QWidget(self)
|
||||
spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
spacer_widget.setVisible(False)
|
||||
|
||||
layout.addWidget(spacer_widget, 1)
|
||||
|
||||
layout.addWidget(up_btn, 0)
|
||||
layout.addWidget(down_btn, 0)
|
||||
|
||||
|
|
@ -115,8 +106,6 @@ class ListItem(QtWidgets.QWidget):
|
|||
self.up_btn = up_btn
|
||||
self.down_btn = down_btn
|
||||
|
||||
self.spacer_widget = spacer_widget
|
||||
|
||||
self._row = -1
|
||||
self._is_last = False
|
||||
|
||||
|
|
@ -129,6 +118,9 @@ class ListItem(QtWidgets.QWidget):
|
|||
*args, **kwargs
|
||||
)
|
||||
|
||||
def make_sure_is_visible(self, *args, **kwargs):
|
||||
return self.input_field.make_sure_is_visible(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def is_invalid(self):
|
||||
return self.input_field.is_invalid
|
||||
|
|
@ -275,6 +267,26 @@ class ListWidget(InputWidget):
|
|||
invalid.extend(input_field.get_invalid())
|
||||
return invalid
|
||||
|
||||
def make_sure_is_visible(self, path, scroll_to):
|
||||
if not path:
|
||||
return False
|
||||
|
||||
entity_path = self.entity.path
|
||||
if entity_path == path:
|
||||
self.set_focus(scroll_to)
|
||||
return True
|
||||
|
||||
if not path.startswith(entity_path):
|
||||
return False
|
||||
|
||||
if self.body_widget and not self.body_widget.is_expanded():
|
||||
self.body_widget.toggle_content(True)
|
||||
|
||||
for input_field in self.input_fields:
|
||||
if input_field.make_sure_is_visible(path, scroll_to):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _on_entity_change(self):
|
||||
# TODO do less inefficient
|
||||
childen_order = []
|
||||
|
|
|
|||
|
|
@ -65,6 +65,21 @@ class ListStrictWidget(BaseWidget):
|
|||
invalid.extend(input_field.get_invalid())
|
||||
return invalid
|
||||
|
||||
def make_sure_is_visible(self, path, scroll_to):
|
||||
if not path:
|
||||
return False
|
||||
|
||||
entity_path = self.entity.path
|
||||
if entity_path == path:
|
||||
self.set_focus(scroll_to)
|
||||
return True
|
||||
|
||||
if path.startswith(entity_path):
|
||||
for input_field in self.input_fields:
|
||||
if input_field.make_sure_is_visible(path, scroll_to):
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_widget_to_layout(self, widget, label=None):
|
||||
# Horizontally added children
|
||||
if self.entity.is_horizontal:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ class ComboItemDelegate(QtWidgets.QStyledItemDelegate):
|
|||
|
||||
class MultiSelectionComboBox(QtWidgets.QComboBox):
|
||||
value_changed = QtCore.Signal()
|
||||
focused_in = QtCore.Signal()
|
||||
|
||||
ignored_keys = {
|
||||
QtCore.Qt.Key_Up,
|
||||
QtCore.Qt.Key_Down,
|
||||
|
|
@ -56,6 +58,10 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
|
|||
self.lines = {}
|
||||
self.item_height = None
|
||||
|
||||
def focusInEvent(self, event):
|
||||
self.focused_in.emit()
|
||||
return super(MultiSelectionComboBox, self).focusInEvent(event)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""Reimplemented."""
|
||||
self._popup_is_shown = False
|
||||
|
|
|
|||
|
|
@ -388,4 +388,32 @@ QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed {
|
|||
|
||||
QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active {
|
||||
background: #3d8ec9;
|
||||
}
|
||||
}
|
||||
|
||||
#BreadcrumbsPathInput {
|
||||
padding: 2px;
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
#BreadcrumbsButton {
|
||||
padding-right: 12px;
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
#BreadcrumbsButton[empty="1"] {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
#BreadcrumbsButton::menu-button {
|
||||
width: 12px;
|
||||
background: rgba(127, 127, 127, 60);
|
||||
}
|
||||
#BreadcrumbsButton::menu-button:hover {
|
||||
background: rgba(127, 127, 127, 90);
|
||||
}
|
||||
|
||||
#BreadcrumbsPanel {
|
||||
border: 1px solid #4e5254;
|
||||
border-radius: 5px;
|
||||
background: #21252B;;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,22 @@ from avalon.mongodb import (
|
|||
from openpype.settings.lib import get_system_settings
|
||||
|
||||
|
||||
class SettingsLineEdit(QtWidgets.QLineEdit):
|
||||
focused_in = QtCore.Signal()
|
||||
|
||||
def focusInEvent(self, event):
|
||||
super(SettingsLineEdit, self).focusInEvent(event)
|
||||
self.focused_in.emit()
|
||||
|
||||
|
||||
class SettingsPlainTextEdit(QtWidgets.QPlainTextEdit):
|
||||
focused_in = QtCore.Signal()
|
||||
|
||||
def focusInEvent(self, event):
|
||||
super(SettingsPlainTextEdit, self).focusInEvent(event)
|
||||
self.focused_in.emit()
|
||||
|
||||
|
||||
class ShadowWidget(QtWidgets.QWidget):
|
||||
def __init__(self, message, parent):
|
||||
super(ShadowWidget, self).__init__(parent)
|
||||
|
|
@ -70,6 +86,8 @@ class IconButton(QtWidgets.QPushButton):
|
|||
|
||||
|
||||
class NumberSpinBox(QtWidgets.QDoubleSpinBox):
|
||||
focused_in = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
min_value = kwargs.pop("minimum", -99999)
|
||||
max_value = kwargs.pop("maximum", 99999)
|
||||
|
|
@ -80,6 +98,10 @@ class NumberSpinBox(QtWidgets.QDoubleSpinBox):
|
|||
self.setMinimum(min_value)
|
||||
self.setMaximum(max_value)
|
||||
|
||||
def focusInEvent(self, event):
|
||||
super(NumberSpinBox, self).focusInEvent(event)
|
||||
self.focused_in.emit()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
if self.hasFocus():
|
||||
super(NumberSpinBox, self).wheelEvent(event)
|
||||
|
|
@ -93,18 +115,23 @@ class NumberSpinBox(QtWidgets.QDoubleSpinBox):
|
|||
return output
|
||||
|
||||
|
||||
class ComboBox(QtWidgets.QComboBox):
|
||||
class SettingsComboBox(QtWidgets.QComboBox):
|
||||
value_changed = QtCore.Signal()
|
||||
focused_in = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ComboBox, self).__init__(*args, **kwargs)
|
||||
super(SettingsComboBox, self).__init__(*args, **kwargs)
|
||||
|
||||
self.currentIndexChanged.connect(self._on_change)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
||||
def wheelEvent(self, event):
|
||||
if self.hasFocus():
|
||||
return super(ComboBox, self).wheelEvent(event)
|
||||
return super(SettingsComboBox, self).wheelEvent(event)
|
||||
|
||||
def focusInEvent(self, event):
|
||||
self.focused_in.emit()
|
||||
return super(SettingsComboBox, self).focusInEvent(event)
|
||||
|
||||
def _on_change(self, *args, **kwargs):
|
||||
self.value_changed.emit()
|
||||
|
|
@ -160,15 +187,13 @@ class ExpandingWidget(QtWidgets.QWidget):
|
|||
after_label_layout = QtWidgets.QHBoxLayout(after_label_widget)
|
||||
after_label_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
spacer_widget = QtWidgets.QWidget(side_line_widget)
|
||||
|
||||
side_line_layout = QtWidgets.QHBoxLayout(side_line_widget)
|
||||
side_line_layout.setContentsMargins(5, 10, 0, 10)
|
||||
side_line_layout.addWidget(button_toggle)
|
||||
side_line_layout.addWidget(before_label_widget)
|
||||
side_line_layout.addWidget(label_widget)
|
||||
side_line_layout.addWidget(after_label_widget)
|
||||
side_line_layout.addWidget(spacer_widget, 1)
|
||||
side_line_layout.addStretch(1)
|
||||
|
||||
top_part_layout = QtWidgets.QHBoxLayout(top_part)
|
||||
top_part_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -176,7 +201,6 @@ class ExpandingWidget(QtWidgets.QWidget):
|
|||
|
||||
before_label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
after_label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
|
|
@ -215,6 +239,9 @@ class ExpandingWidget(QtWidgets.QWidget):
|
|||
self.main_layout.addWidget(content_widget)
|
||||
self.content_widget = content_widget
|
||||
|
||||
def is_expanded(self):
|
||||
return self.button_toggle.isChecked()
|
||||
|
||||
def _btn_clicked(self):
|
||||
self.toggle_content(self.button_toggle.isChecked())
|
||||
|
||||
|
|
@ -341,31 +368,21 @@ class GridLabelWidget(QtWidgets.QWidget):
|
|||
|
||||
self.properties = {}
|
||||
|
||||
label_widget = QtWidgets.QLabel(label, self)
|
||||
|
||||
label_proxy_layout = QtWidgets.QHBoxLayout()
|
||||
label_proxy_layout.setContentsMargins(0, 0, 0, 0)
|
||||
label_proxy_layout.setSpacing(0)
|
||||
|
||||
label_proxy_layout.addWidget(label_widget, 0, QtCore.Qt.AlignRight)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 2, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
label_proxy = QtWidgets.QWidget(self)
|
||||
layout.addLayout(label_proxy_layout, 0)
|
||||
layout.addStretch(1)
|
||||
|
||||
label_proxy_layout = QtWidgets.QHBoxLayout(label_proxy)
|
||||
label_proxy_layout.setContentsMargins(0, 0, 0, 0)
|
||||
label_proxy_layout.setSpacing(0)
|
||||
|
||||
label_widget = QtWidgets.QLabel(label, label_proxy)
|
||||
spacer_widget_h = SpacerWidget(label_proxy)
|
||||
label_proxy_layout.addWidget(
|
||||
spacer_widget_h, 0, alignment=QtCore.Qt.AlignRight
|
||||
)
|
||||
label_proxy_layout.addWidget(
|
||||
label_widget, 0, alignment=QtCore.Qt.AlignRight
|
||||
)
|
||||
|
||||
spacer_widget_v = SpacerWidget(self)
|
||||
|
||||
layout.addWidget(label_proxy, 0)
|
||||
layout.addWidget(spacer_widget_v, 1)
|
||||
|
||||
label_proxy.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
self.label_widget = label_widget
|
||||
|
|
@ -380,6 +397,8 @@ class GridLabelWidget(QtWidgets.QWidget):
|
|||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.input_field:
|
||||
if event and event.button() == QtCore.Qt.LeftButton:
|
||||
self.input_field.focused_in()
|
||||
return self.input_field.show_actions_menu(event)
|
||||
return super(GridLabelWidget, self).mouseReleaseEvent(event)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@ class WrapperWidget(QtWidgets.QWidget):
|
|||
|
||||
self.create_ui()
|
||||
|
||||
def make_sure_is_visible(self, *args, **kwargs):
|
||||
changed = False
|
||||
for input_field in self.input_fields:
|
||||
if input_field.make_sure_is_visible(*args, **kwargs):
|
||||
changed = True
|
||||
break
|
||||
return changed
|
||||
|
||||
def create_ui(self):
|
||||
raise NotImplementedError(
|
||||
"{} does not have implemented `create_ui`.".format(
|
||||
|
|
@ -89,6 +97,14 @@ class CollapsibleWrapper(WrapperWidget):
|
|||
else:
|
||||
body_widget.hide_toolbox(hide_content=False)
|
||||
|
||||
def make_sure_is_visible(self, *args, **kwargs):
|
||||
result = super(CollapsibleWrapper, self).make_sure_is_visible(
|
||||
*args, **kwargs
|
||||
)
|
||||
if result:
|
||||
self.body_widget.toggle_content(True)
|
||||
return result
|
||||
|
||||
def add_widget_to_layout(self, widget, label=None):
|
||||
self.input_fields.append(widget)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.3.0-nightly.9"
|
||||
__version__ = "3.3.0-nightly.10"
|
||||
|
|
|
|||
|
|
@ -50,8 +50,18 @@ function Install-Poetry() {
|
|||
Write-Host "Installing Poetry ... "
|
||||
$python = "python"
|
||||
if (Get-Command "pyenv" -ErrorAction SilentlyContinue) {
|
||||
if (-not (Test-Path -PathType Leaf -Path "$($openpype_root)\.python-version")) {
|
||||
$result = & pyenv global
|
||||
if ($result -eq "no global version configured") {
|
||||
Write-Host "!!! " -NoNewline -ForegroundColor Red
|
||||
Write-Host "Using pyenv but having no local or global version of Python set."
|
||||
Exit-WithCode 1
|
||||
}
|
||||
}
|
||||
$python = & pyenv which python
|
||||
|
||||
}
|
||||
|
||||
$env:POETRY_HOME="$openpype_root\.poetry"
|
||||
(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | & $($python) -
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@ def inject_openpype_environment(deadlinePlugin):
|
|||
"AVALON_TASK, AVALON_APP_NAME"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
print("args::{}".format(args))
|
||||
print("args:::{}".format(args))
|
||||
|
||||
exit_code = subprocess.call(args, shell=True)
|
||||
exit_code = subprocess.call(args, cwd=os.path.dirname(openpype_app))
|
||||
if exit_code != 0:
|
||||
raise RuntimeError("Publishing failed, check worker's log")
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,26 @@ When you publish your model with top group named like `foo_GRP` it will fail. Bu
|
|||
All regexes used here are in Python variant.
|
||||
:::
|
||||
|
||||
### Maya > Deadline submitter
|
||||
This plugin provides connection between Maya and Deadline. It is using [Deadline Webservice](https://docs.thinkboxsoftware.com/products/deadline/10.0/1_User%20Manual/manual/web-service.html) to submit jobs to farm.
|
||||

|
||||
|
||||
You can set various aspects of scene submission to farm with per-project settings in **Setting UI**.
|
||||
|
||||
- **Optional** will mark sumission plugin optional
|
||||
- **Active** will enable/disable plugin
|
||||
- **Tile Assembler Plugin** will set what should be used to assemble tiles on Deadline. Either **Open Image IO** will be used
|
||||
or Deadlines **Draft Tile Assembler**.
|
||||
- **Use Published scene** enable to render from published scene instead of scene in work area. Rendering from published files is much safer.
|
||||
- **Use Asset dependencies** will mark job pending on farm until asset dependencies are fulfilled - for example Deadline will wait for scene file to be synced to cloud, etc.
|
||||
- **Group name** use specific Deadline group for the job.
|
||||
- **Limit Groups** use these Deadline Limit groups for the job.
|
||||
- **Additional `JobInfo` data** JSON of additional Deadline options that will be embedded in `JobInfo` part of the submission data.
|
||||
- **Additional `PluginInfo` data** JSON of additional Deadline options that will be embedded in `PluginInfo` part of the submission data.
|
||||
- **Scene patches** - configure mechanism to add additional lines to published Maya Ascii scene files before they are used for rendering.
|
||||
This is useful to fix some specific renderer glitches and advanced hacking of Maya Scene files. `Patch name` is label for patch for easier orientation.
|
||||
`Patch regex` is regex used to find line in file, after `Patch line` string is inserted. Note that you need to add line ending.
|
||||
|
||||
## Custom Menu
|
||||
You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**.
|
||||

|
||||
|
|
@ -94,4 +114,9 @@ You can add your custom tools menu into Maya by extending definitions in **Maya
|
|||
:::note Work in progress
|
||||
This is still work in progress. Menu definition will be handled more friendly with widgets and not
|
||||
raw json.
|
||||
:::
|
||||
:::
|
||||
|
||||
## Multiplatform path mapping
|
||||
You can configure path mapping using Maya `dirmap` command. This will add bi-directional mapping between
|
||||
list of paths specified in **Settings**. You can find it in **Settings -> Project Settings -> Maya -> Maya Directory Mapping**
|
||||

|
||||
|
|
|
|||
BIN
website/docs/assets/maya-admin_dirmap_settings.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
website/docs/assets/maya-admin_submit_maya_job_to_deadline.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 582 151" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-654,-2331)">
|
||||
<g id="pypeclub_black" transform="matrix(0.775972,0,0,0.911492,654.799,1794.12)">
|
||||
<g transform="matrix(1,0,0,1,-987,-2015)">
|
||||
<g id="pypeclub_black" transform="matrix(0.775972,0,0,0.911492,987.586,1477.95)">
|
||||
<rect x="0" y="589.923" width="748.802" height="164.565" style="fill:none;"/>
|
||||
<g transform="matrix(2.7183,0,0,1.24969,-3.76111,624.796)">
|
||||
<g id="PYPE" transform="matrix(1,0,0,1,-105.172,0)">
|
||||
|
|
@ -20,7 +20,21 @@
|
|||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.474085,0,0,0.877899,142.749,-0.167247)">
|
||||
<text x="2.312px" y="79.899px" style="font-family:'Poppins-Light', 'Poppins';font-weight:300;font-size:100px;">.club</text>
|
||||
<g transform="matrix(100,0,0,100,2.312,79.8987)">
|
||||
<path d="M0.092,0.005C0.077,0.005 0.065,-0 0.056,-0.01C0.046,-0.02 0.041,-0.032 0.041,-0.047C0.041,-0.062 0.046,-0.074 0.056,-0.084C0.065,-0.093 0.077,-0.098 0.092,-0.098C0.106,-0.098 0.118,-0.093 0.128,-0.084C0.137,-0.074 0.142,-0.062 0.142,-0.047C0.142,-0.032 0.137,-0.02 0.128,-0.01C0.118,-0 0.106,0.005 0.092,0.005Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(100,0,0,100,20.5107,79.8987)">
|
||||
<path d="M0.048,-0.273C0.048,-0.33 0.059,-0.379 0.082,-0.422C0.105,-0.464 0.136,-0.497 0.176,-0.52C0.216,-0.543 0.262,-0.554 0.313,-0.554C0.38,-0.554 0.436,-0.537 0.48,-0.504C0.523,-0.471 0.551,-0.425 0.564,-0.368L0.489,-0.368C0.48,-0.407 0.46,-0.438 0.429,-0.461C0.398,-0.483 0.359,-0.494 0.313,-0.494C0.276,-0.494 0.243,-0.486 0.214,-0.469C0.185,-0.452 0.162,-0.428 0.145,-0.395C0.128,-0.362 0.119,-0.321 0.119,-0.273C0.119,-0.225 0.128,-0.184 0.145,-0.151C0.162,-0.118 0.185,-0.093 0.214,-0.076C0.243,-0.059 0.276,-0.051 0.313,-0.051C0.359,-0.051 0.398,-0.062 0.429,-0.085C0.46,-0.107 0.48,-0.138 0.489,-0.178L0.564,-0.178C0.551,-0.122 0.523,-0.077 0.479,-0.043C0.435,-0.009 0.38,0.008 0.313,0.008C0.262,0.008 0.216,-0.004 0.176,-0.027C0.136,-0.05 0.105,-0.082 0.082,-0.125C0.059,-0.167 0.048,-0.216 0.048,-0.273Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(100,0,0,100,81.8095,79.8987)">
|
||||
<rect x="0.08" y="-0.74" width="0.07" height="0.74" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(100,0,0,100,104.708,79.8987)">
|
||||
<path d="M0.553,-0.546L0.553,-0L0.483,-0L0.483,-0.096C0.467,-0.062 0.442,-0.036 0.409,-0.018C0.376,-0 0.338,0.009 0.297,0.009C0.232,0.009 0.178,-0.011 0.137,-0.051C0.096,-0.092 0.075,-0.15 0.075,-0.227L0.075,-0.546L0.144,-0.546L0.144,-0.235C0.144,-0.176 0.159,-0.13 0.189,-0.099C0.218,-0.068 0.259,-0.052 0.31,-0.052C0.363,-0.052 0.405,-0.069 0.436,-0.102C0.467,-0.135 0.483,-0.184 0.483,-0.249L0.483,-0.546L0.553,-0.546Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(100,0,0,100,168.007,79.8987)">
|
||||
<path d="M0.149,-0.425C0.167,-0.463 0.195,-0.494 0.233,-0.518C0.27,-0.542 0.315,-0.554 0.366,-0.554C0.416,-0.554 0.461,-0.543 0.5,-0.52C0.539,-0.497 0.57,-0.464 0.593,-0.422C0.615,-0.379 0.626,-0.33 0.626,-0.274C0.626,-0.218 0.615,-0.169 0.593,-0.126C0.57,-0.083 0.539,-0.05 0.5,-0.027C0.46,-0.004 0.415,0.008 0.366,0.008C0.314,0.008 0.269,-0.004 0.232,-0.028C0.194,-0.051 0.166,-0.082 0.149,-0.12L0.149,-0L0.08,-0L0.08,-0.74L0.149,-0.74L0.149,-0.425ZM0.555,-0.274C0.555,-0.319 0.546,-0.359 0.529,-0.392C0.511,-0.425 0.487,-0.45 0.456,-0.467C0.425,-0.484 0.391,-0.493 0.352,-0.493C0.315,-0.493 0.281,-0.484 0.25,-0.466C0.219,-0.448 0.194,-0.422 0.176,-0.389C0.158,-0.356 0.149,-0.317 0.149,-0.273C0.149,-0.229 0.158,-0.19 0.176,-0.157C0.194,-0.124 0.219,-0.098 0.25,-0.08C0.281,-0.062 0.315,-0.053 0.352,-0.053C0.391,-0.053 0.425,-0.062 0.456,-0.08C0.487,-0.097 0.511,-0.123 0.529,-0.157C0.546,-0.19 0.555,-0.229 0.555,-0.274Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 6.9 KiB |
|
|
@ -1,26 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 582 151" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-35,-2135)">
|
||||
<g id="pypeclub_color_white" transform="matrix(0.775972,0,0,0.911492,35.6167,1597.77)">
|
||||
<g transform="matrix(1,0,0,1,-987,-1797)">
|
||||
<g id="pypeclub_color_white" transform="matrix(0.775972,0,0,0.911492,987.586,1259.42)">
|
||||
<rect x="0" y="589.923" width="748.802" height="164.565" style="fill:none;"/>
|
||||
<g transform="matrix(2.7183,0,0,1.24969,-3.76111,624.796)">
|
||||
<g id="PYPE" transform="matrix(1,0,0,1,-105.172,0)">
|
||||
<g transform="matrix(47.4085,0,0,87.7899,124.81,70.8632)">
|
||||
<path d="M0.629,-0.465C0.629,-0.42 0.619,-0.38 0.598,-0.344C0.577,-0.307 0.547,-0.278 0.506,-0.257C0.466,-0.236 0.417,-0.225 0.36,-0.225L0.272,-0.225L0.272,-0.05C0.272,-0.037 0.267,-0.024 0.257,-0.015C0.248,-0.005 0.235,0 0.222,0L0.1,0C0.072,0 0.05,-0.022 0.05,-0.05L0.05,-0.658C0.05,-0.686 0.072,-0.708 0.1,-0.708L0.36,-0.708C0.447,-0.708 0.513,-0.686 0.56,-0.642C0.606,-0.598 0.629,-0.539 0.629,-0.465ZM0.335,-0.4C0.381,-0.4 0.404,-0.422 0.404,-0.465C0.404,-0.508 0.381,-0.53 0.335,-0.53L0.272,-0.53L0.272,-0.4L0.335,-0.4Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||
<path d="M0.629,-0.465C0.629,-0.42 0.619,-0.38 0.598,-0.344C0.577,-0.307 0.547,-0.278 0.506,-0.257C0.466,-0.236 0.417,-0.225 0.36,-0.225L0.272,-0.225L0.272,-0.05C0.272,-0.037 0.267,-0.024 0.257,-0.015C0.248,-0.005 0.235,0 0.222,0L0.1,-0C0.072,0 0.05,-0.022 0.05,-0.05L0.05,-0.658C0.05,-0.686 0.072,-0.708 0.1,-0.708L0.36,-0.708C0.447,-0.708 0.513,-0.686 0.56,-0.642C0.606,-0.598 0.629,-0.539 0.629,-0.465ZM0.335,-0.4C0.381,-0.4 0.404,-0.422 0.404,-0.465C0.404,-0.508 0.381,-0.53 0.335,-0.53L0.272,-0.53L0.272,-0.4L0.335,-0.4Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(47.4085,0,0,87.7899,155.577,70.8632)">
|
||||
<path d="M0.648,-0.708C0.665,-0.708 0.682,-0.699 0.691,-0.684C0.7,-0.669 0.7,-0.651 0.692,-0.635C0.622,-0.498 0.476,-0.215 0.476,-0.215L0.476,-0.05C0.476,-0.022 0.454,0 0.426,0L0.304,0C0.276,0 0.254,-0.022 0.254,-0.05L0.254,-0.215C0.254,-0.215 0.108,-0.498 0.038,-0.635C0.03,-0.651 0.03,-0.669 0.039,-0.684C0.048,-0.699 0.065,-0.708 0.082,-0.708L0.222,-0.708C0.241,-0.708 0.259,-0.696 0.267,-0.679C0.297,-0.612 0.367,-0.457 0.367,-0.457C0.367,-0.457 0.437,-0.612 0.467,-0.679C0.475,-0.696 0.493,-0.708 0.512,-0.708L0.648,-0.708Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
|
||||
<path d="M0.648,-0.708C0.665,-0.708 0.682,-0.699 0.691,-0.684C0.7,-0.669 0.7,-0.651 0.692,-0.635C0.622,-0.498 0.476,-0.215 0.476,-0.215L0.476,-0.05C0.476,-0.022 0.454,0 0.426,0L0.304,-0C0.276,0 0.254,-0.022 0.254,-0.05L0.254,-0.215C0.254,-0.215 0.108,-0.498 0.038,-0.635C0.03,-0.651 0.03,-0.669 0.039,-0.684C0.048,-0.699 0.065,-0.708 0.082,-0.708L0.222,-0.708C0.241,-0.708 0.259,-0.696 0.267,-0.679C0.297,-0.612 0.367,-0.457 0.367,-0.457C0.367,-0.457 0.437,-0.612 0.467,-0.679C0.475,-0.696 0.493,-0.708 0.512,-0.708L0.648,-0.708Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(47.4085,0,0,87.7899,190.185,70.8632)">
|
||||
<path d="M0.629,-0.465C0.629,-0.42 0.619,-0.38 0.598,-0.344C0.577,-0.307 0.547,-0.278 0.506,-0.257C0.466,-0.236 0.417,-0.225 0.36,-0.225L0.272,-0.225L0.272,-0.05C0.272,-0.037 0.267,-0.024 0.257,-0.015C0.248,-0.005 0.235,0 0.222,0L0.1,0C0.072,0 0.05,-0.022 0.05,-0.05L0.05,-0.658C0.05,-0.686 0.072,-0.708 0.1,-0.708L0.36,-0.708C0.447,-0.708 0.513,-0.686 0.56,-0.642C0.606,-0.598 0.629,-0.539 0.629,-0.465ZM0.335,-0.4C0.381,-0.4 0.404,-0.422 0.404,-0.465C0.404,-0.508 0.381,-0.53 0.335,-0.53L0.272,-0.53L0.272,-0.4L0.335,-0.4Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||
<path d="M0.629,-0.465C0.629,-0.42 0.619,-0.38 0.598,-0.344C0.577,-0.307 0.547,-0.278 0.506,-0.257C0.466,-0.236 0.417,-0.225 0.36,-0.225L0.272,-0.225L0.272,-0.05C0.272,-0.037 0.267,-0.024 0.257,-0.015C0.248,-0.005 0.235,0 0.222,0L0.1,-0C0.072,0 0.05,-0.022 0.05,-0.05L0.05,-0.658C0.05,-0.686 0.072,-0.708 0.1,-0.708L0.36,-0.708C0.447,-0.708 0.513,-0.686 0.56,-0.642C0.606,-0.598 0.629,-0.539 0.629,-0.465ZM0.335,-0.4C0.381,-0.4 0.404,-0.422 0.404,-0.465C0.404,-0.508 0.381,-0.53 0.335,-0.53L0.272,-0.53L0.272,-0.4L0.335,-0.4Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(47.4085,0,0,87.7899,220.953,70.8632)">
|
||||
<path d="M0.272,-0.531L0.272,-0.444L0.492,-0.444L0.492,-0.277L0.272,-0.277L0.272,-0.177L0.472,-0.177C0.485,-0.177 0.498,-0.172 0.507,-0.162C0.517,-0.153 0.522,-0.14 0.522,-0.127L0.522,-0.05C0.522,-0.037 0.517,-0.024 0.507,-0.015C0.498,-0.005 0.485,0 0.472,-0L0.1,0C0.072,0 0.05,-0.022 0.05,-0.05L0.05,-0.658C0.05,-0.686 0.072,-0.708 0.1,-0.708L0.472,-0.708C0.485,-0.708 0.498,-0.703 0.507,-0.693C0.517,-0.684 0.522,-0.671 0.522,-0.658L0.522,-0.581C0.522,-0.568 0.517,-0.555 0.507,-0.546C0.498,-0.536 0.485,-0.531 0.472,-0.531L0.272,-0.531Z" style="fill:url(#_Linear4);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.474085,0,0,0.877899,142.749,-0.167247)">
|
||||
<text x="2.312px" y="79.899px" style="font-family:'Poppins-Light', 'Poppins';font-weight:300;font-size:100px;fill:white;">.club</text>
|
||||
<g transform="matrix(100,0,0,100,2.312,79.8987)">
|
||||
<path d="M0.092,0.005C0.077,0.005 0.065,-0 0.056,-0.01C0.046,-0.02 0.041,-0.032 0.041,-0.047C0.041,-0.062 0.046,-0.074 0.056,-0.084C0.065,-0.093 0.077,-0.098 0.092,-0.098C0.106,-0.098 0.118,-0.093 0.128,-0.084C0.137,-0.074 0.142,-0.062 0.142,-0.047C0.142,-0.032 0.137,-0.02 0.128,-0.01C0.118,-0 0.106,0.005 0.092,0.005Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(100,0,0,100,20.5107,79.8987)">
|
||||
<path d="M0.048,-0.273C0.048,-0.33 0.059,-0.379 0.082,-0.422C0.105,-0.464 0.136,-0.497 0.176,-0.52C0.216,-0.543 0.262,-0.554 0.313,-0.554C0.38,-0.554 0.436,-0.537 0.48,-0.504C0.523,-0.471 0.551,-0.425 0.564,-0.368L0.489,-0.368C0.48,-0.407 0.46,-0.438 0.429,-0.461C0.398,-0.483 0.359,-0.494 0.313,-0.494C0.276,-0.494 0.243,-0.486 0.214,-0.469C0.185,-0.452 0.162,-0.428 0.145,-0.395C0.128,-0.362 0.119,-0.321 0.119,-0.273C0.119,-0.225 0.128,-0.184 0.145,-0.151C0.162,-0.118 0.185,-0.093 0.214,-0.076C0.243,-0.059 0.276,-0.051 0.313,-0.051C0.359,-0.051 0.398,-0.062 0.429,-0.085C0.46,-0.107 0.48,-0.138 0.489,-0.178L0.564,-0.178C0.551,-0.122 0.523,-0.077 0.479,-0.043C0.435,-0.009 0.38,0.008 0.313,0.008C0.262,0.008 0.216,-0.004 0.176,-0.027C0.136,-0.05 0.105,-0.082 0.082,-0.125C0.059,-0.167 0.048,-0.216 0.048,-0.273Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(100,0,0,100,81.8095,79.8987)">
|
||||
<rect x="0.08" y="-0.74" width="0.07" height="0.74" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(100,0,0,100,104.708,79.8987)">
|
||||
<path d="M0.553,-0.546L0.553,-0L0.483,-0L0.483,-0.096C0.467,-0.062 0.442,-0.036 0.409,-0.018C0.376,-0 0.338,0.009 0.297,0.009C0.232,0.009 0.178,-0.011 0.137,-0.051C0.096,-0.092 0.075,-0.15 0.075,-0.227L0.075,-0.546L0.144,-0.546L0.144,-0.235C0.144,-0.176 0.159,-0.13 0.189,-0.099C0.218,-0.068 0.259,-0.052 0.31,-0.052C0.363,-0.052 0.405,-0.069 0.436,-0.102C0.467,-0.135 0.483,-0.184 0.483,-0.249L0.483,-0.546L0.553,-0.546Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(100,0,0,100,168.007,79.8987)">
|
||||
<path d="M0.149,-0.425C0.167,-0.463 0.195,-0.494 0.233,-0.518C0.27,-0.542 0.315,-0.554 0.366,-0.554C0.416,-0.554 0.461,-0.543 0.5,-0.52C0.539,-0.497 0.57,-0.464 0.593,-0.422C0.615,-0.379 0.626,-0.33 0.626,-0.274C0.626,-0.218 0.615,-0.169 0.593,-0.126C0.57,-0.083 0.539,-0.05 0.5,-0.027C0.46,-0.004 0.415,0.008 0.366,0.008C0.314,0.008 0.269,-0.004 0.232,-0.028C0.194,-0.051 0.166,-0.082 0.149,-0.12L0.149,-0L0.08,-0L0.08,-0.74L0.149,-0.74L0.149,-0.425ZM0.555,-0.274C0.555,-0.319 0.546,-0.359 0.529,-0.392C0.511,-0.425 0.487,-0.45 0.456,-0.467C0.425,-0.484 0.391,-0.493 0.352,-0.493C0.315,-0.493 0.281,-0.484 0.25,-0.466C0.219,-0.448 0.194,-0.422 0.176,-0.389C0.158,-0.356 0.149,-0.317 0.149,-0.273C0.149,-0.229 0.158,-0.19 0.176,-0.157C0.194,-0.124 0.219,-0.098 0.25,-0.08C0.281,-0.062 0.315,-0.053 0.352,-0.053C0.391,-0.053 0.425,-0.062 0.456,-0.08C0.487,-0.097 0.511,-0.123 0.529,-0.157C0.546,-0.19 0.555,-0.229 0.555,-0.274Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 8.6 KiB |
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 582 151" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-654,-2135)">
|
||||
<g id="pypeclub_color_white" transform="matrix(0.775972,0,0,0.911492,654.799,1597.77)">
|
||||
<g transform="matrix(1,0,0,1,-987,-2234)">
|
||||
<g id="pypeclub_white" transform="matrix(0.775972,0,0,0.911492,987.586,1696.48)">
|
||||
<rect x="0" y="589.923" width="748.802" height="164.565" style="fill:none;"/>
|
||||
<g transform="matrix(2.7183,0,0,1.24969,-3.76111,624.796)">
|
||||
<g id="PYPE" transform="matrix(1,0,0,1,-105.172,0)">
|
||||
|
|
@ -20,7 +20,21 @@
|
|||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.474085,0,0,0.877899,142.749,-0.167247)">
|
||||
<text x="2.312px" y="79.899px" style="font-family:'Poppins-Light', 'Poppins';font-weight:300;font-size:100px;fill:white;">.club</text>
|
||||
<g transform="matrix(100,0,0,100,2.312,79.8987)">
|
||||
<path d="M0.092,0.005C0.077,0.005 0.065,-0 0.056,-0.01C0.046,-0.02 0.041,-0.032 0.041,-0.047C0.041,-0.062 0.046,-0.074 0.056,-0.084C0.065,-0.093 0.077,-0.098 0.092,-0.098C0.106,-0.098 0.118,-0.093 0.128,-0.084C0.137,-0.074 0.142,-0.062 0.142,-0.047C0.142,-0.032 0.137,-0.02 0.128,-0.01C0.118,-0 0.106,0.005 0.092,0.005Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(100,0,0,100,20.5107,79.8987)">
|
||||
<path d="M0.048,-0.273C0.048,-0.33 0.059,-0.379 0.082,-0.422C0.105,-0.464 0.136,-0.497 0.176,-0.52C0.216,-0.543 0.262,-0.554 0.313,-0.554C0.38,-0.554 0.436,-0.537 0.48,-0.504C0.523,-0.471 0.551,-0.425 0.564,-0.368L0.489,-0.368C0.48,-0.407 0.46,-0.438 0.429,-0.461C0.398,-0.483 0.359,-0.494 0.313,-0.494C0.276,-0.494 0.243,-0.486 0.214,-0.469C0.185,-0.452 0.162,-0.428 0.145,-0.395C0.128,-0.362 0.119,-0.321 0.119,-0.273C0.119,-0.225 0.128,-0.184 0.145,-0.151C0.162,-0.118 0.185,-0.093 0.214,-0.076C0.243,-0.059 0.276,-0.051 0.313,-0.051C0.359,-0.051 0.398,-0.062 0.429,-0.085C0.46,-0.107 0.48,-0.138 0.489,-0.178L0.564,-0.178C0.551,-0.122 0.523,-0.077 0.479,-0.043C0.435,-0.009 0.38,0.008 0.313,0.008C0.262,0.008 0.216,-0.004 0.176,-0.027C0.136,-0.05 0.105,-0.082 0.082,-0.125C0.059,-0.167 0.048,-0.216 0.048,-0.273Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(100,0,0,100,81.8095,79.8987)">
|
||||
<rect x="0.08" y="-0.74" width="0.07" height="0.74" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(100,0,0,100,104.708,79.8987)">
|
||||
<path d="M0.553,-0.546L0.553,-0L0.483,-0L0.483,-0.096C0.467,-0.062 0.442,-0.036 0.409,-0.018C0.376,-0 0.338,0.009 0.297,0.009C0.232,0.009 0.178,-0.011 0.137,-0.051C0.096,-0.092 0.075,-0.15 0.075,-0.227L0.075,-0.546L0.144,-0.546L0.144,-0.235C0.144,-0.176 0.159,-0.13 0.189,-0.099C0.218,-0.068 0.259,-0.052 0.31,-0.052C0.363,-0.052 0.405,-0.069 0.436,-0.102C0.467,-0.135 0.483,-0.184 0.483,-0.249L0.483,-0.546L0.553,-0.546Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(100,0,0,100,168.007,79.8987)">
|
||||
<path d="M0.149,-0.425C0.167,-0.463 0.195,-0.494 0.233,-0.518C0.27,-0.542 0.315,-0.554 0.366,-0.554C0.416,-0.554 0.461,-0.543 0.5,-0.52C0.539,-0.497 0.57,-0.464 0.593,-0.422C0.615,-0.379 0.626,-0.33 0.626,-0.274C0.626,-0.218 0.615,-0.169 0.593,-0.126C0.57,-0.083 0.539,-0.05 0.5,-0.027C0.46,-0.004 0.415,0.008 0.366,0.008C0.314,0.008 0.269,-0.004 0.232,-0.028C0.194,-0.051 0.166,-0.082 0.149,-0.12L0.149,-0L0.08,-0L0.08,-0.74L0.149,-0.74L0.149,-0.425ZM0.555,-0.274C0.555,-0.319 0.546,-0.359 0.529,-0.392C0.511,-0.425 0.487,-0.45 0.456,-0.467C0.425,-0.484 0.391,-0.493 0.352,-0.493C0.315,-0.493 0.281,-0.484 0.25,-0.466C0.219,-0.448 0.194,-0.422 0.176,-0.389C0.158,-0.356 0.149,-0.317 0.149,-0.273C0.149,-0.229 0.158,-0.19 0.176,-0.157C0.194,-0.124 0.219,-0.098 0.25,-0.08C0.281,-0.062 0.315,-0.053 0.352,-0.053C0.391,-0.053 0.425,-0.062 0.456,-0.08C0.487,-0.097 0.511,-0.123 0.529,-0.157C0.546,-0.19 0.555,-0.229 0.555,-0.274Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 7 KiB |