mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into feature/dynamic_modules
This commit is contained in:
commit
cbb7cf8c83
54 changed files with 3741 additions and 190 deletions
37
CHANGELOG.md
37
CHANGELOG.md
|
|
@ -1,11 +1,20 @@
|
|||
# Changelog
|
||||
|
||||
## [3.3.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.3.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Settings: global validators with options [\#1892](https://github.com/pypeclub/OpenPype/pull/1892)
|
||||
- Settings: Conditional dict enum positioning [\#1891](https://github.com/pypeclub/OpenPype/pull/1891)
|
||||
- Expose stop timer through rest api. [\#1886](https://github.com/pypeclub/OpenPype/pull/1886)
|
||||
- TVPaint: Increment workfile [\#1885](https://github.com/pypeclub/OpenPype/pull/1885)
|
||||
- Allow Multiple Notes to run on tasks. [\#1882](https://github.com/pypeclub/OpenPype/pull/1882)
|
||||
- Prepare for pyside2 [\#1869](https://github.com/pypeclub/OpenPype/pull/1869)
|
||||
- Filter hosts in settings host-enum [\#1868](https://github.com/pypeclub/OpenPype/pull/1868)
|
||||
- Local actions with process identifier [\#1867](https://github.com/pypeclub/OpenPype/pull/1867)
|
||||
- Workfile tool start at host launch support [\#1865](https://github.com/pypeclub/OpenPype/pull/1865)
|
||||
- Anatomy schema validation [\#1864](https://github.com/pypeclub/OpenPype/pull/1864)
|
||||
- Ftrack prepare project structure [\#1861](https://github.com/pypeclub/OpenPype/pull/1861)
|
||||
- Independent general environments [\#1853](https://github.com/pypeclub/OpenPype/pull/1853)
|
||||
|
|
@ -17,9 +26,15 @@
|
|||
- Update poetry lock [\#1823](https://github.com/pypeclub/OpenPype/pull/1823)
|
||||
- Settings: settings for plugins [\#1819](https://github.com/pypeclub/OpenPype/pull/1819)
|
||||
- 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**
|
||||
|
||||
- Bug: fixed python detection [\#1893](https://github.com/pypeclub/OpenPype/pull/1893)
|
||||
- global: integrate name missing default template [\#1890](https://github.com/pypeclub/OpenPype/pull/1890)
|
||||
- publisher: editorial plugins fixes [\#1889](https://github.com/pypeclub/OpenPype/pull/1889)
|
||||
- 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)
|
||||
|
|
@ -28,15 +43,13 @@
|
|||
- 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)
|
||||
- nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803)
|
||||
- Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Ftrack push attributes action adds traceback to job [\#1842](https://github.com/pypeclub/OpenPype/pull/1842)
|
||||
- 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)
|
||||
- Build: don't add Poetry to `PATH` [\#1808](https://github.com/pypeclub/OpenPype/pull/1808)
|
||||
|
||||
## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13)
|
||||
|
||||
|
|
@ -56,15 +69,13 @@
|
|||
- Deadline: Nuke submission additional attributes [\#1756](https://github.com/pypeclub/OpenPype/pull/1756)
|
||||
- Settings schema without prefill [\#1753](https://github.com/pypeclub/OpenPype/pull/1753)
|
||||
- Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739)
|
||||
- Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736)
|
||||
- PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803)
|
||||
- Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801)
|
||||
- Invitee email can be None which break the Ftrack commit. [\#1788](https://github.com/pypeclub/OpenPype/pull/1788)
|
||||
- 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)
|
||||
|
|
@ -77,14 +88,12 @@
|
|||
- Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742)
|
||||
- Nuke: fixing render creator for no selection format failing [\#1741](https://github.com/pypeclub/OpenPype/pull/1741)
|
||||
- StandalonePublisher: failing collector for editorial [\#1738](https://github.com/pypeclub/OpenPype/pull/1738)
|
||||
- Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737)
|
||||
- TVPaint white background on thumbnail [\#1735](https://github.com/pypeclub/OpenPype/pull/1735)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- 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)
|
||||
- Expose write attributes to config [\#1770](https://github.com/pypeclub/OpenPype/pull/1770)
|
||||
- TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755)
|
||||
|
||||
## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24)
|
||||
|
|
@ -100,18 +109,10 @@
|
|||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727)
|
||||
|
||||
## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Maya: Extract review hotfix - 2.x backport [\#1713](https://github.com/pypeclub/OpenPype/pull/1713)
|
||||
|
||||
## [3.1.0](https://github.com/pypeclub/OpenPype/tree/3.1.0) (2021-06-15)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.1.0-nightly.4...3.1.0)
|
||||
|
|
|
|||
|
|
@ -102,6 +102,11 @@ def install():
|
|||
.get(platform_name)
|
||||
) or []
|
||||
for path in project_plugins:
|
||||
try:
|
||||
path = str(path.format(**os.environ))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not path or not os.path.exists(path):
|
||||
continue
|
||||
|
||||
|
|
|
|||
53
openpype/hosts/maya/api/commands.py
Normal file
53
openpype/hosts/maya/api/commands.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""OpenPype script commands to be used directly in Maya."""
|
||||
|
||||
|
||||
class ToolWindows:
|
||||
|
||||
_windows = {}
|
||||
|
||||
@classmethod
|
||||
def get_window(cls, tool):
|
||||
"""Get widget for specific tool.
|
||||
|
||||
Args:
|
||||
tool (str): Name of the tool.
|
||||
|
||||
Returns:
|
||||
Stored widget.
|
||||
|
||||
"""
|
||||
try:
|
||||
return cls._windows[tool]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def set_window(cls, tool, window):
|
||||
"""Set widget for the tool.
|
||||
|
||||
Args:
|
||||
tool (str): Name of the tool.
|
||||
window (QtWidgets.QWidget): Widget
|
||||
|
||||
"""
|
||||
cls._windows[tool] = window
|
||||
|
||||
|
||||
def edit_shader_definitions():
|
||||
from avalon.tools import lib
|
||||
from Qt import QtWidgets
|
||||
from openpype.hosts.maya.api.shader_definition_editor import (
|
||||
ShaderDefinitionsEditor
|
||||
)
|
||||
|
||||
top_level_widgets = QtWidgets.QApplication.topLevelWidgets()
|
||||
main_window = next(widget for widget in top_level_widgets
|
||||
if widget.objectName() == "MayaWindow")
|
||||
|
||||
with lib.application():
|
||||
window = ToolWindows.get_window("shader_definition_editor")
|
||||
if not window:
|
||||
window = ShaderDefinitionsEditor(parent=main_window)
|
||||
ToolWindows.set_window("shader_definition_editor", window)
|
||||
window.show()
|
||||
|
|
@ -6,9 +6,9 @@ from avalon.vendor.Qt import QtWidgets, QtGui
|
|||
from avalon.maya import pipeline
|
||||
from openpype.api import BuildWorkfile
|
||||
import maya.cmds as cmds
|
||||
from openpype.settings import get_project_settings
|
||||
|
||||
self = sys.modules[__name__]
|
||||
self._menu = os.environ.get("AVALON_LABEL")
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -17,8 +17,11 @@ log = logging.getLogger(__name__)
|
|||
def _get_menu(menu_name=None):
|
||||
"""Return the menu instance if it currently exists in Maya"""
|
||||
|
||||
project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
|
||||
_menu = project_settings["maya"]["scriptsmenu"]["name"]
|
||||
|
||||
if menu_name is None:
|
||||
menu_name = self._menu
|
||||
menu_name = _menu
|
||||
widgets = dict((
|
||||
w.objectName(), w) for w in QtWidgets.QApplication.allWidgets())
|
||||
menu = widgets.get(menu_name)
|
||||
|
|
@ -55,35 +58,7 @@ def deferred():
|
|||
parent=pipeline._parent
|
||||
)
|
||||
|
||||
# Find the pipeline menu
|
||||
top_menu = _get_menu(pipeline._menu)
|
||||
|
||||
# Try to find workfile tool action in the menu
|
||||
workfile_action = None
|
||||
for action in top_menu.actions():
|
||||
if action.text() == "Work Files":
|
||||
workfile_action = action
|
||||
break
|
||||
|
||||
# Add at the top of menu if "Work Files" action was not found
|
||||
after_action = ""
|
||||
if workfile_action:
|
||||
# Use action's object name for `insertAfter` argument
|
||||
after_action = workfile_action.objectName()
|
||||
|
||||
# Insert action to menu
|
||||
cmds.menuItem(
|
||||
"Work Files",
|
||||
parent=pipeline._menu,
|
||||
command=launch_workfiles_app,
|
||||
insertAfter=after_action
|
||||
)
|
||||
|
||||
# Remove replaced action
|
||||
if workfile_action:
|
||||
top_menu.removeAction(workfile_action)
|
||||
|
||||
log.info("Attempting to install scripts menu..")
|
||||
log.info("Attempting to install scripts menu ...")
|
||||
|
||||
add_build_workfiles_item()
|
||||
add_look_assigner_item()
|
||||
|
|
@ -100,13 +75,18 @@ def deferred():
|
|||
return
|
||||
|
||||
# load configuration of custom menu
|
||||
config_path = os.path.join(os.path.dirname(__file__), "menu.json")
|
||||
config = scriptsmenu.load_configuration(config_path)
|
||||
project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
|
||||
config = project_settings["maya"]["scriptsmenu"]["definition"]
|
||||
_menu = project_settings["maya"]["scriptsmenu"]["name"]
|
||||
|
||||
if not config:
|
||||
log.warning("Skipping studio menu, no definition found.")
|
||||
return
|
||||
|
||||
# run the launcher for Maya menu
|
||||
studio_menu = launchformaya.main(
|
||||
title=self._menu.title(),
|
||||
objectName=self._menu
|
||||
title=_menu.title(),
|
||||
objectName=_menu.title().lower().replace(" ", "_")
|
||||
)
|
||||
|
||||
# apply configuration
|
||||
|
|
@ -116,7 +96,7 @@ def deferred():
|
|||
def uninstall():
|
||||
menu = _get_menu()
|
||||
if menu:
|
||||
log.info("Attempting to uninstall..")
|
||||
log.info("Attempting to uninstall ...")
|
||||
|
||||
try:
|
||||
menu.deleteLater()
|
||||
|
|
@ -136,9 +116,8 @@ def install():
|
|||
|
||||
|
||||
def popup():
|
||||
"""Pop-up the existing menu near the mouse cursor"""
|
||||
"""Pop-up the existing menu near the mouse cursor."""
|
||||
menu = _get_menu()
|
||||
|
||||
cursor = QtGui.QCursor()
|
||||
point = cursor.pos()
|
||||
menu.exec_(point)
|
||||
|
|
|
|||
176
openpype/hosts/maya/api/shader_definition_editor.py
Normal file
176
openpype/hosts/maya/api/shader_definition_editor.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Editor for shader definitions.
|
||||
|
||||
Shader names are stored as simple text file over GridFS in mongodb.
|
||||
|
||||
"""
|
||||
import os
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
from openpype.lib.mongo import OpenPypeMongoConnection
|
||||
from openpype import resources
|
||||
import gridfs
|
||||
|
||||
|
||||
DEFINITION_FILENAME = "{}/maya/shader_definition.txt".format(
|
||||
os.getenv("AVALON_PROJECT"))
|
||||
|
||||
|
||||
class ShaderDefinitionsEditor(QtWidgets.QWidget):
|
||||
"""Widget serving as simple editor for shader name definitions."""
|
||||
|
||||
# name of the file used to store definitions
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ShaderDefinitionsEditor, self).__init__(parent)
|
||||
self._mongo = OpenPypeMongoConnection.get_mongo_client()
|
||||
self._gridfs = gridfs.GridFS(
|
||||
self._mongo[os.getenv("OPENPYPE_DATABASE_NAME")])
|
||||
self._editor = None
|
||||
|
||||
self._original_content = self._read_definition_file()
|
||||
|
||||
self.setObjectName("shaderDefinitionEditor")
|
||||
self.setWindowTitle("OpenPype shader name definition editor")
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setParent(parent)
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
self.resize(750, 500)
|
||||
|
||||
self._setup_ui()
|
||||
self._reload()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup UI of Widget."""
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
label = QtWidgets.QLabel()
|
||||
label.setText("Put shader names here - one name per line:")
|
||||
layout.addWidget(label)
|
||||
self._editor = QtWidgets.QPlainTextEdit()
|
||||
self._editor.setStyleSheet("border: none;")
|
||||
layout.addWidget(self._editor)
|
||||
|
||||
btn_layout = QtWidgets.QHBoxLayout()
|
||||
save_btn = QtWidgets.QPushButton("Save")
|
||||
save_btn.clicked.connect(self._save)
|
||||
|
||||
reload_btn = QtWidgets.QPushButton("Reload")
|
||||
reload_btn.clicked.connect(self._reload)
|
||||
|
||||
exit_btn = QtWidgets.QPushButton("Exit")
|
||||
exit_btn.clicked.connect(self._close)
|
||||
|
||||
btn_layout.addWidget(reload_btn)
|
||||
btn_layout.addWidget(save_btn)
|
||||
btn_layout.addWidget(exit_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def _read_definition_file(self, file=None):
|
||||
"""Read definition file from database.
|
||||
|
||||
Args:
|
||||
file (gridfs.grid_file.GridOut, Optional): File to read. If not
|
||||
set, new query will be issued to find it.
|
||||
|
||||
Returns:
|
||||
str: Content of the file or empty string if file doesn't exist.
|
||||
|
||||
"""
|
||||
content = ""
|
||||
if not file:
|
||||
file = self._gridfs.find_one(
|
||||
{"filename": DEFINITION_FILENAME})
|
||||
if not file:
|
||||
print(">>> [SNDE]: nothing in database yet")
|
||||
return content
|
||||
content = file.read()
|
||||
file.close()
|
||||
return content
|
||||
|
||||
def _write_definition_file(self, content, force=False):
|
||||
"""Write content as definition to file in database.
|
||||
|
||||
Before file is writen, check is made if its content has not
|
||||
changed. If is changed, warning is issued to user if he wants
|
||||
it to overwrite. Note: GridFs doesn't allow changing file content.
|
||||
You need to delete existing file and create new one.
|
||||
|
||||
Args:
|
||||
content (str): Content to write.
|
||||
|
||||
Raises:
|
||||
ContentException: If file is changed in database while
|
||||
editor is running.
|
||||
"""
|
||||
file = self._gridfs.find_one(
|
||||
{"filename": DEFINITION_FILENAME})
|
||||
if file:
|
||||
content_check = self._read_definition_file(file)
|
||||
if content == content_check:
|
||||
print(">>> [SNDE]: content not changed")
|
||||
return
|
||||
if self._original_content != content_check:
|
||||
if not force:
|
||||
raise ContentException("Content changed")
|
||||
print(">>> [SNDE]: overwriting data")
|
||||
file.close()
|
||||
self._gridfs.delete(file._id)
|
||||
|
||||
file = self._gridfs.new_file(
|
||||
filename=DEFINITION_FILENAME,
|
||||
content_type='text/plain',
|
||||
encoding='utf-8')
|
||||
file.write(content)
|
||||
file.close()
|
||||
QtCore.QTimer.singleShot(200, self._reset_style)
|
||||
self._editor.setStyleSheet("border: 1px solid #33AF65;")
|
||||
self._original_content = content
|
||||
|
||||
def _reset_style(self):
|
||||
"""Reset editor style back.
|
||||
|
||||
Used to visually indicate save.
|
||||
|
||||
"""
|
||||
self._editor.setStyleSheet("border: none;")
|
||||
|
||||
def _close(self):
|
||||
self.hide()
|
||||
|
||||
def closeEvent(self, event):
|
||||
event.ignore()
|
||||
self.hide()
|
||||
|
||||
def _reload(self):
|
||||
print(">>> [SNDE]: reloading")
|
||||
self._set_content(self._read_definition_file())
|
||||
|
||||
def _save(self):
|
||||
try:
|
||||
self._write_definition_file(content=self._editor.toPlainText())
|
||||
except ContentException:
|
||||
# content has changed meanwhile
|
||||
print(">>> [SNDE]: content has changed")
|
||||
self._show_overwrite_warning()
|
||||
|
||||
def _set_content(self, content):
|
||||
self._editor.setPlainText(content)
|
||||
|
||||
def _show_overwrite_warning(self):
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self,
|
||||
"Warning",
|
||||
("Content you are editing was changed meanwhile in database.\n"
|
||||
"Please, reload and solve the conflict."),
|
||||
QtWidgets.QMessageBox.OK)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.OK:
|
||||
# do nothing
|
||||
pass
|
||||
|
||||
|
||||
class ContentException(Exception):
|
||||
"""This is risen during save if file is changed in database."""
|
||||
pass
|
||||
|
|
@ -167,6 +167,8 @@ def get_file_node_path(node):
|
|||
|
||||
if cmds.nodeType(node) == 'aiImage':
|
||||
return cmds.getAttr('{0}.filename'.format(node))
|
||||
if cmds.nodeType(node) == 'RedshiftNormalMap':
|
||||
return cmds.getAttr('{}.tex0'.format(node))
|
||||
|
||||
# otherwise use fileTextureName
|
||||
return cmds.getAttr('{0}.fileTextureName'.format(node))
|
||||
|
|
@ -357,6 +359,7 @@ class CollectLook(pyblish.api.InstancePlugin):
|
|||
|
||||
files = cmds.ls(history, type="file", long=True)
|
||||
files.extend(cmds.ls(history, type="aiImage", long=True))
|
||||
files.extend(cmds.ls(history, type="RedshiftNormalMap", long=True))
|
||||
|
||||
self.log.info("Collected file nodes:\n{}".format(files))
|
||||
# Collect textures if any file nodes are found
|
||||
|
|
@ -487,7 +490,7 @@ class CollectLook(pyblish.api.InstancePlugin):
|
|||
"""
|
||||
|
||||
self.log.debug("processing: {}".format(node))
|
||||
if cmds.nodeType(node) not in ["file", "aiImage"]:
|
||||
if cmds.nodeType(node) not in ["file", "aiImage", "RedshiftNormalMap"]:
|
||||
self.log.error(
|
||||
"Unsupported file node: {}".format(cmds.nodeType(node)))
|
||||
raise AssertionError("Unsupported file node")
|
||||
|
|
@ -500,11 +503,19 @@ class CollectLook(pyblish.api.InstancePlugin):
|
|||
self.log.debug("aiImage node")
|
||||
attribute = "{}.filename".format(node)
|
||||
computed_attribute = attribute
|
||||
elif cmds.nodeType(node) == 'RedshiftNormalMap':
|
||||
self.log.debug("RedshiftNormalMap node")
|
||||
attribute = "{}.tex0".format(node)
|
||||
computed_attribute = attribute
|
||||
|
||||
source = cmds.getAttr(attribute)
|
||||
self.log.info(" - file source: {}".format(source))
|
||||
color_space_attr = "{}.colorSpace".format(node)
|
||||
color_space = cmds.getAttr(color_space_attr)
|
||||
try:
|
||||
color_space = cmds.getAttr(color_space_attr)
|
||||
except ValueError:
|
||||
# node doesn't have colorspace attribute
|
||||
color_space = "raw"
|
||||
# Compare with the computed file path, e.g. the one with the <UDIM>
|
||||
# pattern in it, to generate some logging information about this
|
||||
# difference
|
||||
|
|
|
|||
|
|
@ -233,11 +233,14 @@ class ExtractLook(openpype.api.Extractor):
|
|||
for filepath in files_metadata:
|
||||
|
||||
linearize = False
|
||||
if do_maketx and files_metadata[filepath]["color_space"] == "sRGB": # noqa: E501
|
||||
if do_maketx and files_metadata[filepath]["color_space"].lower() == "srgb": # noqa: E501
|
||||
linearize = True
|
||||
# set its file node to 'raw' as tx will be linearized
|
||||
files_metadata[filepath]["color_space"] = "raw"
|
||||
|
||||
if do_maketx:
|
||||
color_space = "raw"
|
||||
|
||||
source, mode, texture_hash = self._process_texture(
|
||||
filepath,
|
||||
do_maketx,
|
||||
|
|
@ -280,14 +283,19 @@ class ExtractLook(openpype.api.Extractor):
|
|||
# This will also trigger in the same order at end of context to
|
||||
# ensure after context it's still the original value.
|
||||
color_space_attr = resource["node"] + ".colorSpace"
|
||||
color_space = cmds.getAttr(color_space_attr)
|
||||
if files_metadata[source]["color_space"] == "raw":
|
||||
# set color space to raw if we linearized it
|
||||
color_space = "Raw"
|
||||
# Remap file node filename to destination
|
||||
try:
|
||||
color_space = cmds.getAttr(color_space_attr)
|
||||
except ValueError:
|
||||
# node doesn't have color space attribute
|
||||
color_space = "raw"
|
||||
else:
|
||||
if files_metadata[source]["color_space"] == "raw":
|
||||
# set color space to raw if we linearized it
|
||||
color_space = "raw"
|
||||
# Remap file node filename to destination
|
||||
remap[color_space_attr] = color_space
|
||||
attr = resource["attribute"]
|
||||
remap[attr] = destinations[source]
|
||||
remap[color_space_attr] = color_space
|
||||
|
||||
self.log.info("Finished remapping destinations ...")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validate model nodes names."""
|
||||
from maya import cmds
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
import avalon.api
|
||||
import openpype.hosts.maya.api.action
|
||||
from openpype.hosts.maya.api.shader_definition_editor import (
|
||||
DEFINITION_FILENAME)
|
||||
from openpype.lib.mongo import OpenPypeMongoConnection
|
||||
import gridfs
|
||||
import re
|
||||
import os
|
||||
|
||||
|
||||
class ValidateModelName(pyblish.api.InstancePlugin):
|
||||
|
|
@ -19,18 +27,18 @@ class ValidateModelName(pyblish.api.InstancePlugin):
|
|||
families = ["model"]
|
||||
label = "Model Name"
|
||||
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
|
||||
# path to shader names definitions
|
||||
# TODO: move it to preset file
|
||||
material_file = None
|
||||
regex = '(.*)_(\\d)*_(.*)_(GEO)'
|
||||
database_file = DEFINITION_FILENAME
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
"""Get invalid nodes."""
|
||||
use_db = cls.database
|
||||
|
||||
# find out if supplied transform is group or not
|
||||
def is_group(groupName):
|
||||
def is_group(group_name):
|
||||
"""Find out if supplied transform is group or not."""
|
||||
try:
|
||||
children = cmds.listRelatives(groupName, children=True)
|
||||
children = cmds.listRelatives(group_name, children=True)
|
||||
for child in children:
|
||||
if not cmds.ls(child, transforms=True):
|
||||
return False
|
||||
|
|
@ -44,29 +52,74 @@ class ValidateModelName(pyblish.api.InstancePlugin):
|
|||
cls.log.error("Instance has no nodes!")
|
||||
return True
|
||||
pass
|
||||
|
||||
# validate top level group name
|
||||
assemblies = cmds.ls(content_instance, assemblies=True, long=True)
|
||||
if len(assemblies) != 1:
|
||||
cls.log.error("Must have exactly one top group")
|
||||
return assemblies or True
|
||||
top_group = assemblies[0]
|
||||
regex = cls.top_level_regex
|
||||
r = re.compile(regex)
|
||||
m = r.match(top_group)
|
||||
if m is None:
|
||||
cls.log.error("invalid name on: {}".format(top_group))
|
||||
cls.log.error("name doesn't match regex {}".format(regex))
|
||||
invalid.append(top_group)
|
||||
else:
|
||||
if "asset" in r.groupindex:
|
||||
if m.group("asset") != avalon.api.Session["AVALON_ASSET"]:
|
||||
cls.log.error("Invalid asset name in top level group.")
|
||||
return top_group
|
||||
if "subset" in r.groupindex:
|
||||
if m.group("subset") != instance.data.get("subset"):
|
||||
cls.log.error("Invalid subset name in top level group.")
|
||||
return top_group
|
||||
if "project" in r.groupindex:
|
||||
if m.group("project") != avalon.api.Session["AVALON_PROJECT"]:
|
||||
cls.log.error("Invalid project name in top level group.")
|
||||
return top_group
|
||||
|
||||
descendants = cmds.listRelatives(content_instance,
|
||||
allDescendents=True,
|
||||
fullPath=True) or []
|
||||
|
||||
descendants = cmds.ls(descendants, noIntermediate=True, long=True)
|
||||
trns = cmds.ls(descendants, long=False, type=('transform'))
|
||||
trns = cmds.ls(descendants, long=False, type='transform')
|
||||
|
||||
# filter out groups
|
||||
filter = [node for node in trns if not is_group(node)]
|
||||
filtered = [node for node in trns if not is_group(node)]
|
||||
|
||||
# load shader list file as utf-8
|
||||
if cls.material_file:
|
||||
shader_file = open(cls.material_file, "r")
|
||||
shaders = shader_file.readlines()
|
||||
shaders = []
|
||||
if not use_db:
|
||||
if cls.material_file:
|
||||
if os.path.isfile(cls.material_file):
|
||||
shader_file = open(cls.material_file, "r")
|
||||
shaders = shader_file.readlines()
|
||||
shader_file.close()
|
||||
else:
|
||||
cls.log.error("Missing shader name definition file.")
|
||||
return True
|
||||
else:
|
||||
client = OpenPypeMongoConnection.get_mongo_client()
|
||||
fs = gridfs.GridFS(client[os.getenv("OPENPYPE_DATABASE_NAME")])
|
||||
shader_file = fs.find_one({"filename": cls.database_file})
|
||||
if not shader_file:
|
||||
cls.log.error("Missing shader name definition in database.")
|
||||
return True
|
||||
shaders = shader_file.read().splitlines()
|
||||
shader_file.close()
|
||||
|
||||
# strip line endings from list
|
||||
shaders = map(lambda s: s.rstrip(), shaders)
|
||||
|
||||
# compile regex for testing names
|
||||
r = re.compile(cls.regex)
|
||||
regex = cls.regex
|
||||
r = re.compile(regex)
|
||||
|
||||
for obj in filter:
|
||||
for obj in filtered:
|
||||
cls.log.info("testing: {}".format(obj))
|
||||
m = r.match(obj)
|
||||
if m is None:
|
||||
cls.log.error("invalid name on: {}".format(obj))
|
||||
|
|
@ -74,7 +127,7 @@ class ValidateModelName(pyblish.api.InstancePlugin):
|
|||
else:
|
||||
# if we have shader files and shader named group is in
|
||||
# regex, test this group against names in shader file
|
||||
if 'shader' in r.groupindex and shaders:
|
||||
if "shader" in r.groupindex and shaders:
|
||||
try:
|
||||
if not m.group('shader') in shaders:
|
||||
cls.log.error(
|
||||
|
|
@ -90,8 +143,8 @@ class ValidateModelName(pyblish.api.InstancePlugin):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
"""Plugin entry point."""
|
||||
invalid = self.get_invalid(instance)
|
||||
|
||||
if invalid:
|
||||
raise RuntimeError("Model naming is invalid. See log.")
|
||||
raise RuntimeError("Model naming is invalid. See the log.")
|
||||
|
|
|
|||
|
|
@ -1660,9 +1660,13 @@ def find_free_space_to_paste_nodes(
|
|||
def launch_workfiles_app():
|
||||
'''Function letting start workfiles after start of host
|
||||
'''
|
||||
# get state from settings
|
||||
open_at_start = get_current_project_settings()["nuke"].get(
|
||||
"general", {}).get("open_workfile_at_start")
|
||||
from openpype.lib import (
|
||||
env_value_to_bool
|
||||
)
|
||||
# get all imortant settings
|
||||
open_at_start = env_value_to_bool(
|
||||
env_key="OPENPYPE_WORKFILE_TOOL_ON_START",
|
||||
default=None)
|
||||
|
||||
# return if none is defined
|
||||
if not open_at_start:
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ class CollectInstances(pyblish.api.InstancePlugin):
|
|||
})
|
||||
for subset, properities in self.subsets.items():
|
||||
version = properities.get("version")
|
||||
if version and version == 0:
|
||||
if version == 0:
|
||||
properities.pop("version")
|
||||
|
||||
# adding Review-able instance
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin):
|
|||
|
||||
# return if any
|
||||
if entity_type:
|
||||
return {"entityType": entity_type, "entityName": value}
|
||||
return {"entity_type": entity_type, "entity_name": value}
|
||||
|
||||
def rename_with_hierarchy(self, instance):
|
||||
search_text = ""
|
||||
|
|
@ -76,8 +76,8 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin):
|
|||
# add current selection context hierarchy from standalonepublisher
|
||||
for entity in reversed(visual_hierarchy):
|
||||
parents.append({
|
||||
"entityType": entity["data"]["entityType"],
|
||||
"entityName": entity["name"]
|
||||
"entity_type": entity["data"]["entityType"],
|
||||
"entity_name": entity["name"]
|
||||
})
|
||||
|
||||
if self.shot_add_hierarchy:
|
||||
|
|
@ -98,7 +98,7 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin):
|
|||
|
||||
# in case SP context is set to the same folder
|
||||
if (_index == 0) and ("folder" in parent_key) \
|
||||
and (parents[-1]["entityName"] == parent_filled):
|
||||
and (parents[-1]["entity_name"] == parent_filled):
|
||||
self.log.debug(f" skiping : {parent_filled}")
|
||||
continue
|
||||
|
||||
|
|
@ -280,9 +280,9 @@ class CollectHierarchyContext(pyblish.api.ContextPlugin):
|
|||
|
||||
for parent in reversed(parents):
|
||||
next_dict = {}
|
||||
parent_name = parent["entityName"]
|
||||
parent_name = parent["entity_name"]
|
||||
next_dict[parent_name] = {}
|
||||
next_dict[parent_name]["entity_type"] = parent["entityType"]
|
||||
next_dict[parent_name]["entity_type"] = parent["entity_type"]
|
||||
next_dict[parent_name]["childs"] = actual
|
||||
actual = next_dict
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class ExtractTrimVideoAudio(openpype.api.Extractor):
|
|||
]
|
||||
|
||||
args = [
|
||||
ffmpeg_path,
|
||||
f"\"{ffmpeg_path}\"",
|
||||
"-ss", str(start / fps),
|
||||
"-i", f"\"{video_file_path}\"",
|
||||
"-t", str(dur / fps)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import pyblish.api
|
||||
|
||||
from avalon.tvpaint import workio
|
||||
from openpype.api import version_up
|
||||
|
||||
|
||||
class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
|
||||
"""Increment current workfile version."""
|
||||
|
||||
order = pyblish.api.IntegratorOrder + 1
|
||||
label = "Increment Workfile Version"
|
||||
optional = True
|
||||
hosts = ["tvpaint"]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
assert all(result["success"] for result in context.data["results"]), (
|
||||
"Publishing not succesfull so version is not increased.")
|
||||
|
||||
path = context.data["currentFile"]
|
||||
workio.save_file(version_up(path))
|
||||
self.log.info('Incrementing workfile version')
|
||||
|
|
@ -1302,10 +1302,18 @@ def _prepare_last_workfile(data, workdir):
|
|||
)
|
||||
data["start_last_workfile"] = start_last_workfile
|
||||
|
||||
workfile_startup = should_workfile_tool_start(
|
||||
project_name, app.host_name, task_name
|
||||
)
|
||||
data["workfile_startup"] = workfile_startup
|
||||
|
||||
# Store boolean as "0"(False) or "1"(True)
|
||||
data["env"]["AVALON_OPEN_LAST_WORKFILE"] = (
|
||||
str(int(bool(start_last_workfile)))
|
||||
)
|
||||
data["env"]["OPENPYPE_WORKFILE_TOOL_ON_START"] = (
|
||||
str(int(bool(workfile_startup)))
|
||||
)
|
||||
|
||||
_sub_msg = "" if start_last_workfile else " not"
|
||||
log.debug(
|
||||
|
|
@ -1344,40 +1352,9 @@ def _prepare_last_workfile(data, workdir):
|
|||
data["last_workfile_path"] = last_workfile_path
|
||||
|
||||
|
||||
def should_start_last_workfile(
|
||||
project_name, host_name, task_name, default_output=False
|
||||
def get_option_from_settings(
|
||||
startup_presets, host_name, task_name, default_output
|
||||
):
|
||||
"""Define if host should start last version workfile if possible.
|
||||
|
||||
Default output is `False`. Can be overriden with environment variable
|
||||
`AVALON_OPEN_LAST_WORKFILE`, valid values without case sensitivity are
|
||||
`"0", "1", "true", "false", "yes", "no"`.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project.
|
||||
host_name (str): Name of host which is launched. In avalon's
|
||||
application context it's value stored in app definition under
|
||||
key `"application_dir"`. Is not case sensitive.
|
||||
task_name (str): Name of task which is used for launching the host.
|
||||
Task name is not case sensitive.
|
||||
|
||||
Returns:
|
||||
bool: True if host should start workfile.
|
||||
|
||||
"""
|
||||
|
||||
project_settings = get_project_settings(project_name)
|
||||
startup_presets = (
|
||||
project_settings
|
||||
["global"]
|
||||
["tools"]
|
||||
["Workfiles"]
|
||||
["last_workfile_on_startup"]
|
||||
)
|
||||
|
||||
if not startup_presets:
|
||||
return default_output
|
||||
|
||||
host_name_lowered = host_name.lower()
|
||||
task_name_lowered = task_name.lower()
|
||||
|
||||
|
|
@ -1421,6 +1398,82 @@ def should_start_last_workfile(
|
|||
return default_output
|
||||
|
||||
|
||||
def should_start_last_workfile(
|
||||
project_name, host_name, task_name, default_output=False
|
||||
):
|
||||
"""Define if host should start last version workfile if possible.
|
||||
|
||||
Default output is `False`. Can be overriden with environment variable
|
||||
`AVALON_OPEN_LAST_WORKFILE`, valid values without case sensitivity are
|
||||
`"0", "1", "true", "false", "yes", "no"`.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project.
|
||||
host_name (str): Name of host which is launched. In avalon's
|
||||
application context it's value stored in app definition under
|
||||
key `"application_dir"`. Is not case sensitive.
|
||||
task_name (str): Name of task which is used for launching the host.
|
||||
Task name is not case sensitive.
|
||||
|
||||
Returns:
|
||||
bool: True if host should start workfile.
|
||||
|
||||
"""
|
||||
|
||||
project_settings = get_project_settings(project_name)
|
||||
startup_presets = (
|
||||
project_settings
|
||||
["global"]
|
||||
["tools"]
|
||||
["Workfiles"]
|
||||
["last_workfile_on_startup"]
|
||||
)
|
||||
|
||||
if not startup_presets:
|
||||
return default_output
|
||||
|
||||
return get_option_from_settings(
|
||||
startup_presets, host_name, task_name, default_output)
|
||||
|
||||
|
||||
def should_workfile_tool_start(
|
||||
project_name, host_name, task_name, default_output=False
|
||||
):
|
||||
"""Define if host should start workfile tool at host launch.
|
||||
|
||||
Default output is `False`. Can be overriden with environment variable
|
||||
`OPENPYPE_WORKFILE_TOOL_ON_START`, valid values without case sensitivity are
|
||||
`"0", "1", "true", "false", "yes", "no"`.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project.
|
||||
host_name (str): Name of host which is launched. In avalon's
|
||||
application context it's value stored in app definition under
|
||||
key `"application_dir"`. Is not case sensitive.
|
||||
task_name (str): Name of task which is used for launching the host.
|
||||
Task name is not case sensitive.
|
||||
|
||||
Returns:
|
||||
bool: True if host should start workfile.
|
||||
|
||||
"""
|
||||
|
||||
project_settings = get_project_settings(project_name)
|
||||
startup_presets = (
|
||||
project_settings
|
||||
["global"]
|
||||
["tools"]
|
||||
["Workfiles"]
|
||||
["open_workfile_tool_on_startup"]
|
||||
)
|
||||
|
||||
if not startup_presets:
|
||||
return default_output
|
||||
|
||||
return get_option_from_settings(
|
||||
startup_presets, host_name, task_name, default_output)
|
||||
|
||||
|
||||
def compile_list_of_regexes(in_list):
|
||||
"""Convert strings in entered list to compiled regex objects."""
|
||||
regexes = list()
|
||||
|
|
|
|||
|
|
@ -11,29 +11,44 @@ from avalon.api import AvalonMongoDB
|
|||
|
||||
|
||||
class AppplicationsAction(BaseAction):
|
||||
"""Application Action class.
|
||||
|
||||
Args:
|
||||
session (ftrack_api.Session): Session where action will be registered.
|
||||
label (str): A descriptive string identifing your action.
|
||||
varaint (str, optional): To group actions together, give them the same
|
||||
label and specify a unique variant per action.
|
||||
identifier (str): An unique identifier for app.
|
||||
description (str): A verbose descriptive text for you action.
|
||||
icon (str): Url path to icon which will be shown in Ftrack web.
|
||||
"""
|
||||
"""Applications Action class."""
|
||||
|
||||
type = "Application"
|
||||
label = "Application action"
|
||||
identifier = "pype_app.{}.".format(str(uuid4()))
|
||||
|
||||
identifier = "openpype_app"
|
||||
_launch_identifier_with_id = None
|
||||
|
||||
icon_url = os.environ.get("OPENPYPE_STATICS_SERVER")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
super(AppplicationsAction, self).__init__(*args, **kwargs)
|
||||
|
||||
self.application_manager = ApplicationManager()
|
||||
self.dbcon = AvalonMongoDB()
|
||||
|
||||
@property
|
||||
def discover_identifier(self):
|
||||
if self._discover_identifier is None:
|
||||
self._discover_identifier = "{}.{}".format(
|
||||
self.identifier, self.process_identifier()
|
||||
)
|
||||
return self._discover_identifier
|
||||
|
||||
@property
|
||||
def launch_identifier(self):
|
||||
if self._launch_identifier is None:
|
||||
self._launch_identifier = "{}.*".format(self.identifier)
|
||||
return self._launch_identifier
|
||||
|
||||
@property
|
||||
def launch_identifier_with_id(self):
|
||||
if self._launch_identifier_with_id is None:
|
||||
self._launch_identifier_with_id = "{}.{}".format(
|
||||
self.identifier, self.process_identifier()
|
||||
)
|
||||
return self._launch_identifier_with_id
|
||||
|
||||
def construct_requirements_validations(self):
|
||||
# Override validation as this action does not need them
|
||||
return
|
||||
|
|
@ -56,7 +71,7 @@ class AppplicationsAction(BaseAction):
|
|||
" and data.actionIdentifier={0}"
|
||||
" and source.user.username={1}"
|
||||
).format(
|
||||
self.identifier + "*",
|
||||
self.launch_identifier,
|
||||
self.session.api_user
|
||||
)
|
||||
self.session.event_hub.subscribe(
|
||||
|
|
@ -136,12 +151,29 @@ class AppplicationsAction(BaseAction):
|
|||
"label": app.group.label,
|
||||
"variant": app.label,
|
||||
"description": None,
|
||||
"actionIdentifier": self.identifier + app_name,
|
||||
"actionIdentifier": "{}.{}".format(
|
||||
self.launch_identifier_with_id, app_name
|
||||
),
|
||||
"icon": app_icon
|
||||
})
|
||||
|
||||
return items
|
||||
|
||||
def _launch(self, event):
|
||||
event_identifier = event["data"]["actionIdentifier"]
|
||||
# Check if identifier is same
|
||||
# - show message that acion may not be triggered on this machine
|
||||
if event_identifier.startswith(self.launch_identifier_with_id):
|
||||
return BaseAction._launch(self, event)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"message": (
|
||||
"There are running more OpenPype processes"
|
||||
" where Application can be launched."
|
||||
)
|
||||
}
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
"""Callback method for the custom action.
|
||||
|
||||
|
|
@ -162,7 +194,8 @@ class AppplicationsAction(BaseAction):
|
|||
*event* the unmodified original event
|
||||
"""
|
||||
identifier = event["data"]["actionIdentifier"]
|
||||
app_name = identifier[len(self.identifier):]
|
||||
id_identifier_len = len(self.launch_identifier_with_id) + 1
|
||||
app_name = identifier[id_identifier_len:]
|
||||
|
||||
entity = entities[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -9,16 +9,24 @@ class MultipleNotes(BaseAction):
|
|||
#: Action label.
|
||||
label = 'Multiple Notes'
|
||||
#: Action description.
|
||||
description = 'Add same note to multiple Asset Versions'
|
||||
description = 'Add same note to multiple entities'
|
||||
icon = statics_icon("ftrack", "action_icons", "MultipleNotes.svg")
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
valid = True
|
||||
|
||||
# Check for multiple selection.
|
||||
if len(entities) < 2:
|
||||
valid = False
|
||||
|
||||
# Check for valid entities.
|
||||
valid_entity_types = ['assetversion', 'task']
|
||||
for entity in entities:
|
||||
if entity.entity_type.lower() != 'assetversion':
|
||||
if entity.entity_type.lower() not in valid_entity_types:
|
||||
valid = False
|
||||
break
|
||||
|
||||
return valid
|
||||
|
||||
def interface(self, session, entities, event):
|
||||
|
|
@ -58,7 +66,7 @@ class MultipleNotes(BaseAction):
|
|||
|
||||
splitter = {
|
||||
'type': 'label',
|
||||
'value': '{}'.format(200*"-")
|
||||
'value': '{}'.format(200 * "-")
|
||||
}
|
||||
|
||||
items = []
|
||||
|
|
|
|||
|
|
@ -428,9 +428,11 @@ class PrepareProjectLocal(BaseAction):
|
|||
|
||||
# Trigger create project structure action
|
||||
if create_project_structure_checked:
|
||||
self.trigger_action(
|
||||
self.create_project_structure_identifier, event
|
||||
trigger_identifier = "{}.{}".format(
|
||||
self.create_project_structure_identifier,
|
||||
self.process_identifier()
|
||||
)
|
||||
self.trigger_action(trigger_identifier, event)
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ class ActionShowWhereIRun(BaseAction):
|
|||
|
||||
return False
|
||||
|
||||
@property
|
||||
def launch_identifier(self):
|
||||
return self.identifier
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
# Don't show info when was launch from this session
|
||||
if session.event_hub.id == event.get("data", {}).get("event_hub_id"):
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ class BaseAction(BaseHandler):
|
|||
icon = None
|
||||
type = 'Action'
|
||||
|
||||
_discover_identifier = None
|
||||
_launch_identifier = None
|
||||
|
||||
settings_frack_subkey = "user_handlers"
|
||||
settings_enabled_key = "enabled"
|
||||
|
||||
|
|
@ -42,6 +45,22 @@ class BaseAction(BaseHandler):
|
|||
|
||||
super().__init__(session)
|
||||
|
||||
@property
|
||||
def discover_identifier(self):
|
||||
if self._discover_identifier is None:
|
||||
self._discover_identifier = "{}.{}".format(
|
||||
self.identifier, self.process_identifier()
|
||||
)
|
||||
return self._discover_identifier
|
||||
|
||||
@property
|
||||
def launch_identifier(self):
|
||||
if self._launch_identifier is None:
|
||||
self._launch_identifier = "{}.{}".format(
|
||||
self.identifier, self.process_identifier()
|
||||
)
|
||||
return self._launch_identifier
|
||||
|
||||
def register(self):
|
||||
'''
|
||||
Registers the action, subscribing the the discover and launch topics.
|
||||
|
|
@ -60,7 +79,7 @@ class BaseAction(BaseHandler):
|
|||
' and data.actionIdentifier={0}'
|
||||
' and source.user.username={1}'
|
||||
).format(
|
||||
self.identifier,
|
||||
self.launch_identifier,
|
||||
self.session.api_user
|
||||
)
|
||||
self.session.event_hub.subscribe(
|
||||
|
|
@ -86,7 +105,7 @@ class BaseAction(BaseHandler):
|
|||
'label': self.label,
|
||||
'variant': self.variant,
|
||||
'description': self.description,
|
||||
'actionIdentifier': self.identifier,
|
||||
'actionIdentifier': self.discover_identifier,
|
||||
'icon': self.icon,
|
||||
}]
|
||||
}
|
||||
|
|
@ -309,6 +328,78 @@ class BaseAction(BaseHandler):
|
|||
return True
|
||||
|
||||
|
||||
class LocalAction(BaseAction):
|
||||
"""Action that warn user when more Processes with same action are running.
|
||||
|
||||
Action is launched all the time but if id does not match id of current
|
||||
instanace then message is shown to user.
|
||||
|
||||
Handy for actions where matters if is executed on specific machine.
|
||||
"""
|
||||
_full_launch_identifier = None
|
||||
|
||||
@property
|
||||
def discover_identifier(self):
|
||||
if self._discover_identifier is None:
|
||||
self._discover_identifier = "{}.{}".format(
|
||||
self.identifier, self.process_identifier()
|
||||
)
|
||||
return self._discover_identifier
|
||||
|
||||
@property
|
||||
def launch_identifier(self):
|
||||
"""Catch all topics with same identifier."""
|
||||
if self._launch_identifier is None:
|
||||
self._launch_identifier = "{}.*".format(self.identifier)
|
||||
return self._launch_identifier
|
||||
|
||||
@property
|
||||
def full_launch_identifier(self):
|
||||
"""Catch all topics with same identifier."""
|
||||
if self._full_launch_identifier is None:
|
||||
self._full_launch_identifier = "{}.{}".format(
|
||||
self.identifier, self.process_identifier()
|
||||
)
|
||||
return self._full_launch_identifier
|
||||
|
||||
def _discover(self, event):
|
||||
entities = self._translate_event(event)
|
||||
if not entities:
|
||||
return
|
||||
|
||||
accepts = self.discover(self.session, entities, event)
|
||||
if not accepts:
|
||||
return
|
||||
|
||||
self.log.debug("Discovering action with selection: {0}".format(
|
||||
event["data"].get("selection", [])
|
||||
))
|
||||
|
||||
return {
|
||||
"items": [{
|
||||
"label": self.label,
|
||||
"variant": self.variant,
|
||||
"description": self.description,
|
||||
"actionIdentifier": self.discover_identifier,
|
||||
"icon": self.icon,
|
||||
}]
|
||||
}
|
||||
|
||||
def _launch(self, event):
|
||||
event_identifier = event["data"]["actionIdentifier"]
|
||||
# Check if identifier is same
|
||||
# - show message that acion may not be triggered on this machine
|
||||
if event_identifier != self.full_launch_identifier:
|
||||
return {
|
||||
"success": False,
|
||||
"message": (
|
||||
"There are running more OpenPype processes"
|
||||
" where this action could be launched."
|
||||
)
|
||||
}
|
||||
return super(LocalAction, self)._launch(event)
|
||||
|
||||
|
||||
class ServerAction(BaseAction):
|
||||
"""Action class meant to be used on event server.
|
||||
|
||||
|
|
@ -318,6 +409,14 @@ class ServerAction(BaseAction):
|
|||
|
||||
settings_frack_subkey = "events"
|
||||
|
||||
@property
|
||||
def discover_identifier(self):
|
||||
return self.identifier
|
||||
|
||||
@property
|
||||
def launch_identifier(self):
|
||||
return self.identifier
|
||||
|
||||
def register(self):
|
||||
"""Register subcription to Ftrack event hub."""
|
||||
self.session.event_hub.subscribe(
|
||||
|
|
@ -328,5 +427,5 @@ class ServerAction(BaseAction):
|
|||
|
||||
launch_subscription = (
|
||||
"topic=ftrack.action.launch and data.actionIdentifier={0}"
|
||||
).format(self.identifier)
|
||||
).format(self.launch_identifier)
|
||||
self.session.event_hub.subscribe(launch_subscription, self._launch)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
import tempfile
|
||||
import json
|
||||
import functools
|
||||
import uuid
|
||||
import datetime
|
||||
import traceback
|
||||
import time
|
||||
|
|
@ -36,6 +37,7 @@ class BaseHandler(object):
|
|||
<description> - a verbose descriptive text for you action
|
||||
<icon> - icon in ftrack
|
||||
'''
|
||||
_process_id = None
|
||||
# Default priority is 100
|
||||
priority = 100
|
||||
# Type is just for logging purpose (e.g.: Action, Event, Application,...)
|
||||
|
|
@ -70,6 +72,13 @@ class BaseHandler(object):
|
|||
self.register = self.register_decorator(self.register)
|
||||
self.launch = self.launch_log(self.launch)
|
||||
|
||||
@staticmethod
|
||||
def process_identifier():
|
||||
"""Helper property to have """
|
||||
if not BaseHandler._process_id:
|
||||
BaseHandler._process_id = str(uuid.uuid4())
|
||||
return BaseHandler._process_id
|
||||
|
||||
# Decorator
|
||||
def register_decorator(self, func):
|
||||
@functools.wraps(func)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from openpype.api import Logger
|
|||
|
||||
log = Logger().get_logger("Event processor")
|
||||
|
||||
|
||||
class TimersManagerModuleRestApi:
|
||||
"""
|
||||
REST API endpoint used for calling from hosts when context change
|
||||
|
|
@ -22,6 +23,11 @@ class TimersManagerModuleRestApi:
|
|||
self.prefix + "/start_timer",
|
||||
self.start_timer
|
||||
)
|
||||
self.server_manager.add_route(
|
||||
"POST",
|
||||
self.prefix + "/stop_timer",
|
||||
self.stop_timer
|
||||
)
|
||||
|
||||
async def start_timer(self, request):
|
||||
data = await request.json()
|
||||
|
|
@ -38,3 +44,7 @@ class TimersManagerModuleRestApi:
|
|||
self.module.stop_timers()
|
||||
self.module.start_timer(project_name, asset_name, task_name, hierarchy)
|
||||
return Response(status=200)
|
||||
|
||||
async def stop_timer(self, request):
|
||||
self.module.stop_timers()
|
||||
return Response(status=200)
|
||||
|
|
|
|||
|
|
@ -303,6 +303,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
key_values = {"families": family, "tasks": task_name}
|
||||
profile = filter_profiles(self.template_name_profiles, key_values,
|
||||
logger=self.log)
|
||||
|
||||
template_name = "publish"
|
||||
if profile:
|
||||
template_name = profile["template_name"]
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin):
|
|||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Asset Name"
|
||||
label = "Validate Editorial Asset Name"
|
||||
|
||||
def process(self, context):
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"publish": {
|
||||
"ValidateEditorialAssetName": {
|
||||
"enabled": true,
|
||||
"optional": false
|
||||
},
|
||||
"ValidateVersion": {
|
||||
"enabled": true,
|
||||
"optional": false
|
||||
},
|
||||
"IntegrateHeroVersion": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
|
|
@ -260,6 +268,13 @@
|
|||
"enabled": true
|
||||
}
|
||||
],
|
||||
"open_workfile_tool_on_startup": [
|
||||
{
|
||||
"hosts": [],
|
||||
"tasks": [],
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"sw_folders": {
|
||||
"compositing": [
|
||||
"nuke",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,22 @@
|
|||
"workfile": "ma",
|
||||
"yetiRig": "ma"
|
||||
},
|
||||
"scriptsmenu": {
|
||||
"name": "OpenPype Tools",
|
||||
"definition": [
|
||||
{
|
||||
"type": "action",
|
||||
"command": "import openpype.hosts.maya.api.commands as op_cmds; op_cmds.edit_shader_definitions()",
|
||||
"sourcetype": "python",
|
||||
"title": "Edit shader name definitions",
|
||||
"tooltip": "Edit shader name definitions used in validation and renaming.",
|
||||
"tags": [
|
||||
"pipeline",
|
||||
"shader"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"create": {
|
||||
"CreateLook": {
|
||||
"enabled": true,
|
||||
|
|
@ -148,12 +164,14 @@
|
|||
},
|
||||
"ValidateModelName": {
|
||||
"enabled": false,
|
||||
"database": true,
|
||||
"material_file": {
|
||||
"windows": "",
|
||||
"darwin": "",
|
||||
"linux": ""
|
||||
},
|
||||
"regex": "(.*)_(\\\\d)*_(.*)_(GEO)"
|
||||
"regex": "(.*)_(\\d)*_(?P<shader>.*)_(GEO)",
|
||||
"top_level_regex": ".*_GRP"
|
||||
},
|
||||
"ValidateTransformNamingSuffix": {
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@
|
|||
},
|
||||
"shot_add_tasks": {}
|
||||
},
|
||||
"shot_add_tasks": {
|
||||
"CollectInstances": {
|
||||
"custom_start_frame": 0,
|
||||
"timeline_frame_start": 900000,
|
||||
"timeline_frame_offset": 0,
|
||||
|
|
|
|||
|
|
@ -144,6 +144,13 @@ class DictConditionalEntity(ItemEntity):
|
|||
|
||||
self.enum_entity = None
|
||||
|
||||
# GUI attributes
|
||||
self.enum_is_horizontal = self.schema_data.get(
|
||||
"enum_is_horizontal", False
|
||||
)
|
||||
# `enum_on_right` can be used only if
|
||||
self.enum_on_right = self.schema_data.get("enum_on_right", False)
|
||||
|
||||
self.highlight_content = self.schema_data.get(
|
||||
"highlight_content", False
|
||||
)
|
||||
|
|
@ -185,13 +192,13 @@ class DictConditionalEntity(ItemEntity):
|
|||
children_def_keys = []
|
||||
for children_def in self.enum_children:
|
||||
if not isinstance(children_def, dict):
|
||||
raise EntitySchemaError((
|
||||
raise EntitySchemaError(self, (
|
||||
"Children definition under key 'enum_children' must"
|
||||
" be a dictionary."
|
||||
))
|
||||
|
||||
if "key" not in children_def:
|
||||
raise EntitySchemaError((
|
||||
raise EntitySchemaError(self, (
|
||||
"Children definition under key 'enum_children' miss"
|
||||
" 'key' definition."
|
||||
))
|
||||
|
|
@ -286,7 +293,7 @@ class DictConditionalEntity(ItemEntity):
|
|||
"multiselection": False,
|
||||
"enum_items": enum_items,
|
||||
"key": enum_key,
|
||||
"label": self.enum_label or enum_key
|
||||
"label": self.enum_label
|
||||
}
|
||||
|
||||
enum_entity = self.create_schema_object(enum_schema, self)
|
||||
|
|
|
|||
|
|
@ -204,6 +204,8 @@
|
|||
- it is possible to add darker background with `"highlight_content"` (Default: `False`)
|
||||
- darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color
|
||||
- output is dictionary `{the "key": children values}`
|
||||
- for UI porposes was added `enum_is_horizontal` which will make combobox appear next to children inputs instead of on top of them (Default: `False`)
|
||||
- this has extended ability of `enum_on_right` which will move combobox to right side next to children widgets (Default: `False`)
|
||||
```
|
||||
# Example
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@
|
|||
"type": "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_maya_scriptsmenu"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_maya_create"
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "shot_add_tasks",
|
||||
"key": "CollectInstances",
|
||||
"label": "Collect Clip Instances",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
|
|
|
|||
|
|
@ -4,6 +4,46 @@
|
|||
"key": "publish",
|
||||
"label": "Publish plugins",
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"key": "ValidateEditorialAssetName",
|
||||
"label": "Validate Editorial Asset Name",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "optional",
|
||||
"label": "Optional"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"key": "ValidateVersion",
|
||||
"label": "Validate Version",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "optional",
|
||||
"label": "Optional"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -112,6 +112,41 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "open_workfile_tool_on_startup",
|
||||
"label": "Open workfile tool on launch",
|
||||
"is_group": true,
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"type": "hosts-enum",
|
||||
"key": "hosts",
|
||||
"label": "Hosts",
|
||||
"multiselection": true,
|
||||
"hosts_filter": [
|
||||
"nuke"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "tasks",
|
||||
"label": "Tasks",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "dict-modifiable",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -147,9 +147,14 @@
|
|||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "database",
|
||||
"label": "Use database shader name definitions"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Path to material file defining list of material names to check. This is material name per line simple text file.<br/>It will be checked against named group <b>shader</b> in your <em>Validation regex</em>.<p>For example: <br/> <code>^.*(?P=<shader>.+)_GEO</code></p>"
|
||||
"label": "Path to material file defining list of material names to check. This is material name per line simple text file.<br/>It will be checked against named group <b>shader</b> in your <em>Validation regex</em>.<p>For example: <br/> <code>^.*(?P=<shader>.+)_GEO</code></p>This is used instead of database definitions if they are disabled."
|
||||
},
|
||||
{
|
||||
"type": "path",
|
||||
|
|
@ -162,6 +167,15 @@
|
|||
"type": "text",
|
||||
"key": "regex",
|
||||
"label": "Validation regex"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Regex for validating name of top level group name.<br/>You can use named capturing groups:<br/><code>(?P<asset>.*)</code> for Asset name<br/><code>(?P<subset>.*)</code> for Subset<br/><code>(?P<project>.*)</code> for project<br/><p>For example to check for asset in name so <code>*_some_asset_name_GRP</code> is valid, use:<br/><code>.*?_(?P<asset>.*)_GEO</code>"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "top_level_regex",
|
||||
"label": "Top level group name regex"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "scriptsmenu",
|
||||
"label": "Scripts Menu Definition",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "name",
|
||||
"label": "Menu Name"
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "raw-json",
|
||||
"key": "definition",
|
||||
"label": "Menu definition",
|
||||
"is_list": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -9,6 +9,31 @@
|
|||
"label": "Color input",
|
||||
"type": "color"
|
||||
},
|
||||
{
|
||||
"type": "dict-conditional",
|
||||
"key": "overriden_value",
|
||||
"label": "Overriden value",
|
||||
"enum_key": "overriden",
|
||||
"enum_is_horizontal": true,
|
||||
"enum_children": [
|
||||
{
|
||||
"key": "overriden",
|
||||
"label": "Override value",
|
||||
"children": [
|
||||
{
|
||||
"type": "number",
|
||||
"key": "value",
|
||||
"label": "value"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "inherit",
|
||||
"label": "Inherit value",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict-conditional",
|
||||
"use_label_wrap": true,
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
msg = "<br><br>".join(warnings)
|
||||
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
dialog.setWindowTitle("Save warnings")
|
||||
dialog.setText(msg)
|
||||
dialog.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
dialog.exec_()
|
||||
|
|
@ -303,6 +304,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
except Exception as exc:
|
||||
formatted_traceback = traceback.format_exception(*sys.exc_info())
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
dialog.setWindowTitle("Unexpected error")
|
||||
msg = "Unexpected error happened!\n\nError: {}".format(str(exc))
|
||||
dialog.setText(msg)
|
||||
dialog.setDetailedText("\n".join(formatted_traceback))
|
||||
|
|
@ -392,6 +394,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
except Exception as exc:
|
||||
formatted_traceback = traceback.format_exception(*sys.exc_info())
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
dialog.setWindowTitle("Unexpected error")
|
||||
msg = "Unexpected error happened!\n\nError: {}".format(str(exc))
|
||||
dialog.setText(msg)
|
||||
dialog.setDetailedText("\n".join(formatted_traceback))
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class DictConditionalWidget(BaseWidget):
|
|||
self.body_widget = None
|
||||
self.content_widget = None
|
||||
self.content_layout = None
|
||||
self.enum_layout = None
|
||||
|
||||
label = None
|
||||
if self.entity.is_dynamic_item:
|
||||
|
|
@ -40,8 +41,36 @@ class DictConditionalWidget(BaseWidget):
|
|||
self._enum_key_by_wrapper_id = {}
|
||||
self._added_wrapper_ids = set()
|
||||
|
||||
self.content_layout.setColumnStretch(0, 0)
|
||||
self.content_layout.setColumnStretch(1, 1)
|
||||
enum_layout = QtWidgets.QGridLayout()
|
||||
enum_layout.setContentsMargins(0, 0, 0, 0)
|
||||
enum_layout.setColumnStretch(0, 0)
|
||||
enum_layout.setColumnStretch(1, 1)
|
||||
|
||||
all_children_layout = QtWidgets.QVBoxLayout()
|
||||
all_children_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
if self.entity.enum_is_horizontal:
|
||||
if self.entity.enum_on_right:
|
||||
self.content_layout.addLayout(all_children_layout, 0, 0)
|
||||
self.content_layout.addLayout(enum_layout, 0, 1)
|
||||
# Stretch combobox to minimum and expand value
|
||||
self.content_layout.setColumnStretch(0, 1)
|
||||
self.content_layout.setColumnStretch(1, 0)
|
||||
else:
|
||||
self.content_layout.addLayout(enum_layout, 0, 0)
|
||||
self.content_layout.addLayout(all_children_layout, 0, 1)
|
||||
# Stretch combobox to minimum and expand value
|
||||
self.content_layout.setColumnStretch(0, 0)
|
||||
self.content_layout.setColumnStretch(1, 1)
|
||||
|
||||
else:
|
||||
# Expand content
|
||||
self.content_layout.setColumnStretch(0, 1)
|
||||
self.content_layout.addLayout(enum_layout, 0, 0)
|
||||
self.content_layout.addLayout(all_children_layout, 1, 0)
|
||||
|
||||
self.enum_layout = enum_layout
|
||||
self.all_children_layout = all_children_layout
|
||||
|
||||
# Add enum entity to layout mapping
|
||||
enum_entity = self.entity.enum_entity
|
||||
|
|
@ -58,6 +87,8 @@ class DictConditionalWidget(BaseWidget):
|
|||
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
content_layout.setSpacing(5)
|
||||
|
||||
all_children_layout.addWidget(content_widget)
|
||||
|
||||
self._content_by_enum_value[enum_key] = {
|
||||
"widget": content_widget,
|
||||
"layout": content_layout
|
||||
|
|
@ -80,9 +111,6 @@ class DictConditionalWidget(BaseWidget):
|
|||
|
||||
for item_key, children in self.entity.children.items():
|
||||
content_widget = self._content_by_enum_value[item_key]["widget"]
|
||||
row = self.content_layout.rowCount()
|
||||
self.content_layout.addWidget(content_widget, row, 0, 1, 2)
|
||||
|
||||
for child_obj in children:
|
||||
self.input_fields.append(
|
||||
self.create_ui_for_entity(
|
||||
|
|
@ -191,12 +219,25 @@ class DictConditionalWidget(BaseWidget):
|
|||
else:
|
||||
map_id = widget.entity.id
|
||||
|
||||
content_widget = self.content_widget
|
||||
content_layout = self.content_layout
|
||||
if map_id != self.entity.enum_entity.id:
|
||||
enum_value = self._enum_key_by_wrapper_id[map_id]
|
||||
content_widget = self._content_by_enum_value[enum_value]["widget"]
|
||||
content_layout = self._content_by_enum_value[enum_value]["layout"]
|
||||
is_enum_item = map_id == self.entity.enum_entity.id
|
||||
if is_enum_item:
|
||||
content_widget = self.content_widget
|
||||
content_layout = self.enum_layout
|
||||
|
||||
if not label:
|
||||
content_layout.addWidget(widget, 0, 0, 1, 2)
|
||||
return
|
||||
|
||||
label_widget = GridLabelWidget(label, widget)
|
||||
label_widget.input_field = widget
|
||||
widget.label_widget = label_widget
|
||||
content_layout.addWidget(label_widget, 0, 0, 1, 1)
|
||||
content_layout.addWidget(widget, 0, 1, 1, 1)
|
||||
return
|
||||
|
||||
enum_value = self._enum_key_by_wrapper_id[map_id]
|
||||
content_widget = self._content_by_enum_value[enum_value]["widget"]
|
||||
content_layout = self._content_by_enum_value[enum_value]["layout"]
|
||||
|
||||
wrapper = self._parent_widget_by_entity_id[map_id]
|
||||
if wrapper is not content_widget:
|
||||
|
|
|
|||
|
|
@ -94,7 +94,8 @@ class MainWidget(QtWidgets.QWidget):
|
|||
super(MainWidget, self).showEvent(event)
|
||||
if self._reset_on_show:
|
||||
self._reset_on_show = False
|
||||
self.reset()
|
||||
# Trigger reset with 100ms delay
|
||||
QtCore.QTimer.singleShot(100, self.reset)
|
||||
|
||||
def _show_password_dialog(self):
|
||||
if self._password_dialog:
|
||||
|
|
@ -107,6 +108,8 @@ class MainWidget(QtWidgets.QWidget):
|
|||
self._password_dialog = None
|
||||
if password_passed:
|
||||
self.reset()
|
||||
if not self.isVisible():
|
||||
self.show()
|
||||
else:
|
||||
self.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -693,16 +693,16 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
)
|
||||
return
|
||||
|
||||
file_path = os.path.join(self.root, work_file)
|
||||
file_path = os.path.join(os.path.normpath(self.root), work_file)
|
||||
|
||||
pipeline.emit("before.workfile.save", file_path)
|
||||
pipeline.emit("before.workfile.save", [file_path])
|
||||
|
||||
self._enter_session() # Make sure we are in the right session
|
||||
self.host.save_file(file_path)
|
||||
|
||||
self.set_asset_task(self._asset, self._task)
|
||||
|
||||
pipeline.emit("after.workfile.save", file_path)
|
||||
pipeline.emit("after.workfile.save", [file_path])
|
||||
|
||||
self.workfile_created.emit(file_path)
|
||||
|
||||
|
|
|
|||
5
openpype/vendor/python/common/scriptsmenu/__init__.py
vendored
Normal file
5
openpype/vendor/python/common/scriptsmenu/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .scriptsmenu import ScriptsMenu
|
||||
from . import version
|
||||
|
||||
__all__ = ["ScriptsMenu"]
|
||||
__version__ = version.version
|
||||
207
openpype/vendor/python/common/scriptsmenu/action.py
vendored
Normal file
207
openpype/vendor/python/common/scriptsmenu/action.py
vendored
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import os
|
||||
|
||||
from .vendor.Qt import QtWidgets
|
||||
|
||||
|
||||
class Action(QtWidgets.QAction):
|
||||
"""Custom Action widget"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
||||
QtWidgets.QAction.__init__(self, parent)
|
||||
|
||||
self._root = None
|
||||
self._tags = list()
|
||||
self._command = None
|
||||
self._sourcetype = None
|
||||
self._iconfile = None
|
||||
self._label = None
|
||||
|
||||
self._COMMAND = """import imp
|
||||
f, filepath, descr = imp.find_module('{module_name}', ['{dirname}'])
|
||||
module = imp.load_module('{module_name}', f, filepath, descr)
|
||||
module.{module_name}()"""
|
||||
|
||||
@property
|
||||
def root(self):
|
||||
return self._root
|
||||
|
||||
@root.setter
|
||||
def root(self, value):
|
||||
self._root = value
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return self._tags
|
||||
|
||||
@tags.setter
|
||||
def tags(self, value):
|
||||
self._tags = value
|
||||
|
||||
@property
|
||||
def command(self):
|
||||
return self._command
|
||||
|
||||
@command.setter
|
||||
def command(self, value):
|
||||
"""
|
||||
Store the command in the QAction
|
||||
|
||||
Args:
|
||||
value (str): the full command which will be executed when clicked
|
||||
|
||||
Return:
|
||||
None
|
||||
"""
|
||||
self._command = value
|
||||
|
||||
@property
|
||||
def sourcetype(self):
|
||||
return self._sourcetype
|
||||
|
||||
@sourcetype.setter
|
||||
def sourcetype(self, value):
|
||||
"""
|
||||
Set the command type to get the correct execution of the command given
|
||||
|
||||
Args:
|
||||
value (str): the name of the command type
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
self._sourcetype = value
|
||||
|
||||
@property
|
||||
def iconfile(self):
|
||||
return self._iconfile
|
||||
|
||||
@iconfile.setter
|
||||
def iconfile(self, value):
|
||||
"""Store the path to the image file which needs to be displayed
|
||||
|
||||
Args:
|
||||
value (str): the path to the image
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self._iconfile = value
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return self._label
|
||||
|
||||
@label.setter
|
||||
def label(self, value):
|
||||
"""
|
||||
Set the abbreviation which will be used as overlay text in the shelf
|
||||
|
||||
Args:
|
||||
value (str): an abbreviation of the name
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
self._label = value
|
||||
|
||||
def run_command(self):
|
||||
"""
|
||||
Run the command of the instance or copy the command to the active shelf
|
||||
based on the current modifiers.
|
||||
|
||||
If callbacks have been registered with fouind modifier integer the
|
||||
function will trigger all callbacks. When a callback function returns a
|
||||
non zero integer it will not execute the action's command
|
||||
|
||||
"""
|
||||
|
||||
# get the current application and its linked keyboard modifiers
|
||||
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
||||
|
||||
# If the menu has a callback registered for the current modifier
|
||||
# we run the callback instead of the action itself.
|
||||
registered = self._root.registered_callbacks
|
||||
callbacks = registered.get(int(modifiers), [])
|
||||
for callback in callbacks:
|
||||
signal = callback(self)
|
||||
if signal != 0:
|
||||
# Exit function on non-zero return code
|
||||
return
|
||||
|
||||
exec(self.process_command())
|
||||
|
||||
def process_command(self):
|
||||
"""
|
||||
Check if the command is a file which needs to be launched and if it
|
||||
has a relative path, if so return the full path by expanding
|
||||
environment variables. Wrap any mel command in a executable string
|
||||
for Python and return the string if the source type is
|
||||
|
||||
Add your own source type and required process to ensure callback
|
||||
is stored correctly.
|
||||
|
||||
An example of a process is the sourcetype is MEL
|
||||
(Maya Embedded Language) as Python cannot run it on its own so it
|
||||
needs to be wrapped in a string in which we explicitly import mel and
|
||||
run it as a mel.eval. The string is then parsed to python as
|
||||
exec("command").
|
||||
|
||||
Returns:
|
||||
str: a clean command which can be used
|
||||
|
||||
"""
|
||||
if self._sourcetype == "python":
|
||||
return self._command
|
||||
|
||||
if self._sourcetype == "mel":
|
||||
# Escape single quotes
|
||||
conversion = self._command.replace("'", "\\'")
|
||||
return "import maya; maya.mel.eval('{}')".format(conversion)
|
||||
|
||||
if self._sourcetype == "file":
|
||||
if os.path.isabs(self._command):
|
||||
filepath = self._command
|
||||
else:
|
||||
filepath = os.path.normpath(os.path.expandvars(self._command))
|
||||
|
||||
return self._wrap_filepath(filepath)
|
||||
|
||||
def has_tag(self, tag):
|
||||
"""Check whether the tag matches with the action's tags.
|
||||
|
||||
A partial match will also return True, for example tag `a` will match
|
||||
correctly with the `ape` tag on the Action.
|
||||
|
||||
Args:
|
||||
tag (str): The tag
|
||||
|
||||
Returns
|
||||
bool: Whether the action is tagged with given tag
|
||||
|
||||
"""
|
||||
|
||||
for tagitem in self.tags:
|
||||
if tag not in tagitem:
|
||||
continue
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _wrap_filepath(self, file_path):
|
||||
"""Create a wrapped string for the python command
|
||||
|
||||
Args:
|
||||
file_path (str): the filepath of a script
|
||||
|
||||
Returns:
|
||||
str: the wrapped command
|
||||
"""
|
||||
|
||||
dirname = os.path.dirname(r"{}".format(file_path))
|
||||
dirpath = dirname.replace("\\", "/")
|
||||
module_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||
|
||||
return self._COMMAND.format(module_name=module_name, dirname=dirpath)
|
||||
54
openpype/vendor/python/common/scriptsmenu/launchformari.py
vendored
Normal file
54
openpype/vendor/python/common/scriptsmenu/launchformari.py
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
|
||||
# Import third-party modules
|
||||
from vendor.Qt import QtWidgets
|
||||
|
||||
# Import local modules
|
||||
import scriptsmenu
|
||||
|
||||
|
||||
def _mari_main_window():
|
||||
"""Get Mari main window.
|
||||
|
||||
Returns:
|
||||
MriMainWindow: Mari's main window.
|
||||
|
||||
"""
|
||||
for obj in QtWidgets.QApplication.topLevelWidgets():
|
||||
if obj.metaObject().className() == 'MriMainWindow':
|
||||
return obj
|
||||
raise RuntimeError('Could not find Mari MainWindow instance')
|
||||
|
||||
|
||||
def _mari_main_menubar():
|
||||
"""Get Mari main menu bar.
|
||||
|
||||
Returns:
|
||||
Retrieve the main menubar of the Mari window.
|
||||
|
||||
"""
|
||||
mari_window = _mari_main_window()
|
||||
menubar = [
|
||||
i for i in mari_window.children() if isinstance(i, QtWidgets.QMenuBar)
|
||||
]
|
||||
assert len(menubar) == 1, "Error, could not find menu bar!"
|
||||
return menubar[0]
|
||||
|
||||
|
||||
def main(title="Scripts"):
|
||||
"""Build the main scripts menu in Mari.
|
||||
|
||||
Args:
|
||||
title (str): Name of the menu in the application.
|
||||
|
||||
Returns:
|
||||
scriptsmenu.ScriptsMenu: Instance object.
|
||||
|
||||
"""
|
||||
mari_main_bar = _mari_main_menubar()
|
||||
for mari_bar in mari_main_bar.children():
|
||||
if isinstance(mari_bar, scriptsmenu.ScriptsMenu):
|
||||
if mari_bar.title() == title:
|
||||
menu = mari_bar
|
||||
return menu
|
||||
menu = scriptsmenu.ScriptsMenu(title=title, parent=mari_main_bar)
|
||||
return menu
|
||||
137
openpype/vendor/python/common/scriptsmenu/launchformaya.py
vendored
Normal file
137
openpype/vendor/python/common/scriptsmenu/launchformaya.py
vendored
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import logging
|
||||
|
||||
import maya.cmds as cmds
|
||||
import maya.mel as mel
|
||||
|
||||
import scriptsmenu
|
||||
from .vendor.Qt import QtCore, QtWidgets
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register_repeat_last(action):
|
||||
"""Register the action in repeatLast to ensure the RepeatLast hotkey works
|
||||
|
||||
Args:
|
||||
action (action.Action): Action wigdet instance
|
||||
|
||||
Returns:
|
||||
int: 0
|
||||
|
||||
"""
|
||||
command = action.process_command()
|
||||
command = command.replace("\n", "; ")
|
||||
# Register command to Maya (mel)
|
||||
cmds.repeatLast(addCommand='python("{}")'.format(command),
|
||||
addCommandLabel=action.label)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def to_shelf(action):
|
||||
"""Copy clicked menu item to the currently active Maya shelf
|
||||
Args:
|
||||
action (action.Action): the action instance which is clicked
|
||||
|
||||
Returns:
|
||||
int: 1
|
||||
|
||||
"""
|
||||
|
||||
shelftoplevel = mel.eval("$gShelfTopLevel = $gShelfTopLevel;")
|
||||
current_active_shelf = cmds.tabLayout(shelftoplevel,
|
||||
query=True,
|
||||
selectTab=True)
|
||||
|
||||
cmds.shelfButton(command=action.process_command(),
|
||||
sourceType="python",
|
||||
parent=current_active_shelf,
|
||||
image=action.iconfile or "pythonFamily.png",
|
||||
annotation=action.statusTip(),
|
||||
imageOverlayLabel=action.label or "")
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
def _maya_main_window():
|
||||
"""Return Maya's main window"""
|
||||
for obj in QtWidgets.QApplication.topLevelWidgets():
|
||||
if obj.objectName() == 'MayaWindow':
|
||||
return obj
|
||||
raise RuntimeError('Could not find MayaWindow instance')
|
||||
|
||||
|
||||
def _maya_main_menubar():
|
||||
"""Retrieve the main menubar of the Maya window"""
|
||||
mayawindow = _maya_main_window()
|
||||
menubar = [i for i in mayawindow.children()
|
||||
if isinstance(i, QtWidgets.QMenuBar)]
|
||||
|
||||
assert len(menubar) == 1, "Error, could not find menu bar!"
|
||||
|
||||
return menubar[0]
|
||||
|
||||
|
||||
def find_scripts_menu(title, parent):
|
||||
"""
|
||||
Check if the menu exists with the given title in the parent
|
||||
|
||||
Args:
|
||||
title (str): the title name of the scripts menu
|
||||
|
||||
parent (QtWidgets.QMenuBar): the menubar to check
|
||||
|
||||
Returns:
|
||||
QtWidgets.QMenu or None
|
||||
|
||||
"""
|
||||
|
||||
menu = None
|
||||
search = [i for i in parent.children() if
|
||||
isinstance(i, scriptsmenu.ScriptsMenu)
|
||||
and i.title() == title]
|
||||
|
||||
if search:
|
||||
assert len(search) < 2, ("Multiple instances of menu '{}' "
|
||||
"in menu bar".format(title))
|
||||
menu = search[0]
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
def main(title="Scripts", parent=None, objectName=None):
|
||||
"""Build the main scripts menu in Maya
|
||||
|
||||
Args:
|
||||
title (str): name of the menu in the application
|
||||
|
||||
parent (QtWidgets.QtMenuBar): the parent object for the menu
|
||||
|
||||
objectName (str): custom objectName for scripts menu
|
||||
|
||||
Returns:
|
||||
scriptsmenu.ScriptsMenu instance
|
||||
|
||||
"""
|
||||
|
||||
mayamainbar = parent or _maya_main_menubar()
|
||||
try:
|
||||
# check menu already exists
|
||||
menu = find_scripts_menu(title, mayamainbar)
|
||||
if not menu:
|
||||
log.info("Attempting to build menu ...")
|
||||
object_name = objectName or title.lower()
|
||||
menu = scriptsmenu.ScriptsMenu(title=title,
|
||||
parent=mayamainbar,
|
||||
objectName=object_name)
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
return
|
||||
|
||||
# Register control + shift callback to add to shelf (maya behavior)
|
||||
modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier
|
||||
menu.register_callback(int(modifiers), to_shelf)
|
||||
|
||||
menu.register_callback(0, register_repeat_last)
|
||||
|
||||
return menu
|
||||
36
openpype/vendor/python/common/scriptsmenu/launchfornuke.py
vendored
Normal file
36
openpype/vendor/python/common/scriptsmenu/launchfornuke.py
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import scriptsmenu
|
||||
from .vendor.Qt import QtWidgets
|
||||
|
||||
|
||||
def _nuke_main_window():
|
||||
"""Return Nuke's main window"""
|
||||
for obj in QtWidgets.QApplication.topLevelWidgets():
|
||||
if (obj.inherits('QMainWindow') and
|
||||
obj.metaObject().className() == 'Foundry::UI::DockMainWindow'):
|
||||
return obj
|
||||
raise RuntimeError('Could not find Nuke MainWindow instance')
|
||||
|
||||
|
||||
def _nuke_main_menubar():
|
||||
"""Retrieve the main menubar of the Nuke window"""
|
||||
nuke_window = _nuke_main_window()
|
||||
menubar = [i for i in nuke_window.children()
|
||||
if isinstance(i, QtWidgets.QMenuBar)]
|
||||
|
||||
assert len(menubar) == 1, "Error, could not find menu bar!"
|
||||
return menubar[0]
|
||||
|
||||
|
||||
def main(title="Scripts"):
|
||||
# Register control + shift callback to add to shelf (Nuke behavior)
|
||||
# modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier
|
||||
# menu.register_callback(modifiers, to_shelf)
|
||||
nuke_main_bar = _nuke_main_menubar()
|
||||
for nuke_bar in nuke_main_bar.children():
|
||||
if isinstance(nuke_bar, scriptsmenu.ScriptsMenu):
|
||||
if nuke_bar.title() == title:
|
||||
menu = nuke_bar
|
||||
return menu
|
||||
|
||||
menu = scriptsmenu.ScriptsMenu(title=title, parent=nuke_main_bar)
|
||||
return menu
|
||||
316
openpype/vendor/python/common/scriptsmenu/scriptsmenu.py
vendored
Normal file
316
openpype/vendor/python/common/scriptsmenu/scriptsmenu.py
vendored
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
import os
|
||||
import json
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from .vendor.Qt import QtWidgets, QtCore
|
||||
from . import action
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ScriptsMenu(QtWidgets.QMenu):
|
||||
"""A Qt menu that displays a list of searchable actions"""
|
||||
|
||||
updated = QtCore.Signal(QtWidgets.QMenu)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize Scripts menu
|
||||
|
||||
Args:
|
||||
title (str): the name of the root menu which will be created
|
||||
|
||||
parent (QtWidgets.QObject) : the QObject to parent the menu to
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
QtWidgets.QMenu.__init__(self, *args, **kwargs)
|
||||
|
||||
self.searchbar = None
|
||||
self.update_action = None
|
||||
|
||||
self._script_actions = []
|
||||
self._callbacks = defaultdict(list)
|
||||
|
||||
# Automatically add it to the parent menu
|
||||
parent = kwargs.get("parent", None)
|
||||
if parent:
|
||||
parent.addMenu(self)
|
||||
|
||||
objectname = kwargs.get("objectName", "scripts")
|
||||
title = kwargs.get("title", "Scripts")
|
||||
self.setObjectName(objectname)
|
||||
self.setTitle(title)
|
||||
|
||||
# add default items in the menu
|
||||
self.create_default_items()
|
||||
|
||||
def on_update(self):
|
||||
self.updated.emit(self)
|
||||
|
||||
@property
|
||||
def registered_callbacks(self):
|
||||
return self._callbacks.copy()
|
||||
|
||||
def create_default_items(self):
|
||||
"""Add a search bar to the top of the menu given"""
|
||||
|
||||
# create widget and link function
|
||||
searchbar = QtWidgets.QLineEdit()
|
||||
searchbar.setFixedWidth(120)
|
||||
searchbar.setPlaceholderText("Search ...")
|
||||
searchbar.textChanged.connect(self._update_search)
|
||||
self.searchbar = searchbar
|
||||
|
||||
# create widget holder
|
||||
searchbar_action = QtWidgets.QWidgetAction(self)
|
||||
|
||||
# add widget to widget holder
|
||||
searchbar_action.setDefaultWidget(self.searchbar)
|
||||
searchbar_action.setObjectName("Searchbar")
|
||||
|
||||
# add update button and link function
|
||||
update_action = QtWidgets.QAction(self)
|
||||
update_action.setObjectName("Update Scripts")
|
||||
update_action.setText("Update Scripts")
|
||||
update_action.setVisible(False)
|
||||
update_action.triggered.connect(self.on_update)
|
||||
self.update_action = update_action
|
||||
|
||||
# add action to menu
|
||||
self.addAction(searchbar_action)
|
||||
self.addAction(update_action)
|
||||
|
||||
# add separator object
|
||||
separator = self.addSeparator()
|
||||
separator.setObjectName("separator")
|
||||
|
||||
def add_menu(self, title, parent=None):
|
||||
"""Create a sub menu for a parent widget
|
||||
|
||||
Args:
|
||||
parent(QtWidgets.QWidget): the object to parent the menu to
|
||||
|
||||
title(str): the title of the menu
|
||||
|
||||
Returns:
|
||||
QtWidget.QMenu instance
|
||||
"""
|
||||
|
||||
if not parent:
|
||||
parent = self
|
||||
|
||||
menu = QtWidgets.QMenu(parent, title)
|
||||
menu.setTitle(title)
|
||||
menu.setObjectName(title)
|
||||
menu.setTearOffEnabled(True)
|
||||
parent.addMenu(menu)
|
||||
|
||||
return menu
|
||||
|
||||
def add_script(self, parent, title, command, sourcetype, icon=None,
|
||||
tags=None, label=None, tooltip=None):
|
||||
"""Create an action item which runs a script when clicked
|
||||
|
||||
Args:
|
||||
parent (QtWidget.QWidget): The widget to parent the item to
|
||||
|
||||
title (str): The text which will be displayed in the menu
|
||||
|
||||
command (str): The command which needs to be run when the item is
|
||||
clicked.
|
||||
|
||||
sourcetype (str): The type of command, the way the command is
|
||||
processed is based on the source type.
|
||||
|
||||
icon (str): The file path of an icon to display with the menu item
|
||||
|
||||
tags (list, tuple): Keywords which describe the action
|
||||
|
||||
label (str): A short description of the script which will be displayed
|
||||
when hovering over the menu item
|
||||
|
||||
tooltip (str): A tip for the user about the usage fo the tool
|
||||
|
||||
Returns:
|
||||
QtWidget.QAction instance
|
||||
|
||||
"""
|
||||
|
||||
assert tags is None or isinstance(tags, (list, tuple))
|
||||
# Ensure tags is a list
|
||||
tags = list() if tags is None else list(tags)
|
||||
tags.append(title.lower())
|
||||
|
||||
assert icon is None or isinstance(icon, str), (
|
||||
"Invalid data type for icon, supported : None, string")
|
||||
|
||||
# create new action
|
||||
script_action = action.Action(parent)
|
||||
script_action.setText(title)
|
||||
script_action.setObjectName(title)
|
||||
script_action.tags = tags
|
||||
|
||||
# link action to root for callback library
|
||||
script_action.root = self
|
||||
|
||||
# Set up the command
|
||||
script_action.sourcetype = sourcetype
|
||||
script_action.command = command
|
||||
|
||||
try:
|
||||
script_action.process_command()
|
||||
except RuntimeError as e:
|
||||
raise RuntimeError("Script action can't be "
|
||||
"processed: {}".format(e))
|
||||
|
||||
if icon:
|
||||
iconfile = os.path.expandvars(icon)
|
||||
script_action.iconfile = iconfile
|
||||
script_action_icon = QtWidgets.QIcon(iconfile)
|
||||
script_action.setIcon(script_action_icon)
|
||||
|
||||
if label:
|
||||
script_action.label = label
|
||||
|
||||
if tooltip:
|
||||
script_action.setStatusTip(tooltip)
|
||||
|
||||
script_action.triggered.connect(script_action.run_command)
|
||||
parent.addAction(script_action)
|
||||
|
||||
# Add to our searchable actions
|
||||
self._script_actions.append(script_action)
|
||||
|
||||
return script_action
|
||||
|
||||
def build_from_configuration(self, parent, configuration):
|
||||
"""Process the configurations and store the configuration
|
||||
|
||||
This creates all submenus from a configuration.json file.
|
||||
|
||||
When the configuration holds the key `main` all scripts under `main` will
|
||||
be added to the main menu first before adding the rest
|
||||
|
||||
Args:
|
||||
parent (ScriptsMenu): script menu instance
|
||||
configuration (list): A ScriptsMenu configuration list
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
for item in configuration:
|
||||
assert isinstance(item, dict), "Configuration is wrong!"
|
||||
|
||||
# skip items which have no `type` key
|
||||
item_type = item.get('type', None)
|
||||
if not item_type:
|
||||
log.warning("Missing 'type' from configuration item")
|
||||
continue
|
||||
|
||||
# add separator
|
||||
# Special behavior for separators
|
||||
if item_type == "separator":
|
||||
parent.addSeparator()
|
||||
|
||||
# add submenu
|
||||
# items should hold a collection of submenu items (dict)
|
||||
elif item_type == "menu":
|
||||
assert "items" in item, "Menu is missing 'items' key"
|
||||
menu = self.add_menu(parent=parent, title=item["title"])
|
||||
self.build_from_configuration(menu, item["items"])
|
||||
|
||||
# add script
|
||||
elif item_type == "action":
|
||||
# filter out `type` from the item dict
|
||||
config = {key: value for key, value in
|
||||
item.items() if key != "type"}
|
||||
|
||||
self.add_script(parent=parent, **config)
|
||||
|
||||
def set_update_visible(self, state):
|
||||
self.update_action.setVisible(state)
|
||||
|
||||
def clear_menu(self):
|
||||
"""Clear all menu items which are not default
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
# TODO: Set up a more robust implementation for this
|
||||
# Delete all except the first three actions
|
||||
for _action in self.actions()[3:]:
|
||||
self.removeAction(_action)
|
||||
|
||||
def register_callback(self, modifiers, callback):
|
||||
self._callbacks[modifiers].append(callback)
|
||||
|
||||
def _update_search(self, search):
|
||||
"""Hide all the samples which do not match the user's import
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
if not search:
|
||||
for action in self._script_actions:
|
||||
action.setVisible(True)
|
||||
else:
|
||||
for action in self._script_actions:
|
||||
if not action.has_tag(search.lower()):
|
||||
action.setVisible(False)
|
||||
|
||||
# Set visibility for all submenus
|
||||
for action in self.actions():
|
||||
if not action.menu():
|
||||
continue
|
||||
|
||||
menu = action.menu()
|
||||
visible = any(action.isVisible() for action in menu.actions())
|
||||
action.setVisible(visible)
|
||||
|
||||
|
||||
def load_configuration(path):
|
||||
"""Load the configuration from a file
|
||||
|
||||
Read out the JSON file which will dictate the structure of the scripts menu
|
||||
|
||||
Args:
|
||||
path (str): file path of the .JSON file
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
"""
|
||||
|
||||
if not os.path.isfile(path):
|
||||
raise AttributeError("Given configuration is not "
|
||||
"a file!\n'{}'".format(path))
|
||||
|
||||
extension = os.path.splitext(path)[-1]
|
||||
if extension != ".json":
|
||||
raise AttributeError("Given configuration file has unsupported "
|
||||
"file type, provide a .json file")
|
||||
|
||||
# retrieve and store config
|
||||
with open(path, "r") as f:
|
||||
configuration = json.load(f)
|
||||
|
||||
return configuration
|
||||
|
||||
|
||||
def application(configuration, parent):
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
scriptsmenu = ScriptsMenu(configuration, parent)
|
||||
scriptsmenu.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
1989
openpype/vendor/python/common/scriptsmenu/vendor/Qt.py
vendored
Normal file
1989
openpype/vendor/python/common/scriptsmenu/vendor/Qt.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
0
openpype/vendor/python/common/scriptsmenu/vendor/__init__.py
vendored
Normal file
0
openpype/vendor/python/common/scriptsmenu/vendor/__init__.py
vendored
Normal file
9
openpype/vendor/python/common/scriptsmenu/version.py
vendored
Normal file
9
openpype/vendor/python/common/scriptsmenu/version.py
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
VERSION_MAJOR = 1
|
||||
VERSION_MINOR = 5
|
||||
VERSION_PATCH = 1
|
||||
|
||||
|
||||
version = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)
|
||||
__version__ = version
|
||||
|
||||
__all__ = ['version', '__version__']
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.3.0-nightly.5"
|
||||
__version__ = "3.3.0-nightly.7"
|
||||
|
|
|
|||
|
|
@ -80,17 +80,6 @@ function Show-PSWarning() {
|
|||
}
|
||||
}
|
||||
|
||||
function Install-Poetry() {
|
||||
Write-Host ">>> " -NoNewline -ForegroundColor Green
|
||||
Write-Host "Installing Poetry ... "
|
||||
$python = "python"
|
||||
if (Get-Command "pyenv" -ErrorAction SilentlyContinue) {
|
||||
$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) -
|
||||
}
|
||||
|
||||
$art = @"
|
||||
|
||||
. . .. . ..
|
||||
|
|
|
|||
|
|
@ -62,9 +62,12 @@ function Test-Python() {
|
|||
Write-Host "Detecting host Python ... " -NoNewline
|
||||
$python = "python"
|
||||
if (Get-Command "pyenv" -ErrorAction SilentlyContinue) {
|
||||
$python = & pyenv which python
|
||||
$pyenv_python = & pyenv which python
|
||||
if (Test-Path -PathType Leaf -Path "$($pyenv_python)") {
|
||||
$python = $pyenv_python
|
||||
}
|
||||
}
|
||||
if (-not (Get-Command "python3" -ErrorAction SilentlyContinue)) {
|
||||
if (-not (Get-Command $python -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "!!! Python not detected" -ForegroundColor red
|
||||
Set-Location -Path $current_dir
|
||||
Exit-WithCode 1
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ title: Maya
|
|||
sidebar_label: Maya
|
||||
---
|
||||
|
||||
## Maya
|
||||
## Publish Plugins
|
||||
|
||||
### Publish Plugins
|
||||
### Render Settings Validator
|
||||
|
||||
#### Render Settings Validator (`ValidateRenderSettings`)
|
||||
`ValidateRenderSettings`
|
||||
|
||||
Render Settings Validator is here to make sure artists will submit renders
|
||||
we correct settings. Some of these settings are needed by OpenPype but some
|
||||
|
|
@ -49,4 +49,49 @@ Arnolds Camera (AA) samples to 6.
|
|||
Note that `aiOptions` is not the name of node but rather its type. For renderers there is usually
|
||||
just one instance of this node type but if that is not so, validator will go through all its
|
||||
instances and check the value there. Node type for **VRay** settings is `VRaySettingsNode`, for **Renderman**
|
||||
it is `rmanGlobals`, for **Redshift** it is `RedshiftOptions`.
|
||||
it is `rmanGlobals`, for **Redshift** it is `RedshiftOptions`.
|
||||
|
||||
### Model Name Validator
|
||||
|
||||
`ValidateRenderSettings`
|
||||
|
||||
This validator can enforce specific names for model members. It will check them against **Validation Regex**.
|
||||
There is special group in that regex - **shader**. If present, it will take that part of the name as shader name
|
||||
and it will compare it with list of shaders defined either in file name specified in **Material File** or from
|
||||
database file that is per project and can be directly edited from Maya's *OpenPype Tools > Edit Shader name definitions* when
|
||||
**Use database shader name definitions** is on. This list defines simply as one shader name per line.
|
||||
|
||||

|
||||
|
||||
For example - you are using default regex `(.*)_(\d)*_(?P<shader>.*)_(GEO)` and you have two shaders defined
|
||||
in either file or database `foo` and `bar`.
|
||||
|
||||
Object named `SomeCube_0001_foo_GEO` will pass but `SomeCube_GEO` will not and `SomeCube_001_xxx_GEO` will not too.
|
||||
|
||||
#### Top level group name
|
||||
There is a validation for top level group name too. You can specify whatever regex you'd like to use. Default will
|
||||
pass everything with `_GRP` suffix. You can use *named capturing groups* to validate against specific data. If you
|
||||
put `(?P<asset>.*)` it will try to match everything captured in that group against current asset name. Likewise you can
|
||||
use it for **subset** and **project** - `(?P<subset>.*)` and `(?P<project>.*)`.
|
||||
|
||||
**Example**
|
||||
|
||||
You are working on asset (shot) `0030_OGC_0190`. You have this regex in **Top level group name**:
|
||||
```regexp
|
||||
.*?_(?P<asset>.*)_GRP
|
||||
```
|
||||
|
||||
When you publish your model with top group named like `foo_GRP` it will fail. But with `foo_0030_OGC_0190_GRP` it will pass.
|
||||
|
||||
:::info About regex
|
||||
All regexes used here are in Python variant.
|
||||
:::
|
||||
|
||||
## Custom Menu
|
||||
You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**.
|
||||

|
||||
|
||||
:::note Work in progress
|
||||
This is still work in progress. Menu definition will be handled more friendly with widgets and not
|
||||
raw json.
|
||||
:::
|
||||
BIN
website/docs/assets/maya-admin_model_name_validator.png
Normal file
BIN
website/docs/assets/maya-admin_model_name_validator.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
website/docs/assets/maya-admin_scriptsmenu.png
Normal file
BIN
website/docs/assets/maya-admin_scriptsmenu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Loading…
Add table
Add a link
Reference in a new issue