mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-26 05:42:15 +01:00
Merge branch 'develop' into deature/OP-2839_Basic-event-system
This commit is contained in:
commit
7bcc0c8688
14 changed files with 129 additions and 1479 deletions
44
CHANGELOG.md
44
CHANGELOG.md
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
|
||||
from .app import (
|
||||
show,
|
||||
cli
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"show",
|
||||
"cli",
|
||||
]
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from . import cli
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.exit(cli(sys.argv[1:]))
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.9.0-nightly.5"
|
||||
__version__ = "3.9.0-nightly.6"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue