Merge branch 'develop' into deature/OP-2839_Basic-event-system

This commit is contained in:
Jakub Trllo 2022-03-08 11:06:08 +01:00
commit 7bcc0c8688
14 changed files with 129 additions and 1479 deletions

View file

@ -1,6 +1,6 @@
# Changelog
## [3.9.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD)
## [3.9.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.2...HEAD)
@ -12,56 +12,56 @@
- Documentation: fixed broken links [\#2799](https://github.com/pypeclub/OpenPype/pull/2799)
- Documentation: broken link fix [\#2785](https://github.com/pypeclub/OpenPype/pull/2785)
- Documentation: link fixes [\#2772](https://github.com/pypeclub/OpenPype/pull/2772)
- Update docusaurus to latest version [\#2760](https://github.com/pypeclub/OpenPype/pull/2760)
- Various testing updates [\#2726](https://github.com/pypeclub/OpenPype/pull/2726)
**🚀 Enhancements**
- Ftrack: Can sync fps as string [\#2836](https://github.com/pypeclub/OpenPype/pull/2836)
- General: Color dialog UI fixes [\#2817](https://github.com/pypeclub/OpenPype/pull/2817)
- Nuke: adding Reformat to baking mov plugin [\#2811](https://github.com/pypeclub/OpenPype/pull/2811)
- Manager: Update all to latest button [\#2805](https://github.com/pypeclub/OpenPype/pull/2805)
- General: Set context environments for non host applications [\#2803](https://github.com/pypeclub/OpenPype/pull/2803)
- Houdini: Remove duplicate ValidateOutputNode plug-in [\#2780](https://github.com/pypeclub/OpenPype/pull/2780)
- Tray publisher: New Tray Publisher host \(beta\) [\#2778](https://github.com/pypeclub/OpenPype/pull/2778)
- Slack: Added regex for filtering on subset names [\#2775](https://github.com/pypeclub/OpenPype/pull/2775)
- Houdini: Implement Reset Frame Range [\#2770](https://github.com/pypeclub/OpenPype/pull/2770)
- Pyblish Pype: Remove redundant new line in installed fonts printing [\#2758](https://github.com/pypeclub/OpenPype/pull/2758)
- Flame: use Shot Name on segment for asset name [\#2751](https://github.com/pypeclub/OpenPype/pull/2751)
- Flame: adding validator source clip [\#2746](https://github.com/pypeclub/OpenPype/pull/2746)
- Ftrack: Disable ftrack module by default [\#2732](https://github.com/pypeclub/OpenPype/pull/2732)
- Houdini: Move Houdini Save Current File to beginning of ExtractorOrder [\#2747](https://github.com/pypeclub/OpenPype/pull/2747)
- RoyalRender: Minor enhancements [\#2700](https://github.com/pypeclub/OpenPype/pull/2700)
**🐛 Bug fixes**
- Maya: Stop creation of reviews for Cryptomattes [\#2832](https://github.com/pypeclub/OpenPype/pull/2832)
- Deadline: Remove recreated event [\#2828](https://github.com/pypeclub/OpenPype/pull/2828)
- Deadline: Added missing events folder [\#2827](https://github.com/pypeclub/OpenPype/pull/2827)
- Settings: Missing document with OP versions may break start of OpenPype [\#2825](https://github.com/pypeclub/OpenPype/pull/2825)
- Deadline: more detailed temp file name for environment json [\#2824](https://github.com/pypeclub/OpenPype/pull/2824)
- General: Host name was formed from obsolete code [\#2821](https://github.com/pypeclub/OpenPype/pull/2821)
- Settings UI: Fix "Apply from" action [\#2820](https://github.com/pypeclub/OpenPype/pull/2820)
- Ftrack: Job killer with missing user [\#2819](https://github.com/pypeclub/OpenPype/pull/2819)
- StandalonePublisher: use dynamic groups in subset names [\#2816](https://github.com/pypeclub/OpenPype/pull/2816)
- Settings UI: Search case sensitivity [\#2810](https://github.com/pypeclub/OpenPype/pull/2810)
- Flame Babypublisher optimalization [\#2806](https://github.com/pypeclub/OpenPype/pull/2806)
- resolve: fixing fusion module loading [\#2802](https://github.com/pypeclub/OpenPype/pull/2802)
- Ftrack: Unset task ids from asset versions before tasks are removed [\#2800](https://github.com/pypeclub/OpenPype/pull/2800)
- Slack: fail gracefully if slack exception [\#2798](https://github.com/pypeclub/OpenPype/pull/2798)
- Flame: Fix version string in default settings [\#2783](https://github.com/pypeclub/OpenPype/pull/2783)
- After Effects: Fix typo in name `afftereffects` -\> `aftereffects` [\#2768](https://github.com/pypeclub/OpenPype/pull/2768)
- Avoid renaming udim indexes [\#2765](https://github.com/pypeclub/OpenPype/pull/2765)
- Houdini: Fix open last workfile [\#2767](https://github.com/pypeclub/OpenPype/pull/2767)
- Maya: Fix `unique\_namespace` when in an namespace that is empty [\#2759](https://github.com/pypeclub/OpenPype/pull/2759)
- Loader UI: Fix right click in representation widget [\#2757](https://github.com/pypeclub/OpenPype/pull/2757)
- Aftereffects 2022 and Deadline [\#2748](https://github.com/pypeclub/OpenPype/pull/2748)
- Flame: bunch of bugs [\#2745](https://github.com/pypeclub/OpenPype/pull/2745)
- Maya: Save current scene on workfile publish [\#2744](https://github.com/pypeclub/OpenPype/pull/2744)
- Version Up: Preserve parts of filename after version number \(like subversion\) on version\_up [\#2741](https://github.com/pypeclub/OpenPype/pull/2741)
- Maya: Remove some unused code [\#2709](https://github.com/pypeclub/OpenPype/pull/2709)
- Multiple hosts: unify menu style across hosts [\#2693](https://github.com/pypeclub/OpenPype/pull/2693)
**Merged pull requests:**
- General: Move change context functions [\#2839](https://github.com/pypeclub/OpenPype/pull/2839)
- Tools: Don't use avalon tools code [\#2829](https://github.com/pypeclub/OpenPype/pull/2829)
- Move Unreal Implementation to OpenPype [\#2823](https://github.com/pypeclub/OpenPype/pull/2823)
- Ftrack: Job killer with missing user [\#2819](https://github.com/pypeclub/OpenPype/pull/2819)
- Ftrack: Unset task ids from asset versions before tasks are removed [\#2800](https://github.com/pypeclub/OpenPype/pull/2800)
- Slack: fail gracefully if slack exception [\#2798](https://github.com/pypeclub/OpenPype/pull/2798)
- Nuke: Use AVALON\_APP to get value for "app" key [\#2818](https://github.com/pypeclub/OpenPype/pull/2818)
- Ftrack: Moved module one hierarchy level higher [\#2792](https://github.com/pypeclub/OpenPype/pull/2792)
- SyncServer: Moved module one hierarchy level higher [\#2791](https://github.com/pypeclub/OpenPype/pull/2791)
- Royal render: Move module one hierarchy level higher [\#2790](https://github.com/pypeclub/OpenPype/pull/2790)
- Deadline: Move module one hierarchy level higher [\#2789](https://github.com/pypeclub/OpenPype/pull/2789)
- Houdini: Remove duplicate ValidateOutputNode plug-in [\#2780](https://github.com/pypeclub/OpenPype/pull/2780)
- Slack: Added regex for filtering on subset names [\#2775](https://github.com/pypeclub/OpenPype/pull/2775)
- Houdini: Fix open last workfile [\#2767](https://github.com/pypeclub/OpenPype/pull/2767)
- General: Extract template formatting from anatomy [\#2766](https://github.com/pypeclub/OpenPype/pull/2766)
- Harmony: Rendering in Deadline didn't work in other machines than submitter [\#2754](https://github.com/pypeclub/OpenPype/pull/2754)
- Houdini: Move Houdini Save Current File to beginning of ExtractorOrder [\#2747](https://github.com/pypeclub/OpenPype/pull/2747)
- Maya: set Deadline job/batch name to original source workfile name instead of published workfile [\#2733](https://github.com/pypeclub/OpenPype/pull/2733)
## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07)

View file

@ -50,6 +50,10 @@ class ExtractCamera(api.Extractor):
filepath=filepath,
use_active_collection=False,
use_selection=True,
bake_anim_use_nla_strips=False,
bake_anim_use_all_actions=False,
add_leaf_bones=False,
armature_nodetype='ROOT',
object_types={'CAMERA'},
bake_anim_simplify_factor=0.0
)

View file

@ -4,7 +4,6 @@ import os
import sys
import json
import tempfile
import platform
import contextlib
import subprocess
from collections import OrderedDict
@ -64,10 +63,6 @@ def maketx(source, destination, *args):
maketx_path = get_oiio_tools_path("maketx")
if platform.system().lower() == "windows":
# Ensure .exe extension
maketx_path += ".exe"
if not os.path.exists(maketx_path):
print(
"OIIO tool not found in {}".format(maketx_path))

View file

@ -20,6 +20,16 @@ from .events import (
emit_event,
register_event_callback
)
from .vendor_bin_utils import (
find_executable,
get_vendor_bin_path,
get_oiio_tools_path,
get_ffmpeg_tool_path,
ffprobe_streams,
is_oiio_supported
)
from .env_tools import (
env_value_to_bool,
get_paths_from_environ,
@ -61,14 +71,6 @@ from .anatomy import (
from .config import get_datetime_data
from .vendor_bin_utils import (
get_vendor_bin_path,
get_oiio_tools_path,
get_ffmpeg_tool_path,
ffprobe_streams,
is_oiio_supported
)
from .python_module_tools import (
import_filepath,
modules_from_path,
@ -200,6 +202,7 @@ __all__ = [
"emit_event",
"register_event_callback",
"find_executable",
"get_openpype_execute_args",
"get_pype_execute_args",
"get_linux_launcher_args",

View file

@ -7,7 +7,6 @@ import platform
import collections
import inspect
import subprocess
import distutils.spawn
from abc import ABCMeta, abstractmethod
import six
@ -36,8 +35,10 @@ from .python_module_tools import (
modules_from_path,
classes_from_module
)
from .execute import get_linux_launcher_args
from .execute import (
find_executable,
get_linux_launcher_args
)
_logger = None
@ -647,7 +648,7 @@ class ApplicationExecutable:
def _realpath(self):
"""Check if path is valid executable path."""
# Check for executable in PATH
result = distutils.spawn.find_executable(self.executable_path)
result = find_executable(self.executable_path)
if result is not None:
return result

View file

@ -4,9 +4,9 @@ import subprocess
import platform
import json
import tempfile
import distutils.spawn
from .log import PypeLogger as Logger
from .vendor_bin_utils import find_executable
# MSDN process creation flag (Windows only)
CREATE_NO_WINDOW = 0x08000000
@ -341,7 +341,7 @@ def get_linux_launcher_args(*args):
os.path.dirname(openpype_executable),
filename
)
executable_path = distutils.spawn.find_executable(new_executable)
executable_path = find_executable(new_executable)
if executable_path is None:
return None
launch_args = [executable_path]

View file

@ -3,9 +3,87 @@ import logging
import json
import platform
import subprocess
import distutils
log = logging.getLogger("FFmpeg utils")
log = logging.getLogger("Vendor utils")
def is_file_executable(filepath):
"""Filepath lead to executable file.
Args:
filepath(str): Full path to file.
"""
if not filepath:
return False
if os.path.isfile(filepath):
if os.access(filepath, os.X_OK):
return True
log.info(
"Filepath is not available for execution \"{}\"".format(filepath)
)
return False
def find_executable(executable):
"""Find full path to executable.
Also tries additional extensions if passed executable does not contain one.
Paths where it is looked for executable is defined by 'PATH' environment
variable, 'os.confstr("CS_PATH")' or 'os.defpath'.
Args:
executable(str): Name of executable with or without extension. Can be
path to file.
Returns:
str: Full path to executable with extension (is file).
None: When the executable was not found.
"""
# Skip if passed path is file
if is_file_executable(executable):
return executable
low_platform = platform.system().lower()
_, ext = os.path.splitext(executable)
# Prepare variants for which it will be looked
variants = [executable]
# Add other extension variants only if passed executable does not have one
if not ext:
if low_platform == "windows":
exts = [".exe", ".ps1", ".bat"]
for ext in os.getenv("PATHEXT", "").split(os.pathsep):
ext = ext.lower()
if ext and ext not in exts:
exts.append(ext)
else:
exts = [".sh"]
for ext in exts:
variant = executable + ext
if is_file_executable(variant):
return variant
variants.append(variant)
# Get paths where to look for executable
path_str = os.environ.get("PATH", None)
if path_str is None:
if hasattr(os, "confstr"):
path_str = os.confstr("CS_PATH")
elif hasattr(os, "defpath"):
path_str = os.defpath
if path_str:
paths = path_str.split(os.pathsep)
for path in paths:
for variant in variants:
filepath = os.path.abspath(os.path.join(path, variant))
if is_file_executable(filepath):
return filepath
return None
def get_vendor_bin_path(bin_app):
@ -41,11 +119,7 @@ def get_oiio_tools_path(tool="oiiotool"):
Default is "oiiotool".
"""
oiio_dir = get_vendor_bin_path("oiio")
if platform.system().lower() == "windows" and not tool.lower().endswith(
".exe"
):
tool = "{}.exe".format(tool)
return os.path.join(oiio_dir, tool)
return find_executable(os.path.join(oiio_dir, tool))
def get_ffmpeg_tool_path(tool="ffmpeg"):
@ -61,7 +135,7 @@ def get_ffmpeg_tool_path(tool="ffmpeg"):
ffmpeg_dir = get_vendor_bin_path("ffmpeg")
if platform.system().lower() == "windows":
ffmpeg_dir = os.path.join(ffmpeg_dir, "bin")
return os.path.join(ffmpeg_dir, tool)
return find_executable(os.path.join(ffmpeg_dir, tool))
def ffprobe_streams(path_to_file, logger=None):
@ -122,7 +196,7 @@ def is_oiio_supported():
"""
loaded_path = oiio_path = get_oiio_tools_path()
if oiio_path:
oiio_path = distutils.spawn.find_executable(oiio_path)
oiio_path = find_executable(oiio_path)
if not oiio_path:
log.debug("OIIOTool is not configured or not present at {}".format(

View file

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

View file

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

View file

@ -1,654 +0,0 @@
import os
import sys
from subprocess import Popen
import ftrack_api
from Qt import QtWidgets, QtCore
from openpype import style
from openpype.api import get_current_project_settings
from openpype.lib.avalon_context import update_current_task
from openpype.tools.utils.lib import qt_app_context
from avalon import io, api, schema
from . import widget, model
module = sys.modules[__name__]
module.window = None
class Window(QtWidgets.QDialog):
"""Asset creator interface
"""
def __init__(self, parent=None, context=None):
super(Window, self).__init__(parent)
self.context = context
project_name = io.active_project()
self.setWindowTitle("Asset creator ({0})".format(project_name))
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
# Validators
self.valid_parent = False
self.session = None
# assets widget
assets_widget = QtWidgets.QWidget()
assets_widget.setContentsMargins(0, 0, 0, 0)
assets_layout = QtWidgets.QVBoxLayout(assets_widget)
assets = widget.AssetWidget()
assets.view.setSelectionMode(assets.view.ExtendedSelection)
assets_layout.addWidget(assets)
# Outlink
label_outlink = QtWidgets.QLabel("Outlink:")
input_outlink = QtWidgets.QLineEdit()
input_outlink.setReadOnly(True)
input_outlink.setStyleSheet("background-color: #333333;")
checkbox_outlink = QtWidgets.QCheckBox("Use outlink")
# Parent
label_parent = QtWidgets.QLabel("*Parent:")
input_parent = QtWidgets.QLineEdit()
input_parent.setReadOnly(True)
input_parent.setStyleSheet("background-color: #333333;")
# Name
label_name = QtWidgets.QLabel("*Name:")
input_name = QtWidgets.QLineEdit()
input_name.setPlaceholderText("<asset name>")
# Asset Build
label_assetbuild = QtWidgets.QLabel("Asset Build:")
combo_assetbuilt = QtWidgets.QComboBox()
# Task template
label_task_template = QtWidgets.QLabel("Task template:")
combo_task_template = QtWidgets.QComboBox()
# Info widget
info_widget = QtWidgets.QWidget()
info_widget.setContentsMargins(10, 10, 10, 10)
info_layout = QtWidgets.QVBoxLayout(info_widget)
# Inputs widget
inputs_widget = QtWidgets.QWidget()
inputs_widget.setContentsMargins(0, 0, 0, 0)
inputs_layout = QtWidgets.QFormLayout(inputs_widget)
inputs_layout.addRow(label_outlink, input_outlink)
inputs_layout.addRow(None, checkbox_outlink)
inputs_layout.addRow(label_parent, input_parent)
inputs_layout.addRow(label_name, input_name)
inputs_layout.addRow(label_assetbuild, combo_assetbuilt)
inputs_layout.addRow(label_task_template, combo_task_template)
# Add button
btns_widget = QtWidgets.QWidget()
btns_widget.setContentsMargins(0, 0, 0, 0)
btn_layout = QtWidgets.QHBoxLayout(btns_widget)
btn_create_asset = QtWidgets.QPushButton("Create asset")
btn_create_asset.setToolTip(
"Creates all necessary components for asset"
)
checkbox_app = None
if self.context is not None:
checkbox_app = QtWidgets.QCheckBox("Open {}".format(
self.context.capitalize())
)
btn_layout.addWidget(checkbox_app)
btn_layout.addWidget(btn_create_asset)
task_view = QtWidgets.QTreeView()
task_view.setIndentation(0)
task_model = model.TasksModel()
task_view.setModel(task_model)
info_layout.addWidget(inputs_widget)
info_layout.addWidget(task_view)
info_layout.addWidget(btns_widget)
# Body
body = QtWidgets.QSplitter()
body.setContentsMargins(0, 0, 0, 0)
body.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
body.setOrientation(QtCore.Qt.Horizontal)
body.addWidget(assets_widget)
body.addWidget(info_widget)
body.setStretchFactor(0, 100)
body.setStretchFactor(1, 150)
# statusbar
message = QtWidgets.QLabel()
message.setFixedHeight(20)
statusbar = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(statusbar)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(message)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(body)
layout.addWidget(statusbar)
self.data = {
"label": {
"message": message,
},
"view": {
"tasks": task_view
},
"model": {
"assets": assets,
"tasks": task_model
},
"inputs": {
"outlink": input_outlink,
"outlink_cb": checkbox_outlink,
"parent": input_parent,
"name": input_name,
"assetbuild": combo_assetbuilt,
"tasktemplate": combo_task_template,
"open_app": checkbox_app
},
"buttons": {
"create_asset": btn_create_asset
}
}
# signals
btn_create_asset.clicked.connect(self.create_asset)
assets.selection_changed.connect(self.on_asset_changed)
input_name.textChanged.connect(self.on_asset_name_change)
checkbox_outlink.toggled.connect(self.on_outlink_checkbox_change)
combo_task_template.currentTextChanged.connect(
self.on_task_template_changed
)
if self.context is not None:
checkbox_app.toggled.connect(self.on_app_checkbox_change)
# on start
self.on_start()
self.resize(600, 500)
self.echo("Connected to project: {0}".format(project_name))
def open_app(self):
if self.context == 'maya':
Popen("maya")
else:
message = QtWidgets.QMessageBox(self)
message.setWindowTitle("App is not set")
message.setIcon(QtWidgets.QMessageBox.Critical)
message.show()
def on_start(self):
project_name = io.Session['AVALON_PROJECT']
project_query = 'Project where full_name is "{}"'.format(project_name)
if self.session is None:
session = ftrack_api.Session()
self.session = session
else:
session = self.session
ft_project = session.query(project_query).one()
schema_name = ft_project['project_schema']['name']
# Load config
schemas_items = get_current_project_settings().get('ftrack', {}).get(
'project_schemas', {}
)
# Get info if it is silo project
self.silos = io.distinct("silo")
if self.silos and None in self.silos:
self.silos = None
key = "default"
if schema_name in schemas_items:
key = schema_name
self.config_data = schemas_items[key]
# set outlink
input_outlink = self.data['inputs']['outlink']
checkbox_outlink = self.data['inputs']['outlink_cb']
outlink_text = io.Session.get('AVALON_ASSET', '')
checkbox_outlink.setChecked(True)
if outlink_text == '':
outlink_text = '< No context >'
checkbox_outlink.setChecked(False)
checkbox_outlink.hide()
input_outlink.setText(outlink_text)
# load asset build types
self.load_assetbuild_types()
# Load task templates
self.load_task_templates()
self.data["model"]["assets"].refresh()
self.on_asset_changed()
def create_asset(self):
name_input = self.data['inputs']['name']
name = name_input.text()
test_name = name.replace(' ', '')
error_message = None
message = QtWidgets.QMessageBox(self)
message.setWindowTitle("Some errors have occurred")
message.setIcon(QtWidgets.QMessageBox.Critical)
# TODO: show error messages on any error
if self.valid_parent is not True and test_name == '':
error_message = "Name is not set and Parent is not selected"
elif self.valid_parent is not True:
error_message = "Parent is not selected"
elif test_name == '':
error_message = "Name is not set"
if error_message is not None:
message.setText(error_message)
message.show()
return
test_name_exists = io.find({
'type': 'asset',
'name': name
})
existing_assets = [x for x in test_name_exists]
if len(existing_assets) > 0:
message.setText("Entered Asset name is occupied")
message.show()
return
checkbox_app = self.data['inputs']['open_app']
if checkbox_app is not None and checkbox_app.isChecked() is True:
task_view = self.data["view"]["tasks"]
task_model = self.data["model"]["tasks"]
try:
index = task_view.selectedIndexes()[0]
task_name = task_model.itemData(index)[0]
except Exception:
message.setText("Please select task")
message.show()
return
# Get ftrack session
if self.session is None:
session = ftrack_api.Session()
self.session = session
else:
session = self.session
# Get Ftrack project entity
project_name = io.Session['AVALON_PROJECT']
project_query = 'Project where full_name is "{}"'.format(project_name)
try:
ft_project = session.query(project_query).one()
except Exception:
message.setText("Ftrack project was not found")
message.show()
return
# Get Ftrack entity of parent
ft_parent = None
assets_model = self.data["model"]["assets"]
selected = assets_model.get_selected_assets()
parent = io.find_one({"_id": selected[0], "type": "asset"})
asset_id = parent.get('data', {}).get('ftrackId', None)
asset_entity_type = parent.get('data', {}).get('entityType', None)
asset_query = '{} where id is "{}"'
if asset_id is not None and asset_entity_type is not None:
try:
ft_parent = session.query(asset_query.format(
asset_entity_type, asset_id)
).one()
except Exception:
ft_parent = None
if ft_parent is None:
ft_parent = self.get_ftrack_asset(parent, ft_project)
if ft_parent is None:
message.setText("Parent's Ftrack entity was not found")
message.show()
return
asset_build_combo = self.data['inputs']['assetbuild']
asset_type_name = asset_build_combo.currentText()
asset_type_query = 'Type where name is "{}"'.format(asset_type_name)
try:
asset_type = session.query(asset_type_query).one()
except Exception:
message.setText("Selected Asset Build type does not exists")
message.show()
return
for children in ft_parent['children']:
if children['name'] == name:
message.setText("Entered Asset name is occupied")
message.show()
return
task_template_combo = self.data['inputs']['tasktemplate']
task_template = task_template_combo.currentText()
tasks = []
for template in self.config_data['task_templates']:
if template['name'] == task_template:
tasks = template['task_types']
break
available_task_types = []
task_types = ft_project['project_schema']['_task_type_schema']
for task_type in task_types['types']:
available_task_types.append(task_type['name'])
not_possible_tasks = []
for task in tasks:
if task not in available_task_types:
not_possible_tasks.append(task)
if len(not_possible_tasks) != 0:
message.setText((
"These Task types weren't found"
" in Ftrack project schema:\n{}").format(
', '.join(not_possible_tasks))
)
message.show()
return
# Create asset build
asset_build_data = {
'name': name,
'project_id': ft_project['id'],
'parent_id': ft_parent['id'],
'type': asset_type
}
new_entity = session.create('AssetBuild', asset_build_data)
task_data = {
'project_id': ft_project['id'],
'parent_id': new_entity['id']
}
for task in tasks:
type = session.query('Type where name is "{}"'.format(task)).one()
task_data['type_id'] = type['id']
task_data['name'] = task
session.create('Task', task_data)
av_project = io.find_one({'type': 'project'})
hiearchy_items = []
hiearchy_items.extend(self.get_avalon_parent(parent))
hiearchy_items.append(parent['name'])
hierarchy = os.path.sep.join(hiearchy_items)
new_asset_data = {
'ftrackId': new_entity['id'],
'entityType': new_entity.entity_type,
'visualParent': parent['_id'],
'tasks': tasks,
'parents': hiearchy_items,
'hierarchy': hierarchy
}
new_asset_info = {
'parent': av_project['_id'],
'name': name,
'schema': "openpype:asset-3.0",
'type': 'asset',
'data': new_asset_data
}
# Backwards compatibility (add silo from parent if is silo project)
if self.silos:
new_asset_info["silo"] = parent["silo"]
try:
schema.validate(new_asset_info)
except Exception:
message.setText((
'Asset information are not valid'
' to create asset in avalon database'
))
message.show()
session.rollback()
return
io.insert_one(new_asset_info)
session.commit()
outlink_cb = self.data['inputs']['outlink_cb']
if outlink_cb.isChecked() is True:
outlink_input = self.data['inputs']['outlink']
outlink_name = outlink_input.text()
outlink_asset = io.find_one({
'type': 'asset',
'name': outlink_name
})
outlink_ft_id = outlink_asset.get('data', {}).get('ftrackId', None)
outlink_entity_type = outlink_asset.get(
'data', {}
).get('entityType', None)
if outlink_ft_id is not None and outlink_entity_type is not None:
try:
outlink_entity = session.query(asset_query.format()).one()
except Exception:
outlink_entity = None
if outlink_entity is None:
outlink_entity = self.get_ftrack_asset(
outlink_asset, ft_project
)
if outlink_entity is None:
message.setText("Outlink's Ftrack entity was not found")
message.show()
return
link_data = {
'from_id': new_entity['id'],
'to_id': outlink_entity['id']
}
session.create('TypedContextLink', link_data)
session.commit()
if checkbox_app is not None and checkbox_app.isChecked() is True:
origin_asset = api.Session.get('AVALON_ASSET', None)
origin_task = api.Session.get('AVALON_TASK', None)
asset_name = name
task_view = self.data["view"]["tasks"]
task_model = self.data["model"]["tasks"]
try:
index = task_view.selectedIndexes()[0]
except Exception:
message.setText("No task is selected. App won't be launched")
message.show()
return
task_name = task_model.itemData(index)[0]
try:
update_current_task(task=task_name, asset=asset_name)
self.open_app()
finally:
if origin_task is not None and origin_asset is not None:
update_current_task(
task=origin_task, asset=origin_asset
)
message.setWindowTitle("Asset Created")
message.setText("Asset Created successfully")
message.setIcon(QtWidgets.QMessageBox.Information)
message.show()
def get_ftrack_asset(self, asset, ft_project):
parenthood = []
parenthood.extend(self.get_avalon_parent(asset))
parenthood.append(asset['name'])
parenthood = list(reversed(parenthood))
output_entity = None
ft_entity = ft_project
index = len(parenthood) - 1
while True:
name = parenthood[index]
found = False
for children in ft_entity['children']:
if children['name'] == name:
ft_entity = children
found = True
break
if found is False:
return None
if index == 0:
output_entity = ft_entity
break
index -= 1
return output_entity
def get_avalon_parent(self, entity):
parent_id = entity['data']['visualParent']
parents = []
if parent_id is not None:
parent = io.find_one({'_id': parent_id})
parents.extend(self.get_avalon_parent(parent))
parents.append(parent['name'])
return parents
def echo(self, message):
widget = self.data["label"]["message"]
widget.setText(str(message))
QtCore.QTimer.singleShot(5000, lambda: widget.setText(""))
print(message)
def load_task_templates(self):
templates = self.config_data.get('task_templates', [])
all_names = []
for template in templates:
all_names.append(template['name'])
tt_combobox = self.data['inputs']['tasktemplate']
tt_combobox.clear()
tt_combobox.addItems(all_names)
def load_assetbuild_types(self):
types = []
schemas = self.config_data.get('schemas', [])
for _schema in schemas:
if _schema['object_type'] == 'Asset Build':
types = _schema['task_types']
break
ab_combobox = self.data['inputs']['assetbuild']
ab_combobox.clear()
ab_combobox.addItems(types)
def on_app_checkbox_change(self):
task_model = self.data['model']['tasks']
app_checkbox = self.data['inputs']['open_app']
if app_checkbox.isChecked() is True:
task_model.selectable = True
else:
task_model.selectable = False
def on_outlink_checkbox_change(self):
checkbox_outlink = self.data['inputs']['outlink_cb']
outlink_input = self.data['inputs']['outlink']
if checkbox_outlink.isChecked() is True:
outlink_text = io.Session['AVALON_ASSET']
else:
outlink_text = '< Outlinks won\'t be set >'
outlink_input.setText(outlink_text)
def on_task_template_changed(self):
combobox = self.data['inputs']['tasktemplate']
task_model = self.data['model']['tasks']
name = combobox.currentText()
tasks = []
for template in self.config_data['task_templates']:
if template['name'] == name:
tasks = template['task_types']
break
task_model.set_tasks(tasks)
def on_asset_changed(self):
"""Callback on asset selection changed
This updates the task view.
"""
assets_model = self.data["model"]["assets"]
parent_input = self.data['inputs']['parent']
selected = assets_model.get_selected_assets()
self.valid_parent = False
if len(selected) > 1:
parent_input.setText('< Please select only one asset! >')
elif len(selected) == 1:
if isinstance(selected[0], io.ObjectId):
self.valid_parent = True
asset = io.find_one({"_id": selected[0], "type": "asset"})
parent_input.setText(asset['name'])
else:
parent_input.setText('< Selected invalid parent(silo) >')
else:
parent_input.setText('< Nothing is selected >')
self.creatability_check()
def on_asset_name_change(self):
self.creatability_check()
def creatability_check(self):
name_input = self.data['inputs']['name']
name = str(name_input.text()).strip()
creatable = False
if name and self.valid_parent:
creatable = True
self.data["buttons"]["create_asset"].setEnabled(creatable)
def show(parent=None, debug=False, context=None):
"""Display Loader GUI
Arguments:
debug (bool, optional): Run loader in debug-mode,
defaults to False
"""
try:
module.window.close()
del module.window
except (RuntimeError, AttributeError):
pass
if debug is True:
io.install()
with qt_app_context():
window = Window(parent, context)
window.setStyleSheet(style.load_stylesheet())
window.show()
module.window = window
def cli(args):
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("project")
parser.add_argument("asset")
args = parser.parse_args(args)
project = args.project
asset = args.asset
io.install()
api.Session["AVALON_PROJECT"] = project
if asset != '':
api.Session["AVALON_ASSET"] = asset
show()

View file

@ -1,310 +0,0 @@
import re
import logging
from Qt import QtCore, QtWidgets
from avalon.vendor import qtawesome
from avalon import io
from avalon import style
log = logging.getLogger(__name__)
class Item(dict):
"""An item that can be represented in a tree view using `TreeModel`.
The item can store data just like a regular dictionary.
>>> data = {"name": "John", "score": 10}
>>> item = Item(data)
>>> assert item["name"] == "John"
"""
def __init__(self, data=None):
super(Item, self).__init__()
self._children = list()
self._parent = None
if data is not None:
assert isinstance(data, dict)
self.update(data)
def childCount(self):
return len(self._children)
def child(self, row):
if row >= len(self._children):
log.warning("Invalid row as child: {0}".format(row))
return
return self._children[row]
def children(self):
return self._children
def parent(self):
return self._parent
def row(self):
"""
Returns:
int: Index of this item under parent"""
if self._parent is not None:
siblings = self.parent().children()
return siblings.index(self)
def add_child(self, child):
"""Add a child to this item"""
child._parent = self
self._children.append(child)
class TreeModel(QtCore.QAbstractItemModel):
Columns = list()
ItemRole = QtCore.Qt.UserRole + 1
def __init__(self, parent=None):
super(TreeModel, self).__init__(parent)
self._root_item = Item()
def rowCount(self, parent):
if parent.isValid():
item = parent.internalPointer()
else:
item = self._root_item
return item.childCount()
def columnCount(self, parent):
return len(self.Columns)
def data(self, index, role):
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
item = index.internalPointer()
column = index.column()
key = self.Columns[column]
return item.get(key, None)
if role == self.ItemRole:
return index.internalPointer()
def setData(self, index, value, role=QtCore.Qt.EditRole):
"""Change the data on the items.
Returns:
bool: Whether the edit was successful
"""
if index.isValid():
if role == QtCore.Qt.EditRole:
item = index.internalPointer()
column = index.column()
key = self.Columns[column]
item[key] = value
# passing `list()` for PyQt5 (see PYSIDE-462)
self.dataChanged.emit(index, index, list())
# must return true if successful
return True
return False
def setColumns(self, keys):
assert isinstance(keys, (list, tuple))
self.Columns = keys
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if section < len(self.Columns):
return self.Columns[section]
super(TreeModel, self).headerData(section, orientation, role)
def flags(self, index):
flags = QtCore.Qt.ItemIsEnabled
item = index.internalPointer()
if item.get("enabled", True):
flags |= QtCore.Qt.ItemIsSelectable
return flags
def parent(self, index):
item = index.internalPointer()
parent_item = item.parent()
# If it has no parents we return invalid
if parent_item == self._root_item or not parent_item:
return QtCore.QModelIndex()
return self.createIndex(parent_item.row(), 0, parent_item)
def index(self, row, column, parent):
"""Return index for row/column under parent"""
if not parent.isValid():
parent_item = self._root_item
else:
parent_item = parent.internalPointer()
child_item = parent_item.child(row)
if child_item:
return self.createIndex(row, column, child_item)
else:
return QtCore.QModelIndex()
def add_child(self, item, parent=None):
if parent is None:
parent = self._root_item
parent.add_child(item)
def column_name(self, column):
"""Return column key by index"""
if column < len(self.Columns):
return self.Columns[column]
def clear(self):
self.beginResetModel()
self._root_item = Item()
self.endResetModel()
class TasksModel(TreeModel):
"""A model listing the tasks combined for a list of assets"""
Columns = ["Tasks"]
def __init__(self):
super(TasksModel, self).__init__()
self._num_assets = 0
self._icons = {
"__default__": qtawesome.icon("fa.male",
color=style.colors.default),
"__no_task__": qtawesome.icon("fa.exclamation-circle",
color=style.colors.mid)
}
self._get_task_icons()
def _get_task_icons(self):
# Get the project configured icons from database
project = io.find_one({"type": "project"})
tasks = project["config"].get("tasks", [])
for task in tasks:
icon_name = task.get("icon", None)
if icon_name:
icon = qtawesome.icon("fa.{}".format(icon_name),
color=style.colors.default)
self._icons[task["name"]] = icon
def set_tasks(self, tasks):
"""Set assets to track by their database id
Arguments:
asset_ids (list): List of asset ids.
"""
self.clear()
# let cleared task view if no tasks are available
if len(tasks) == 0:
return
self.beginResetModel()
icon = self._icons["__default__"]
for task in tasks:
item = Item({
"Tasks": task,
"icon": icon
})
self.add_child(item)
self.endResetModel()
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, section, orientation, role):
# Override header for count column to show amount of assets
# it is listing the tasks for
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
if section == 1: # count column
return "count ({0})".format(self._num_assets)
return super(TasksModel, self).headerData(section, orientation, role)
def data(self, index, role):
if not index.isValid():
return
# Add icon to the first column
if role == QtCore.Qt.DecorationRole:
if index.column() == 0:
return index.internalPointer()["icon"]
return super(TasksModel, self).data(index, role)
class DeselectableTreeView(QtWidgets.QTreeView):
"""A tree view that deselects on clicking on an empty area in the view"""
def mousePressEvent(self, event):
index = self.indexAt(event.pos())
if not index.isValid():
# clear the selection
self.clearSelection()
# clear the current index
self.setCurrentIndex(QtCore.QModelIndex())
QtWidgets.QTreeView.mousePressEvent(self, event)
class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
"""Filters to the regex if any of the children matches allow parent"""
def filterAcceptsRow(self, row, parent):
regex = self.filterRegExp()
if not regex.isEmpty():
pattern = regex.pattern()
model = self.sourceModel()
source_index = model.index(row, self.filterKeyColumn(), parent)
if source_index.isValid():
# Check current index itself
key = model.data(source_index, self.filterRole())
if re.search(pattern, key, re.IGNORECASE):
return True
# Check children
rows = model.rowCount(source_index)
for i in range(rows):
if self.filterAcceptsRow(i, source_index):
return True
# Otherwise filter it
return False
return super(RecursiveSortFilterProxyModel,
self).filterAcceptsRow(row, parent)

View file

@ -1,448 +0,0 @@
import logging
import contextlib
import collections
from avalon.vendor import qtawesome
from Qt import QtWidgets, QtCore, QtGui
from avalon import style, io
from .model import (
TreeModel,
Item,
RecursiveSortFilterProxyModel,
DeselectableTreeView
)
log = logging.getLogger(__name__)
def _iter_model_rows(model,
column,
include_root=False):
"""Iterate over all row indices in a model"""
indices = [QtCore.QModelIndex()] # start iteration at root
for index in indices:
# Add children to the iterations
child_rows = model.rowCount(index)
for child_row in range(child_rows):
child_index = model.index(child_row, column, index)
indices.append(child_index)
if not include_root and not index.isValid():
continue
yield index
@contextlib.contextmanager
def preserve_expanded_rows(tree_view,
column=0,
role=QtCore.Qt.DisplayRole):
"""Preserves expanded row in QTreeView by column's data role.
This function is created to maintain the expand vs collapse status of
the model items. When refresh is triggered the items which are expanded
will stay expanded and vice versa.
Arguments:
tree_view (QWidgets.QTreeView): the tree view which is
nested in the application
column (int): the column to retrieve the data from
role (int): the role which dictates what will be returned
Returns:
None
"""
model = tree_view.model()
expanded = set()
for index in _iter_model_rows(model,
column=column,
include_root=False):
if tree_view.isExpanded(index):
value = index.data(role)
expanded.add(value)
try:
yield
finally:
if not expanded:
return
for index in _iter_model_rows(model,
column=column,
include_root=False):
value = index.data(role)
state = value in expanded
if state:
tree_view.expand(index)
else:
tree_view.collapse(index)
@contextlib.contextmanager
def preserve_selection(tree_view,
column=0,
role=QtCore.Qt.DisplayRole,
current_index=True):
"""Preserves row selection in QTreeView by column's data role.
This function is created to maintain the selection status of
the model items. When refresh is triggered the items which are expanded
will stay expanded and vice versa.
tree_view (QWidgets.QTreeView): the tree view nested in the application
column (int): the column to retrieve the data from
role (int): the role which dictates what will be returned
Returns:
None
"""
model = tree_view.model()
selection_model = tree_view.selectionModel()
flags = selection_model.Select | selection_model.Rows
if current_index:
current_index_value = tree_view.currentIndex().data(role)
else:
current_index_value = None
selected_rows = selection_model.selectedRows()
if not selected_rows:
yield
return
selected = set(row.data(role) for row in selected_rows)
try:
yield
finally:
if not selected:
return
# Go through all indices, select the ones with similar data
for index in _iter_model_rows(model,
column=column,
include_root=False):
value = index.data(role)
state = value in selected
if state:
tree_view.scrollTo(index) # Ensure item is visible
selection_model.select(index, flags)
if current_index_value and value == current_index_value:
tree_view.setCurrentIndex(index)
class AssetModel(TreeModel):
"""A model listing assets in the silo in the active project.
The assets are displayed in a treeview, they are visually parented by
a `visualParent` field in the database containing an `_id` to a parent
asset.
"""
Columns = ["label"]
Name = 0
Deprecated = 2
ObjectId = 3
DocumentRole = QtCore.Qt.UserRole + 2
ObjectIdRole = QtCore.Qt.UserRole + 3
def __init__(self, parent=None):
super(AssetModel, self).__init__(parent=parent)
self.refresh()
def _add_hierarchy(self, assets, parent=None, silos=None):
"""Add the assets that are related to the parent as children items.
This method does *not* query the database. These instead are queried
in a single batch upfront as an optimization to reduce database
queries. Resulting in up to 10x speed increase.
Args:
assets (dict): All assets in the currently active silo stored
by key/value
Returns:
None
"""
if silos:
# WARNING: Silo item "_id" is set to silo value
# mainly because GUI issue with preserve selection and expanded row
# and because of easier hierarchy parenting (in "assets")
for silo in silos:
item = Item({
"_id": silo,
"name": silo,
"label": silo,
"type": "silo"
})
self.add_child(item, parent=parent)
self._add_hierarchy(assets, parent=item)
parent_id = parent["_id"] if parent else None
current_assets = assets.get(parent_id, list())
for asset in current_assets:
# get label from data, otherwise use name
data = asset.get("data", {})
label = data.get("label", asset["name"])
tags = data.get("tags", [])
# store for the asset for optimization
deprecated = "deprecated" in tags
item = Item({
"_id": asset["_id"],
"name": asset["name"],
"label": label,
"type": asset["type"],
"tags": ", ".join(tags),
"deprecated": deprecated,
"_document": asset
})
self.add_child(item, parent=parent)
# Add asset's children recursively if it has children
if asset["_id"] in assets:
self._add_hierarchy(assets, parent=item)
def refresh(self):
"""Refresh the data for the model."""
self.clear()
self.beginResetModel()
# Get all assets in current silo sorted by name
db_assets = io.find({"type": "asset"}).sort("name", 1)
silos = db_assets.distinct("silo") or None
# if any silo is set to None then it's expected it should not be used
if silos and None in silos:
silos = None
# Group the assets by their visual parent's id
assets_by_parent = collections.defaultdict(list)
for asset in db_assets:
parent_id = (
asset.get("data", {}).get("visualParent") or
asset.get("silo")
)
assets_by_parent[parent_id].append(asset)
# Build the hierarchical tree items recursively
self._add_hierarchy(
assets_by_parent,
parent=None,
silos=silos
)
self.endResetModel()
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def data(self, index, role):
if not index.isValid():
return
item = index.internalPointer()
if role == QtCore.Qt.DecorationRole: # icon
column = index.column()
if column == self.Name:
# Allow a custom icon and custom icon color to be defined
data = item.get("_document", {}).get("data", {})
icon = data.get("icon", None)
if icon is None and item.get("type") == "silo":
icon = "database"
color = data.get("color", style.colors.default)
if icon is None:
# Use default icons if no custom one is specified.
# If it has children show a full folder, otherwise
# show an open folder
has_children = self.rowCount(index) > 0
icon = "folder" if has_children else "folder-o"
# Make the color darker when the asset is deprecated
if item.get("deprecated", False):
color = QtGui.QColor(color).darker(250)
try:
key = "fa.{0}".format(icon) # font-awesome key
icon = qtawesome.icon(key, color=color)
return icon
except Exception as exception:
# Log an error message instead of erroring out completely
# when the icon couldn't be created (e.g. invalid name)
log.error(exception)
return
if role == QtCore.Qt.ForegroundRole: # font color
if "deprecated" in item.get("tags", []):
return QtGui.QColor(style.colors.light).darker(250)
if role == self.ObjectIdRole:
return item.get("_id", None)
if role == self.DocumentRole:
return item.get("_document", None)
return super(AssetModel, self).data(index, role)
class AssetWidget(QtWidgets.QWidget):
"""A Widget to display a tree of assets with filter
To list the assets of the active project:
>>> # widget = AssetWidget()
>>> # widget.refresh()
>>> # widget.show()
"""
assets_refreshed = QtCore.Signal() # on model refresh
selection_changed = QtCore.Signal() # on view selection change
current_changed = QtCore.Signal() # on view current index change
def __init__(self, parent=None):
super(AssetWidget, self).__init__(parent=parent)
self.setContentsMargins(0, 0, 0, 0)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
# Tree View
model = AssetModel(self)
proxy = RecursiveSortFilterProxyModel()
proxy.setSourceModel(model)
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
view = DeselectableTreeView()
view.setIndentation(15)
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
view.setHeaderHidden(True)
view.setModel(proxy)
# Header
header = QtWidgets.QHBoxLayout()
icon = qtawesome.icon("fa.refresh", color=style.colors.light)
refresh = QtWidgets.QPushButton(icon, "")
refresh.setToolTip("Refresh items")
filter = QtWidgets.QLineEdit()
filter.textChanged.connect(proxy.setFilterFixedString)
filter.setPlaceholderText("Filter assets..")
header.addWidget(filter)
header.addWidget(refresh)
# Layout
layout.addLayout(header)
layout.addWidget(view)
# Signals/Slots
selection = view.selectionModel()
selection.selectionChanged.connect(self.selection_changed)
selection.currentChanged.connect(self.current_changed)
refresh.clicked.connect(self.refresh)
self.refreshButton = refresh
self.model = model
self.proxy = proxy
self.view = view
def _refresh_model(self):
with preserve_expanded_rows(
self.view, column=0, role=self.model.ObjectIdRole
):
with preserve_selection(
self.view, column=0, role=self.model.ObjectIdRole
):
self.model.refresh()
self.assets_refreshed.emit()
def refresh(self):
self._refresh_model()
def get_active_asset(self):
"""Return the asset id the current asset."""
current = self.view.currentIndex()
return current.data(self.model.ItemRole)
def get_active_index(self):
return self.view.currentIndex()
def get_selected_assets(self):
"""Return the assets' ids that are selected."""
selection = self.view.selectionModel()
rows = selection.selectedRows()
return [row.data(self.model.ObjectIdRole) for row in rows]
def select_assets(self, assets, expand=True, key="name"):
"""Select assets by name.
Args:
assets (list): List of asset names
expand (bool): Whether to also expand to the asset in the view
Returns:
None
"""
# TODO: Instead of individual selection optimize for many assets
if not isinstance(assets, (tuple, list)):
assets = [assets]
assert isinstance(
assets, (tuple, list)
), "Assets must be list or tuple"
# convert to list - tuple cant be modified
assets = list(assets)
# Clear selection
selection_model = self.view.selectionModel()
selection_model.clearSelection()
# Select
mode = selection_model.Select | selection_model.Rows
for index in iter_model_rows(
self.proxy, column=0, include_root=False
):
# stop iteration if there are no assets to process
if not assets:
break
value = index.data(self.model.ItemRole).get(key)
if value not in assets:
continue
# Remove processed asset
assets.pop(assets.index(value))
selection_model.select(index, mode)
if expand:
# Expand parent index
self.view.expand(self.proxy.parent(index))
# Set the currently active index
self.view.setCurrentIndex(index)

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.9.0-nightly.5"
__version__ = "3.9.0-nightly.6"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.9.0-nightly.5" # OpenPype
version = "3.9.0-nightly.6" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"