Merge branch 'develop' into feature/houdini-cb-update

This commit is contained in:
Ondřej Samohel 2021-08-17 16:32:29 +02:00
commit fdd6ab3a95
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
89 changed files with 4329 additions and 1384 deletions

View file

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

View file

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

View file

@ -47,7 +47,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
"subset": subset,
"label": scene_file,
"family": family,
"families": [family, "ftrack"],
"families": [family],
"representations": list()
})

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
from .module import (
PythonInterpreterAction
)
__all__ = (
"PythonInterpreterAction",
)

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

View file

@ -0,0 +1,8 @@
from .widgets import (
PythonInterpreterWidget
)
__all__ = (
"PythonInterpreterWidget",
)

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

View 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

View file

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

View file

@ -304,7 +304,8 @@
"aftereffects"
],
"families": [
"render"
"render",
"workfile"
],
"tasks": [],
"add_ftrack_family": true,

View file

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

View file

@ -1,6 +1,5 @@
{
"project_setup": {
"dev_mode": true,
"install_unreal_python_engine": false
"dev_mode": true
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View 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 arent 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.

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.3.0-nightly.9"
__version__ = "3.3.0-nightly.10"

View file

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

View file

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

View file

@ -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.
![Maya > Deadline Settings](assets/maya-admin_submit_maya_job_to_deadline.png)
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**.
![Custom menu definition](assets/maya-admin_scriptsmenu.png)
@ -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**
![Dirmap settings](assets/maya-admin_dirmap_settings.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

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

Before After
Before After

View file

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

Before After
Before After

View file

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

Before After
Before After