mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge branch 'develop' of github.com:pypeclub/OpenPype into feature/OP-3426_Add-support-for-Deadline-for-automatic-tests
This commit is contained in:
commit
aeab0c3f6b
52 changed files with 2149 additions and 504 deletions
28
.github/workflows/milestone_assign.yml
vendored
Normal file
28
.github/workflows/milestone_assign.yml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
name: Milestone - assign to PRs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
run_if_release:
|
||||
if: startsWith(github.base_ref, 'release/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Assign Milestone [next-minor]'
|
||||
if: github.event.pull_request.milestone == null
|
||||
uses: zoispag/action-assign-milestone@v1
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
milestone: 'next-minor'
|
||||
|
||||
run_if_develop:
|
||||
if: ${{ github.base_ref == 'develop' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Assign Milestone [next-patch]'
|
||||
if: github.event.pull_request.milestone == null
|
||||
uses: zoispag/action-assign-milestone@v1
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
milestone: 'next-patch'
|
||||
62
.github/workflows/milestone_create.yml
vendored
Normal file
62
.github/workflows/milestone_create.yml
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
name: Milestone - create default
|
||||
|
||||
on:
|
||||
milestone:
|
||||
types: [closed, edited]
|
||||
|
||||
jobs:
|
||||
generate-next-patch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Get Milestones'
|
||||
uses: "WyriHaximus/github-action-get-milestones@master"
|
||||
id: milestones
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number')
|
||||
id: querymilestone
|
||||
env:
|
||||
MILESTONES: ${{ steps.milestones.outputs.milestones }}
|
||||
MILESTONE: "next-patch"
|
||||
|
||||
- name: Read output
|
||||
run: |
|
||||
echo "${{ steps.querymilestone.outputs.number }}"
|
||||
|
||||
- name: 'Create `next-patch` milestone'
|
||||
if: steps.querymilestone.outputs.number == ''
|
||||
id: createmilestone
|
||||
uses: "WyriHaximus/github-action-create-milestone@v1"
|
||||
with:
|
||||
title: 'next-patch'
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
generate-next-minor:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Get Milestones'
|
||||
uses: "WyriHaximus/github-action-get-milestones@master"
|
||||
id: milestones
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number')
|
||||
id: querymilestone
|
||||
env:
|
||||
MILESTONES: ${{ steps.milestones.outputs.milestones }}
|
||||
MILESTONE: "next-minor"
|
||||
|
||||
- name: Read output
|
||||
run: |
|
||||
echo "${{ steps.querymilestone.outputs.number }}"
|
||||
|
||||
- name: 'Create `next-minor` milestone'
|
||||
if: steps.querymilestone.outputs.number == ''
|
||||
id: createmilestone
|
||||
uses: "WyriHaximus/github-action-create-milestone@v1"
|
||||
with:
|
||||
title: 'next-minor'
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
53
CHANGELOG.md
53
CHANGELOG.md
|
|
@ -1,8 +1,40 @@
|
|||
# Changelog
|
||||
|
||||
## [3.14.4](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.14.5](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...HEAD)
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...HEAD)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Maya: add OBJ extractor to model family [\#4021](https://github.com/pypeclub/OpenPype/pull/4021)
|
||||
- Publish report viewer tool [\#4010](https://github.com/pypeclub/OpenPype/pull/4010)
|
||||
- Nuke | Global: adding custom tags representation filtering [\#4009](https://github.com/pypeclub/OpenPype/pull/4009)
|
||||
- Publisher: Create context has shared data for collection phase [\#3995](https://github.com/pypeclub/OpenPype/pull/3995)
|
||||
- Resolve: updating to v18 compatibility [\#3986](https://github.com/pypeclub/OpenPype/pull/3986)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- TrayPublisher: Fix missing argument [\#4019](https://github.com/pypeclub/OpenPype/pull/4019)
|
||||
- General: Fix python 2 compatibility of ffmpeg and oiio tools discovery [\#4011](https://github.com/pypeclub/OpenPype/pull/4011)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- Maya: Removed unused imports [\#4008](https://github.com/pypeclub/OpenPype/pull/4008)
|
||||
- Unreal: Fix import of moved function [\#4007](https://github.com/pypeclub/OpenPype/pull/4007)
|
||||
- Houdini: Change import of RepairAction [\#4005](https://github.com/pypeclub/OpenPype/pull/4005)
|
||||
- Nuke/Hiero: Refactor openpype.api imports [\#4000](https://github.com/pypeclub/OpenPype/pull/4000)
|
||||
- TVPaint: Defined with HostBase [\#3994](https://github.com/pypeclub/OpenPype/pull/3994)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Unreal: Remove redundant Creator stub [\#4012](https://github.com/pypeclub/OpenPype/pull/4012)
|
||||
- Unreal: add `uproject` extension to Unreal project template [\#4004](https://github.com/pypeclub/OpenPype/pull/4004)
|
||||
- Unreal: fix order of includes [\#4002](https://github.com/pypeclub/OpenPype/pull/4002)
|
||||
- Fusion: Implement backwards compatibility \(+/- Fusion 17.2\) [\#3958](https://github.com/pypeclub/OpenPype/pull/3958)
|
||||
|
||||
## [3.14.4](https://github.com/pypeclub/OpenPype/tree/3.14.4) (2022-10-19)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...3.14.4)
|
||||
|
||||
**🆕 New features**
|
||||
|
||||
|
|
@ -27,7 +59,6 @@
|
|||
- Maya: Moved plugin from global to maya [\#3939](https://github.com/pypeclub/OpenPype/pull/3939)
|
||||
- Publisher: Create dialog is part of main window [\#3936](https://github.com/pypeclub/OpenPype/pull/3936)
|
||||
- Fusion: Implement Alembic and FBX mesh loader [\#3927](https://github.com/pypeclub/OpenPype/pull/3927)
|
||||
- Maya: Remove hardcoded requirement for maya/ start for image file prefix [\#3873](https://github.com/pypeclub/OpenPype/pull/3873)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
|
|
@ -71,14 +102,6 @@
|
|||
**🚀 Enhancements**
|
||||
|
||||
- Publisher: Enhancement proposals [\#3897](https://github.com/pypeclub/OpenPype/pull/3897)
|
||||
- Maya: better logging in Maketx [\#3886](https://github.com/pypeclub/OpenPype/pull/3886)
|
||||
- Photoshop: review can be turned off [\#3885](https://github.com/pypeclub/OpenPype/pull/3885)
|
||||
- TrayPublisher: added persisting of last selected project [\#3871](https://github.com/pypeclub/OpenPype/pull/3871)
|
||||
- TrayPublisher: added text filter on project name to Tray Publisher [\#3867](https://github.com/pypeclub/OpenPype/pull/3867)
|
||||
- Github issues adding `running version` section [\#3864](https://github.com/pypeclub/OpenPype/pull/3864)
|
||||
- Publisher: Increase size of main window [\#3862](https://github.com/pypeclub/OpenPype/pull/3862)
|
||||
- Flame: make migratable projects after creation [\#3860](https://github.com/pypeclub/OpenPype/pull/3860)
|
||||
- Photoshop: synchronize image version with workfile [\#3854](https://github.com/pypeclub/OpenPype/pull/3854)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
|
|
@ -86,12 +109,6 @@
|
|||
- Flame: loading multilayer exr to batch/reel is working [\#3901](https://github.com/pypeclub/OpenPype/pull/3901)
|
||||
- Hiero: Fix inventory check on launch [\#3895](https://github.com/pypeclub/OpenPype/pull/3895)
|
||||
- WebPublisher: Fix import after refactor [\#3891](https://github.com/pypeclub/OpenPype/pull/3891)
|
||||
- TVPaint: Fix renaming of rendered files [\#3882](https://github.com/pypeclub/OpenPype/pull/3882)
|
||||
- Publisher: Nice checkbox visible in Python 2 [\#3877](https://github.com/pypeclub/OpenPype/pull/3877)
|
||||
- Settings: Add missing default settings [\#3870](https://github.com/pypeclub/OpenPype/pull/3870)
|
||||
- General: Copy of workfile does not use 'copy' function but 'copyfile' [\#3869](https://github.com/pypeclub/OpenPype/pull/3869)
|
||||
- Tray Publisher: skip plugin if otioTimeline is missing [\#3856](https://github.com/pypeclub/OpenPype/pull/3856)
|
||||
- Flame: retimed attributes are integrated with settings [\#3855](https://github.com/pypeclub/OpenPype/pull/3855)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
|
|
@ -105,8 +122,6 @@
|
|||
**Merged pull requests:**
|
||||
|
||||
- Maya: Fix Scene Inventory possibly starting off-screen due to maya preferences [\#3923](https://github.com/pypeclub/OpenPype/pull/3923)
|
||||
- Maya: RenderSettings set default image format for V-Ray+Redshift to exr [\#3879](https://github.com/pypeclub/OpenPype/pull/3879)
|
||||
- Remove lockfile during publish [\#3874](https://github.com/pypeclub/OpenPype/pull/3874)
|
||||
|
||||
## [3.14.2](https://github.com/pypeclub/OpenPype/tree/3.14.2) (2022-09-12)
|
||||
|
||||
|
|
|
|||
|
|
@ -284,6 +284,13 @@ def projectmanager():
|
|||
PypeCommands().launch_project_manager()
|
||||
|
||||
|
||||
@main.command(context_settings={"ignore_unknown_options": True})
|
||||
def publish_report_viewer():
|
||||
from openpype.tools.publisher.publish_report_viewer import main
|
||||
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("output_path")
|
||||
@click.option("--project", help="Define project context")
|
||||
|
|
|
|||
|
|
@ -44,11 +44,26 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
|||
|
||||
class FusionLogHandler(logging.Handler):
|
||||
# Keep a reference to fusion's Print function (Remote Object)
|
||||
_print = getattr(sys.modules["__main__"], "fusion").Print
|
||||
_print = None
|
||||
|
||||
@property
|
||||
def print(self):
|
||||
if self._print is not None:
|
||||
# Use cached
|
||||
return self._print
|
||||
|
||||
_print = getattr(sys.modules["__main__"], "fusion").Print
|
||||
if _print is None:
|
||||
# Backwards compatibility: Print method on Fusion instance was
|
||||
# added around Fusion 17.4 and wasn't available on PyRemote Object
|
||||
# before
|
||||
_print = get_current_comp().Print
|
||||
self._print = _print
|
||||
return _print
|
||||
|
||||
def emit(self, record):
|
||||
entry = self.format(record)
|
||||
self._print(entry)
|
||||
self.print(entry)
|
||||
|
||||
|
||||
def install():
|
||||
|
|
|
|||
|
|
@ -251,7 +251,6 @@ def reload_config():
|
|||
import importlib
|
||||
|
||||
for module in (
|
||||
"openpype.api",
|
||||
"openpype.hosts.hiero.lib",
|
||||
"openpype.hosts.hiero.menu",
|
||||
"openpype.hosts.hiero.tags"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from pyblish import api
|
||||
import openpype.api as pype
|
||||
|
||||
from openpype.lib import version_up
|
||||
|
||||
|
||||
class IntegrateVersionUpWorkfile(api.ContextPlugin):
|
||||
|
|
@ -15,7 +16,7 @@ class IntegrateVersionUpWorkfile(api.ContextPlugin):
|
|||
def process(self, context):
|
||||
project = context.data["activeProject"]
|
||||
path = context.data.get("currentFile")
|
||||
new_path = pype.version_up(path)
|
||||
new_path = version_up(path)
|
||||
|
||||
if project:
|
||||
project.saveAs(new_path)
|
||||
|
|
|
|||
0
openpype/hosts/maya/api/obj.py
Normal file
0
openpype/hosts/maya/api/obj.py
Normal file
|
|
@ -13,22 +13,14 @@ from openpype.settings import (
|
|||
get_system_settings,
|
||||
get_project_settings,
|
||||
)
|
||||
from openpype.lib import requests_get
|
||||
from openpype.modules import ModulesManager
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.hosts.maya.api import (
|
||||
lib,
|
||||
lib_rendersettings,
|
||||
plugin
|
||||
)
|
||||
from openpype.lib import requests_get
|
||||
from openpype.api import (
|
||||
get_system_settings,
|
||||
get_project_settings)
|
||||
from openpype.modules import ModulesManager
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline import (
|
||||
CreatorError,
|
||||
legacy_io,
|
||||
)
|
||||
from openpype.pipeline.context_tools import get_current_project_asset
|
||||
|
||||
|
||||
class CreateRender(plugin.Creator):
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ class ImportMayaLoader(load.LoaderPlugin):
|
|||
so you could also use it as a new base.
|
||||
|
||||
"""
|
||||
representations = ["ma", "mb"]
|
||||
representations = ["ma", "mb", "obj"]
|
||||
families = ["*"]
|
||||
|
||||
label = "Import"
|
||||
|
|
|
|||
78
openpype/hosts/maya/plugins/publish/extract_obj.py
Normal file
78
openpype/hosts/maya/plugins/publish/extract_obj.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
from maya import cmds
|
||||
# import maya.mel as mel
|
||||
import pyblish.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
||||
class ExtractObj(publish.Extractor):
|
||||
"""Extract OBJ from Maya.
|
||||
|
||||
This extracts reproducible OBJ exports ignoring any of the settings
|
||||
set on the local machine in the OBJ export options window.
|
||||
|
||||
"""
|
||||
order = pyblish.api.ExtractorOrder
|
||||
hosts = ["maya"]
|
||||
label = "Extract OBJ"
|
||||
families = ["model"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# Define output path
|
||||
|
||||
staging_dir = self.staging_dir(instance)
|
||||
filename = "{0}.obj".format(instance.name)
|
||||
path = os.path.join(staging_dir, filename)
|
||||
|
||||
# The export requires forward slashes because we need to
|
||||
# format it into a string in a mel expression
|
||||
|
||||
self.log.info("Extracting OBJ to: {0}".format(path))
|
||||
|
||||
members = instance.data("setMembers")
|
||||
members = cmds.ls(members,
|
||||
dag=True,
|
||||
shapes=True,
|
||||
type=("mesh", "nurbsCurve"),
|
||||
noIntermediate=True,
|
||||
long=True)
|
||||
self.log.info("Members: {0}".format(members))
|
||||
self.log.info("Instance: {0}".format(instance[:]))
|
||||
|
||||
if not cmds.pluginInfo('objExport', query=True, loaded=True):
|
||||
cmds.loadPlugin('objExport')
|
||||
|
||||
# Export
|
||||
with lib.no_display_layers(instance):
|
||||
with lib.displaySmoothness(members,
|
||||
divisionsU=0,
|
||||
divisionsV=0,
|
||||
pointsWire=4,
|
||||
pointsShaded=1,
|
||||
polygonObject=1):
|
||||
with lib.shader(members,
|
||||
shadingEngine="initialShadingGroup"):
|
||||
with lib.maintained_selection():
|
||||
cmds.select(members, noExpand=True)
|
||||
cmds.file(path,
|
||||
exportSelected=True,
|
||||
type='OBJexport',
|
||||
preserveReferences=True,
|
||||
force=True)
|
||||
|
||||
if "representation" not in instance.data:
|
||||
instance.data["representation"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'obj',
|
||||
'ext': 'obj',
|
||||
'files': filename,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extract OBJ successful to: {0}".format(path))
|
||||
|
|
@ -2930,3 +2930,47 @@ def get_nodes_by_names(names):
|
|||
nuke.toNode(name)
|
||||
for name in names
|
||||
]
|
||||
|
||||
|
||||
def get_viewer_config_from_string(input_string):
|
||||
"""Convert string to display and viewer string
|
||||
|
||||
Args:
|
||||
input_string (str): string with viewer
|
||||
|
||||
Raises:
|
||||
IndexError: if more then one slash in input string
|
||||
IndexError: if missing closing bracket
|
||||
|
||||
Returns:
|
||||
tuple[str]: display, viewer
|
||||
"""
|
||||
display = None
|
||||
viewer = input_string
|
||||
# check if () or / or \ in name
|
||||
if "/" in viewer:
|
||||
split = viewer.split("/")
|
||||
|
||||
# rise if more then one column
|
||||
if len(split) > 2:
|
||||
raise IndexError((
|
||||
"Viewer Input string is not correct. "
|
||||
"more then two `/` slashes! {}"
|
||||
).format(input_string))
|
||||
|
||||
viewer = split[1]
|
||||
display = split[0]
|
||||
elif "(" in viewer:
|
||||
pattern = r"([\w\d\s]+).*[(](.*)[)]"
|
||||
result = re.findall(pattern, viewer)
|
||||
try:
|
||||
result = result.pop()
|
||||
display = str(result[1]).rstrip()
|
||||
viewer = str(result[0]).rstrip()
|
||||
except IndexError:
|
||||
raise IndexError((
|
||||
"Viewer Input string is not correct. "
|
||||
"Missing bracket! {}"
|
||||
).format(input_string))
|
||||
|
||||
return (display, viewer)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ def reload_config():
|
|||
"""
|
||||
|
||||
for module in (
|
||||
"openpype.api",
|
||||
"openpype.hosts.nuke.api.actions",
|
||||
"openpype.hosts.nuke.api.menu",
|
||||
"openpype.hosts.nuke.api.plugin",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ from .lib import (
|
|||
add_publish_knob,
|
||||
get_nuke_imageio_settings,
|
||||
set_node_knobs_from_settings,
|
||||
get_view_process_node
|
||||
get_view_process_node,
|
||||
get_viewer_config_from_string
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -190,7 +191,20 @@ class ExporterReview(object):
|
|||
if "#" in self.fhead:
|
||||
self.fhead = self.fhead.replace("#", "")[:-1]
|
||||
|
||||
def get_representation_data(self, tags=None, range=False):
|
||||
def get_representation_data(
|
||||
self, tags=None, range=False,
|
||||
custom_tags=None
|
||||
):
|
||||
""" Add representation data to self.data
|
||||
|
||||
Args:
|
||||
tags (list[str], optional): list of defined tags.
|
||||
Defaults to None.
|
||||
range (bool, optional): flag for adding ranges.
|
||||
Defaults to False.
|
||||
custom_tags (list[str], optional): user inputed custom tags.
|
||||
Defaults to None.
|
||||
"""
|
||||
add_tags = tags or []
|
||||
repre = {
|
||||
"name": self.name,
|
||||
|
|
@ -200,6 +214,9 @@ class ExporterReview(object):
|
|||
"tags": [self.name.replace("_", "-")] + add_tags
|
||||
}
|
||||
|
||||
if custom_tags:
|
||||
repre["custom_tags"] = custom_tags
|
||||
|
||||
if range:
|
||||
repre.update({
|
||||
"frameStart": self.first_frame,
|
||||
|
|
@ -312,7 +329,8 @@ class ExporterReviewLut(ExporterReview):
|
|||
dag_node.setInput(0, self.previous_node)
|
||||
self._temp_nodes.append(dag_node)
|
||||
self.previous_node = dag_node
|
||||
self.log.debug("OCIODisplay... `{}`".format(self._temp_nodes))
|
||||
self.log.debug(
|
||||
"OCIODisplay... `{}`".format(self._temp_nodes))
|
||||
|
||||
# GenerateLUT
|
||||
gen_lut_node = nuke.createNode("GenerateLUT")
|
||||
|
|
@ -417,6 +435,7 @@ class ExporterReviewMov(ExporterReview):
|
|||
return path
|
||||
|
||||
def generate_mov(self, farm=False, **kwargs):
|
||||
add_tags = []
|
||||
self.publish_on_farm = farm
|
||||
read_raw = kwargs["read_raw"]
|
||||
reformat_node_add = kwargs["reformat_node_add"]
|
||||
|
|
@ -435,10 +454,10 @@ class ExporterReviewMov(ExporterReview):
|
|||
self.log.debug(">> baking_view_profile `{}`".format(
|
||||
baking_view_profile))
|
||||
|
||||
add_tags = kwargs.get("add_tags", [])
|
||||
add_custom_tags = kwargs.get("add_custom_tags", [])
|
||||
|
||||
self.log.info(
|
||||
"__ add_tags: `{0}`".format(add_tags))
|
||||
"__ add_custom_tags: `{0}`".format(add_custom_tags))
|
||||
|
||||
subset = self.instance.data["subset"]
|
||||
self._temp_nodes[subset] = []
|
||||
|
|
@ -493,7 +512,15 @@ class ExporterReviewMov(ExporterReview):
|
|||
if not self.viewer_lut_raw:
|
||||
# OCIODisplay
|
||||
dag_node = nuke.createNode("OCIODisplay")
|
||||
dag_node["view"].setValue(str(baking_view_profile))
|
||||
|
||||
display, viewer = get_viewer_config_from_string(
|
||||
str(baking_view_profile)
|
||||
)
|
||||
if display:
|
||||
dag_node["display"].setValue(display)
|
||||
|
||||
# assign viewer
|
||||
dag_node["view"].setValue(viewer)
|
||||
|
||||
# connect
|
||||
dag_node.setInput(0, self.previous_node)
|
||||
|
|
@ -544,6 +571,7 @@ class ExporterReviewMov(ExporterReview):
|
|||
# ---------- generate representation data
|
||||
self.get_representation_data(
|
||||
tags=["review", "delete"] + add_tags,
|
||||
custom_tags=add_custom_tags,
|
||||
range=True
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import os
|
|||
import nuke
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api as pype
|
||||
|
||||
from openpype.lib import get_version_from_path
|
||||
from openpype.hosts.nuke.api.lib import (
|
||||
add_publish_knob,
|
||||
get_avalon_knob_data
|
||||
|
|
@ -74,7 +75,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
|
|||
"fps": root['fps'].value(),
|
||||
|
||||
"currentFile": current_file,
|
||||
"version": int(pype.get_version_from_path(current_file)),
|
||||
"version": int(get_version_from_path(current_file)),
|
||||
|
||||
"host": pyblish.api.current_host(),
|
||||
"hostVersion": nuke.NUKE_VERSION_STRING
|
||||
|
|
|
|||
|
|
@ -17,11 +17,27 @@ from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS
|
|||
REVIEW_EXTENSIONS = IMAGE_EXTENSIONS + VIDEO_EXTENSIONS
|
||||
|
||||
|
||||
def _cache_and_get_instances(creator):
|
||||
"""Cache instances in shared data.
|
||||
|
||||
Args:
|
||||
creator (Creator): Plugin which would like to get instances from host.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Cached instances list from host implementation.
|
||||
"""
|
||||
|
||||
shared_key = "openpype.traypublisher.instances"
|
||||
if shared_key not in creator.collection_shared_data:
|
||||
creator.collection_shared_data[shared_key] = list_instances()
|
||||
return creator.collection_shared_data[shared_key]
|
||||
|
||||
|
||||
class HiddenTrayPublishCreator(HiddenCreator):
|
||||
host_name = "traypublisher"
|
||||
|
||||
def collect_instances(self):
|
||||
for instance_data in list_instances():
|
||||
for instance_data in _cache_and_get_instances(self):
|
||||
creator_id = instance_data.get("creator_identifier")
|
||||
if creator_id == self.identifier:
|
||||
instance = CreatedInstance.from_existing(
|
||||
|
|
@ -58,7 +74,7 @@ class TrayPublishCreator(Creator):
|
|||
host_name = "traypublisher"
|
||||
|
||||
def collect_instances(self):
|
||||
for instance_data in list_instances():
|
||||
for instance_data in _cache_and_get_instances(self):
|
||||
creator_id = instance_data.get("creator_identifier")
|
||||
if creator_id == self.identifier:
|
||||
instance = CreatedInstance.from_existing(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Unreal Editor OpenPype host API."""
|
||||
|
||||
from .plugin import (
|
||||
Loader,
|
||||
Creator
|
||||
)
|
||||
from .plugin import Loader
|
||||
|
||||
from .pipeline import (
|
||||
install,
|
||||
uninstall,
|
||||
|
|
@ -25,7 +23,6 @@ from .pipeline import (
|
|||
__all__ = [
|
||||
"install",
|
||||
"uninstall",
|
||||
"Creator",
|
||||
"Loader",
|
||||
"ls",
|
||||
"publish",
|
||||
|
|
|
|||
|
|
@ -1,16 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from abc import ABC
|
||||
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
LoaderPlugin,
|
||||
)
|
||||
|
||||
|
||||
class Creator(LegacyCreator):
|
||||
"""This serves as skeleton for future OpenPype specific functionality"""
|
||||
defaults = ['Main']
|
||||
maintain_selection = False
|
||||
from openpype.pipeline import LoaderPlugin
|
||||
|
||||
|
||||
class Loader(LoaderPlugin, ABC):
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import unreal
|
|||
from unreal import EditorAssetLibrary as eal
|
||||
from unreal import EditorLevelLibrary as ell
|
||||
|
||||
from openpype.hosts.unreal.api import plugin
|
||||
from openpype.hosts.unreal.api.pipeline import instantiate
|
||||
from openpype.pipeline import LegacyCreator
|
||||
|
||||
|
||||
class CreateCamera(plugin.Creator):
|
||||
class CreateCamera(LegacyCreator):
|
||||
"""Layout output for character rigs"""
|
||||
|
||||
name = "layoutMain"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from unreal import EditorLevelLibrary
|
||||
|
||||
from openpype.hosts.unreal.api import plugin
|
||||
from openpype.pipeline import LegacyCreator
|
||||
from openpype.hosts.unreal.api.pipeline import instantiate
|
||||
|
||||
|
||||
class CreateLayout(plugin.Creator):
|
||||
class CreateLayout(LegacyCreator):
|
||||
"""Layout output for character rigs."""
|
||||
|
||||
name = "layoutMain"
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
"""Create look in Unreal."""
|
||||
import unreal # noqa
|
||||
from openpype.hosts.unreal.api import pipeline, plugin
|
||||
from openpype.pipeline import LegacyCreator
|
||||
|
||||
|
||||
class CreateLook(plugin.Creator):
|
||||
class CreateLook(LegacyCreator):
|
||||
"""Shader connections defining shape look."""
|
||||
|
||||
name = "unrealLook"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import unreal
|
||||
|
||||
from openpype.hosts.unreal.api import pipeline
|
||||
from openpype.hosts.unreal.api.plugin import Creator
|
||||
from openpype.pipeline import LegacyCreator
|
||||
|
||||
|
||||
class CreateRender(Creator):
|
||||
class CreateRender(LegacyCreator):
|
||||
"""Create instance for sequence for rendering"""
|
||||
|
||||
name = "unrealRender"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Create Static Meshes as FBX geometry."""
|
||||
import unreal # noqa
|
||||
from openpype.hosts.unreal.api import plugin
|
||||
from openpype.hosts.unreal.api.pipeline import (
|
||||
instantiate,
|
||||
)
|
||||
from openpype.pipeline import LegacyCreator
|
||||
|
||||
|
||||
class CreateStaticMeshFBX(plugin.Creator):
|
||||
class CreateStaticMeshFBX(LegacyCreator):
|
||||
"""Static FBX geometry."""
|
||||
|
||||
name = "unrealStaticMeshMain"
|
||||
|
|
|
|||
|
|
@ -195,6 +195,28 @@ def find_tool_in_custom_paths(paths, tool, validation_func=None):
|
|||
return None
|
||||
|
||||
|
||||
def _check_args_returncode(args):
|
||||
try:
|
||||
# Python 2 compatibility where DEVNULL is not available
|
||||
if hasattr(subprocess, "DEVNULL"):
|
||||
proc = subprocess.Popen(
|
||||
args,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
proc.wait()
|
||||
else:
|
||||
with open(os.devnull, "w") as devnull:
|
||||
proc = subprocess.Popen(
|
||||
args, stdout=devnull, stderr=devnull,
|
||||
)
|
||||
proc.wait()
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
return proc.returncode == 0
|
||||
|
||||
|
||||
def _oiio_executable_validation(filepath):
|
||||
"""Validate oiio tool executable if can be executed.
|
||||
|
||||
|
|
@ -223,18 +245,7 @@ def _oiio_executable_validation(filepath):
|
|||
if not filepath:
|
||||
return False
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[filepath, "--help"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
proc.wait()
|
||||
return proc.returncode == 0
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
return _check_args_returncode([filepath, "--help"])
|
||||
|
||||
|
||||
def get_oiio_tools_path(tool="oiiotool"):
|
||||
|
|
@ -302,18 +313,7 @@ def _ffmpeg_executable_validation(filepath):
|
|||
if not filepath:
|
||||
return False
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[filepath, "-version"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
proc.wait()
|
||||
return proc.returncode == 0
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
return _check_args_returncode([filepath, "-version"])
|
||||
|
||||
|
||||
def get_ffmpeg_tool_path(tool="ffmpeg"):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import os
|
||||
import sys
|
||||
import copy
|
||||
import logging
|
||||
import traceback
|
||||
import collections
|
||||
import inspect
|
||||
from uuid import uuid4
|
||||
|
|
@ -22,11 +24,18 @@ from .creator_plugins import (
|
|||
Creator,
|
||||
AutoCreator,
|
||||
discover_creator_plugins,
|
||||
discover_convertor_plugins,
|
||||
CreatorError,
|
||||
)
|
||||
|
||||
UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"])
|
||||
|
||||
|
||||
class UnavailableSharedData(Exception):
|
||||
"""Shared data are not available at the moment when are accessed."""
|
||||
pass
|
||||
|
||||
|
||||
class ImmutableKeyError(TypeError):
|
||||
"""Accessed key is immutable so does not allow changes or removements."""
|
||||
|
||||
|
|
@ -62,6 +71,112 @@ class HostMissRequiredMethod(Exception):
|
|||
super(HostMissRequiredMethod, self).__init__(msg)
|
||||
|
||||
|
||||
class ConvertorsOperationFailed(Exception):
|
||||
def __init__(self, msg, failed_info):
|
||||
super(ConvertorsOperationFailed, self).__init__(msg)
|
||||
self.failed_info = failed_info
|
||||
|
||||
|
||||
class ConvertorsFindFailed(ConvertorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to find incompatible subsets"
|
||||
super(ConvertorsFindFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
class ConvertorsConversionFailed(ConvertorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to convert incompatible subsets"
|
||||
super(ConvertorsConversionFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
def prepare_failed_convertor_operation_info(identifier, exc_info):
|
||||
exc_type, exc_value, exc_traceback = exc_info
|
||||
formatted_traceback = "".join(traceback.format_exception(
|
||||
exc_type, exc_value, exc_traceback
|
||||
))
|
||||
|
||||
return {
|
||||
"convertor_identifier": identifier,
|
||||
"message": str(exc_value),
|
||||
"traceback": formatted_traceback
|
||||
}
|
||||
|
||||
|
||||
class CreatorsOperationFailed(Exception):
|
||||
"""Raised when a creator process crashes in 'CreateContext'.
|
||||
|
||||
The exception contains information about the creator and error. The data
|
||||
are prepared using 'prepare_failed_creator_operation_info' and can be
|
||||
serialized using json.
|
||||
|
||||
Usage is for UI purposes which may not have access to exceptions directly
|
||||
and would not have ability to catch exceptions 'per creator'.
|
||||
|
||||
Args:
|
||||
msg (str): General error message.
|
||||
failed_info (list[dict[str, Any]]): List of failed creators with
|
||||
exception message and optionally formatted traceback.
|
||||
"""
|
||||
|
||||
def __init__(self, msg, failed_info):
|
||||
super(CreatorsOperationFailed, self).__init__(msg)
|
||||
self.failed_info = failed_info
|
||||
|
||||
|
||||
class CreatorsCollectionFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to collect instances"
|
||||
super(CreatorsCollectionFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
class CreatorsSaveFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed update instance changes"
|
||||
super(CreatorsSaveFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
class CreatorsRemoveFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to remove instances"
|
||||
super(CreatorsRemoveFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
class CreatorsCreateFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Faled to create instances"
|
||||
super(CreatorsCreateFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
def prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback=True
|
||||
):
|
||||
formatted_traceback = None
|
||||
exc_type, exc_value, exc_traceback = exc_info
|
||||
if add_traceback:
|
||||
formatted_traceback = "".join(traceback.format_exception(
|
||||
exc_type, exc_value, exc_traceback
|
||||
))
|
||||
|
||||
return {
|
||||
"creator_identifier": identifier,
|
||||
"creator_label": label,
|
||||
"message": str(exc_value),
|
||||
"traceback": formatted_traceback
|
||||
}
|
||||
|
||||
|
||||
class InstanceMember:
|
||||
"""Representation of instance member.
|
||||
|
||||
|
|
@ -847,6 +962,37 @@ class CreatedInstance:
|
|||
self[key] = new_value
|
||||
|
||||
|
||||
class ConvertorItem(object):
|
||||
"""Item representing convertor plugin.
|
||||
|
||||
Args:
|
||||
identifier (str): Identifier of convertor.
|
||||
label (str): Label which will be shown in UI.
|
||||
"""
|
||||
|
||||
def __init__(self, identifier, label):
|
||||
self._id = str(uuid4())
|
||||
self.identifier = identifier
|
||||
self.label = label
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"identifier": self.identifier,
|
||||
"label": self.label
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
obj = cls(data["identifier"], data["label"])
|
||||
obj._id = data["id"]
|
||||
return obj
|
||||
|
||||
|
||||
class CreateContext:
|
||||
"""Context of instance creation.
|
||||
|
||||
|
|
@ -912,6 +1058,9 @@ class CreateContext:
|
|||
# Manual creators
|
||||
self.manual_creators = {}
|
||||
|
||||
self.convertors_plugins = {}
|
||||
self.convertor_items_by_id = {}
|
||||
|
||||
self.publish_discover_result = None
|
||||
self.publish_plugins_mismatch_targets = []
|
||||
self.publish_plugins = []
|
||||
|
|
@ -925,6 +1074,9 @@ class CreateContext:
|
|||
self._bulk_counter = 0
|
||||
self._bulk_instances_to_process = []
|
||||
|
||||
# Shared data across creators during collection phase
|
||||
self._collection_shared_data = None
|
||||
|
||||
# Trigger reset if was enabled
|
||||
if reset:
|
||||
self.reset(discover_publish_plugins)
|
||||
|
|
@ -980,14 +1132,32 @@ class CreateContext:
|
|||
|
||||
All changes will be lost if were not saved explicitely.
|
||||
"""
|
||||
|
||||
self.reset_preparation()
|
||||
|
||||
self.reset_avalon_context()
|
||||
self.reset_plugins(discover_publish_plugins)
|
||||
self.reset_context_data()
|
||||
|
||||
with self.bulk_instances_collection():
|
||||
self.reset_instances()
|
||||
self.find_convertor_items()
|
||||
self.execute_autocreators()
|
||||
|
||||
self.reset_finalization()
|
||||
|
||||
def reset_preparation(self):
|
||||
"""Prepare attributes that must be prepared/cleaned before reset."""
|
||||
|
||||
# Give ability to store shared data for collection phase
|
||||
self._collection_shared_data = {}
|
||||
|
||||
def reset_finalization(self):
|
||||
"""Cleanup of attributes after reset."""
|
||||
|
||||
# Stop access to collection shared data
|
||||
self._collection_shared_data = None
|
||||
|
||||
def reset_avalon_context(self):
|
||||
"""Give ability to reset avalon context.
|
||||
|
||||
|
|
@ -1026,6 +1196,12 @@ class CreateContext:
|
|||
Reloads creators from preregistered paths and can load publish plugins
|
||||
if it's enabled on context.
|
||||
"""
|
||||
|
||||
self._reset_publish_plugins(discover_publish_plugins)
|
||||
self._reset_creator_plugins()
|
||||
self._reset_convertor_plugins()
|
||||
|
||||
def _reset_publish_plugins(self, discover_publish_plugins):
|
||||
import pyblish.logic
|
||||
|
||||
from openpype.pipeline import OpenPypePyblishPluginMixin
|
||||
|
|
@ -1067,6 +1243,7 @@ class CreateContext:
|
|||
self.publish_plugins = plugins_by_targets
|
||||
self.plugins_with_defs = plugins_with_defs
|
||||
|
||||
def _reset_creator_plugins(self):
|
||||
# Prepare settings
|
||||
system_settings = get_system_settings()
|
||||
project_settings = get_project_settings(self.project_name)
|
||||
|
|
@ -1118,6 +1295,27 @@ class CreateContext:
|
|||
|
||||
self.creators = creators
|
||||
|
||||
def _reset_convertor_plugins(self):
|
||||
convertors_plugins = {}
|
||||
for convertor_class in discover_convertor_plugins():
|
||||
if inspect.isabstract(convertor_class):
|
||||
self.log.info(
|
||||
"Skipping abstract Creator {}".format(str(convertor_class))
|
||||
)
|
||||
continue
|
||||
|
||||
convertor_identifier = convertor_class.identifier
|
||||
if convertor_identifier in convertors_plugins:
|
||||
self.log.warning((
|
||||
"Duplicated Converter identifier. "
|
||||
"Using first and skipping following"
|
||||
))
|
||||
continue
|
||||
|
||||
convertors_plugins[convertor_identifier] = convertor_class(self)
|
||||
|
||||
self.convertors_plugins = convertors_plugins
|
||||
|
||||
def reset_context_data(self):
|
||||
"""Reload context data using host implementation.
|
||||
|
||||
|
|
@ -1186,9 +1384,75 @@ class CreateContext:
|
|||
with self.bulk_instances_collection():
|
||||
self._bulk_instances_to_process.append(instance)
|
||||
|
||||
def create(self, identifier, *args, **kwargs):
|
||||
"""Wrapper for creators to trigger created.
|
||||
|
||||
Different types of creators may expect different arguments thus the
|
||||
hints for args are blind.
|
||||
|
||||
Args:
|
||||
identifier (str): Creator's identifier.
|
||||
*args (Tuple[Any]): Arguments for create method.
|
||||
**kwargs (Dict[Any, Any]): Keyword argument for create method.
|
||||
"""
|
||||
|
||||
error_message = "Failed to run Creator with identifier \"{}\". {}"
|
||||
creator = self.creators.get(identifier)
|
||||
label = getattr(creator, "label", None)
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
# Fake CreatorError (Could be maybe specific exception?)
|
||||
if creator is None:
|
||||
raise CreatorError(
|
||||
"Creator {} was not found".format(identifier)
|
||||
)
|
||||
|
||||
creator.create(*args, **kwargs)
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
raise CreatorsCreateFailed([
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
])
|
||||
|
||||
def creator_removed_instance(self, instance):
|
||||
"""When creator removes instance context should be acknowledged.
|
||||
|
||||
If creator removes instance conext should know about it to avoid
|
||||
possible issues in the session.
|
||||
|
||||
Args:
|
||||
instance (CreatedInstance): Object of instance which was removed
|
||||
from scene metadata.
|
||||
"""
|
||||
|
||||
self._instances_by_id.pop(instance.id, None)
|
||||
|
||||
def add_convertor_item(self, convertor_identifier, label):
|
||||
self.convertor_items_by_id[convertor_identifier] = ConvertorItem(
|
||||
convertor_identifier, label
|
||||
)
|
||||
|
||||
def remove_convertor_item(self, convertor_identifier):
|
||||
self.convertor_items_by_id.pop(convertor_identifier, None)
|
||||
|
||||
@contextmanager
|
||||
def bulk_instances_collection(self):
|
||||
"""Validate context of instances in bulk.
|
||||
|
|
@ -1221,24 +1485,112 @@ class CreateContext:
|
|||
self._instances_by_id = {}
|
||||
|
||||
# Collect instances
|
||||
error_message = "Collection of instances for creator {} failed. {}"
|
||||
failed_info = []
|
||||
for creator in self.creators.values():
|
||||
creator.collect_instances()
|
||||
label = creator.label
|
||||
identifier = creator.identifier
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
creator.collect_instances()
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsCollectionFailed(failed_info)
|
||||
|
||||
def find_convertor_items(self):
|
||||
"""Go through convertor plugins to look for items to convert.
|
||||
|
||||
Raises:
|
||||
ConvertorsFindFailed: When one or more convertors fails during
|
||||
finding.
|
||||
"""
|
||||
|
||||
self.convertor_items_by_id = {}
|
||||
|
||||
failed_info = []
|
||||
for convertor in self.convertors_plugins.values():
|
||||
try:
|
||||
convertor.find_instances()
|
||||
|
||||
except:
|
||||
failed_info.append(
|
||||
prepare_failed_convertor_operation_info(
|
||||
convertor.identifier, sys.exc_info()
|
||||
)
|
||||
)
|
||||
self.log.warning(
|
||||
"Failed to find instances of convertor \"{}\"".format(
|
||||
convertor.identifier
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise ConvertorsFindFailed(failed_info)
|
||||
|
||||
def execute_autocreators(self):
|
||||
"""Execute discovered AutoCreator plugins.
|
||||
|
||||
Reset instances if any autocreator executed properly.
|
||||
"""
|
||||
|
||||
error_message = "Failed to run AutoCreator with identifier \"{}\". {}"
|
||||
failed_info = []
|
||||
for identifier, creator in self.autocreators.items():
|
||||
label = creator.label
|
||||
failed = False
|
||||
add_traceback = False
|
||||
try:
|
||||
creator.create()
|
||||
|
||||
except Exception:
|
||||
# TODO raise report exception if any crashed
|
||||
msg = (
|
||||
"Failed to run AutoCreator with identifier \"{}\" ({})."
|
||||
).format(identifier, inspect.getfile(creator.__class__))
|
||||
self.log.warning(msg, exc_info=True)
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
# Use bare except because some hosts raise their exceptions that
|
||||
# do not inherit from python's `BaseException`
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsCreateFailed(failed_info)
|
||||
|
||||
def validate_instances_context(self, instances=None):
|
||||
"""Validate 'asset' and 'task' instance context."""
|
||||
|
|
@ -1315,17 +1667,48 @@ class CreateContext:
|
|||
identifier = instance.creator_identifier
|
||||
instances_by_identifier[identifier].append(instance)
|
||||
|
||||
for identifier, cretor_instances in instances_by_identifier.items():
|
||||
error_message = "Instances update of creator \"{}\" failed. {}"
|
||||
failed_info = []
|
||||
for identifier, creator_instances in instances_by_identifier.items():
|
||||
update_list = []
|
||||
for instance in cretor_instances:
|
||||
for instance in creator_instances:
|
||||
instance_changes = instance.changes()
|
||||
if instance_changes:
|
||||
update_list.append(UpdateData(instance, instance_changes))
|
||||
|
||||
creator = self.creators[identifier]
|
||||
if update_list:
|
||||
if not update_list:
|
||||
continue
|
||||
|
||||
label = creator.label
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
creator.update_instances(update_list)
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""), exc_info=True)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsSaveFailed(failed_info)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
"""Remove instances from context.
|
||||
|
||||
|
|
@ -1333,14 +1716,48 @@ class CreateContext:
|
|||
instances(list<CreatedInstance>): Instances that should be removed
|
||||
from context.
|
||||
"""
|
||||
|
||||
instances_by_identifier = collections.defaultdict(list)
|
||||
for instance in instances:
|
||||
identifier = instance.creator_identifier
|
||||
instances_by_identifier[identifier].append(instance)
|
||||
|
||||
error_message = "Instances removement of creator \"{}\" failed. {}"
|
||||
failed_info = []
|
||||
for identifier, creator_instances in instances_by_identifier.items():
|
||||
creator = self.creators.get(identifier)
|
||||
creator.remove_instances(creator_instances)
|
||||
label = creator.label
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
creator.remove_instances(creator_instances)
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, exc_info[1])
|
||||
)
|
||||
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsRemoveFailed(failed_info)
|
||||
|
||||
def _get_publish_plugins_with_attr_for_family(self, family):
|
||||
"""Publish plugin attributes for passed family.
|
||||
|
|
@ -1372,3 +1789,68 @@ class CreateContext:
|
|||
if not plugin.__instanceEnabled__:
|
||||
plugins.append(plugin)
|
||||
return plugins
|
||||
|
||||
@property
|
||||
def collection_shared_data(self):
|
||||
"""Access to shared data that can be used during creator's collection.
|
||||
|
||||
Retruns:
|
||||
Dict[str, Any]: Shared data.
|
||||
|
||||
Raises:
|
||||
UnavailableSharedData: When called out of collection phase.
|
||||
"""
|
||||
|
||||
if self._collection_shared_data is None:
|
||||
raise UnavailableSharedData(
|
||||
"Accessed Collection shared data out of collection phase"
|
||||
)
|
||||
return self._collection_shared_data
|
||||
|
||||
def run_convertor(self, convertor_identifier):
|
||||
"""Run convertor plugin by it's idenfitifier.
|
||||
|
||||
Conversion is skipped if convertor is not available.
|
||||
|
||||
Args:
|
||||
convertor_identifier (str): Identifier of convertor.
|
||||
"""
|
||||
|
||||
convertor = self.convertors_plugins.get(convertor_identifier)
|
||||
if convertor is not None:
|
||||
convertor.convert()
|
||||
|
||||
def run_convertors(self, convertor_identifiers):
|
||||
"""Run convertor plugins by idenfitifiers.
|
||||
|
||||
Conversion is skipped if convertor is not available. It is recommended
|
||||
to trigger reset after conversion to reload instances.
|
||||
|
||||
Args:
|
||||
convertor_identifiers (Iterator[str]): Identifiers of convertors
|
||||
to run.
|
||||
|
||||
Raises:
|
||||
ConvertorsConversionFailed: When one or more convertors fails.
|
||||
"""
|
||||
|
||||
failed_info = []
|
||||
for convertor_identifier in convertor_identifiers:
|
||||
try:
|
||||
self.run_convertor(convertor_identifier)
|
||||
|
||||
except:
|
||||
failed_info.append(
|
||||
prepare_failed_convertor_operation_info(
|
||||
convertor_identifier, sys.exc_info()
|
||||
)
|
||||
)
|
||||
self.log.warning(
|
||||
"Failed to convert instances of convertor \"{}\"".format(
|
||||
convertor_identifier
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise ConvertorsConversionFailed(failed_info)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from abc import (
|
|||
abstractmethod,
|
||||
abstractproperty
|
||||
)
|
||||
|
||||
import six
|
||||
|
||||
from openpype.settings import get_system_settings, get_project_settings
|
||||
|
|
@ -32,6 +33,111 @@ class CreatorError(Exception):
|
|||
super(CreatorError, self).__init__(message)
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class SubsetConvertorPlugin(object):
|
||||
"""Helper for conversion of instances created using legacy creators.
|
||||
|
||||
Conversion from legacy creators would mean to loose legacy instances,
|
||||
convert them automatically or write a script which must user run. All of
|
||||
these solutions are workign but will happen without asking or user must
|
||||
know about them. This plugin can be used to show legacy instances in
|
||||
Publisher and give user ability to run conversion script.
|
||||
|
||||
Convertor logic should be very simple. Method 'find_instances' is to
|
||||
look for legacy instances in scene a possibly call
|
||||
pre-implemented 'add_convertor_item'.
|
||||
|
||||
User will have ability to trigger conversion which is executed by calling
|
||||
'convert' which should call 'remove_convertor_item' when is done.
|
||||
|
||||
It does make sense to add only one or none legacy item to create context
|
||||
for convertor as it's not possible to choose which instace are converted
|
||||
and which are not.
|
||||
|
||||
Convertor can use 'collection_shared_data' property like creators. Also
|
||||
can store any information to it's object for conversion purposes.
|
||||
|
||||
Args:
|
||||
create_context
|
||||
"""
|
||||
|
||||
_log = None
|
||||
|
||||
def __init__(self, create_context):
|
||||
self._create_context = create_context
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
"""Logger of the plugin.
|
||||
|
||||
Returns:
|
||||
logging.Logger: Logger with name of the plugin.
|
||||
"""
|
||||
|
||||
if self._log is None:
|
||||
self._log = Logger.get_logger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
@abstractproperty
|
||||
def identifier(self):
|
||||
"""Converted identifier.
|
||||
|
||||
Returns:
|
||||
str: Converted identifier unique for all converters in host.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_instances(self):
|
||||
"""Look for legacy instances in the scene.
|
||||
|
||||
Should call 'add_convertor_item' if there is at least one instance to
|
||||
convert.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def convert(self):
|
||||
"""Conversion code."""
|
||||
|
||||
pass
|
||||
|
||||
@property
|
||||
def create_context(self):
|
||||
"""Quick access to create context."""
|
||||
|
||||
return self._create_context
|
||||
|
||||
@property
|
||||
def collection_shared_data(self):
|
||||
"""Access to shared data that can be used during 'find_instances'.
|
||||
|
||||
Retruns:
|
||||
Dict[str, Any]: Shared data.
|
||||
|
||||
Raises:
|
||||
UnavailableSharedData: When called out of collection phase.
|
||||
"""
|
||||
|
||||
return self._create_context.collection_shared_data
|
||||
|
||||
def add_convertor_item(self, label):
|
||||
"""Add item to CreateContext.
|
||||
|
||||
Args:
|
||||
label (str): Label of item which will show in UI.
|
||||
"""
|
||||
|
||||
self._create_context.add_convertor_item(self.identifier, label)
|
||||
|
||||
def remove_convertor_item(self):
|
||||
"""Remove legacy item from create context when conversion finished."""
|
||||
|
||||
self._create_context.remove_convertor_item(self.identifier)
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class BaseCreator:
|
||||
"""Plugin that create and modify instance data before publishing process.
|
||||
|
|
@ -323,6 +429,19 @@ class BaseCreator:
|
|||
|
||||
return self.instance_attr_defs
|
||||
|
||||
@property
|
||||
def collection_shared_data(self):
|
||||
"""Access to shared data that can be used during creator's collection.
|
||||
|
||||
Retruns:
|
||||
Dict[str, Any]: Shared data.
|
||||
|
||||
Raises:
|
||||
UnavailableSharedData: When called out of collection phase.
|
||||
"""
|
||||
|
||||
return self.create_context.collection_shared_data
|
||||
|
||||
|
||||
class Creator(BaseCreator):
|
||||
"""Creator that has more information for artist to show in UI.
|
||||
|
|
@ -455,6 +574,10 @@ def discover_creator_plugins():
|
|||
return discover(BaseCreator)
|
||||
|
||||
|
||||
def discover_convertor_plugins():
|
||||
return discover(SubsetConvertorPlugin)
|
||||
|
||||
|
||||
def discover_legacy_creator_plugins():
|
||||
from openpype.lib import Logger
|
||||
|
||||
|
|
@ -512,6 +635,9 @@ def register_creator_plugin(plugin):
|
|||
elif issubclass(plugin, LegacyCreator):
|
||||
register_plugin(LegacyCreator, plugin)
|
||||
|
||||
elif issubclass(plugin, SubsetConvertorPlugin):
|
||||
register_plugin(SubsetConvertorPlugin, plugin)
|
||||
|
||||
|
||||
def deregister_creator_plugin(plugin):
|
||||
if issubclass(plugin, BaseCreator):
|
||||
|
|
@ -520,12 +646,17 @@ def deregister_creator_plugin(plugin):
|
|||
elif issubclass(plugin, LegacyCreator):
|
||||
deregister_plugin(LegacyCreator, plugin)
|
||||
|
||||
elif issubclass(plugin, SubsetConvertorPlugin):
|
||||
deregister_plugin(SubsetConvertorPlugin, plugin)
|
||||
|
||||
|
||||
def register_creator_plugin_path(path):
|
||||
register_plugin_path(BaseCreator, path)
|
||||
register_plugin_path(LegacyCreator, path)
|
||||
register_plugin_path(SubsetConvertorPlugin, path)
|
||||
|
||||
|
||||
def deregister_creator_plugin_path(path):
|
||||
deregister_plugin_path(BaseCreator, path)
|
||||
deregister_plugin_path(LegacyCreator, path)
|
||||
deregister_plugin_path(SubsetConvertorPlugin, path)
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
for repre in instance.data["representations"]:
|
||||
repre_name = str(repre.get("name"))
|
||||
tags = repre.get("tags") or []
|
||||
custom_tags = repre.get("custom_tags")
|
||||
if "review" not in tags:
|
||||
self.log.debug((
|
||||
"Repre: {} - Didn't found \"review\" in tags. Skipping"
|
||||
|
|
@ -158,15 +159,18 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
)
|
||||
continue
|
||||
|
||||
# Filter output definition by representation tags (optional)
|
||||
outputs = self.filter_outputs_by_tags(profile_outputs, tags)
|
||||
# Filter output definition by representation's
|
||||
# custom tags (optional)
|
||||
outputs = self.filter_outputs_by_custom_tags(
|
||||
profile_outputs, custom_tags)
|
||||
if not outputs:
|
||||
self.log.info((
|
||||
"Skipped representation. All output definitions from"
|
||||
" selected profile does not match to representation's"
|
||||
" tags. \"{}\""
|
||||
" custom tags. \"{}\""
|
||||
).format(str(tags)))
|
||||
continue
|
||||
|
||||
outputs_per_representations.append((repre, outputs))
|
||||
return outputs_per_representations
|
||||
|
||||
|
|
@ -1656,7 +1660,9 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
return True
|
||||
return False
|
||||
|
||||
def filter_output_defs(self, profile, subset_name, families):
|
||||
def filter_output_defs(
|
||||
self, profile, subset_name, families
|
||||
):
|
||||
"""Return outputs matching input instance families.
|
||||
|
||||
Output definitions without families filter are marked as valid.
|
||||
|
|
@ -1664,6 +1670,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
Args:
|
||||
profile (dict): Profile from presets matching current context.
|
||||
families (list): All families of current instance.
|
||||
subset_name (str): name of subset
|
||||
|
||||
Returns:
|
||||
list: Containg all output definitions matching entered families.
|
||||
|
|
@ -1711,40 +1718,51 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
|
||||
return filtered_outputs
|
||||
|
||||
def filter_outputs_by_tags(self, outputs, tags):
|
||||
"""Filter output definitions by entered representation tags.
|
||||
def filter_outputs_by_custom_tags(self, outputs, custom_tags):
|
||||
"""Filter output definitions by entered representation custom_tags.
|
||||
|
||||
Output definitions without tags filter are marked as valid.
|
||||
Output definitions without custom_tags filter are marked as invalid,
|
||||
only in case representation is having any custom_tags defined.
|
||||
|
||||
Args:
|
||||
outputs (list): Contain list of output definitions from presets.
|
||||
tags (list): Tags of processed representation.
|
||||
custom_tags (list): Custom Tags of processed representation.
|
||||
|
||||
Returns:
|
||||
list: Containg all output definitions matching entered tags.
|
||||
"""
|
||||
filtered_outputs = []
|
||||
repre_tags_low = [tag.lower() for tag in tags]
|
||||
for output_def in outputs:
|
||||
valid = True
|
||||
output_filters = output_def.get("filter")
|
||||
if output_filters:
|
||||
# Check tag filters
|
||||
tag_filters = output_filters.get("tags")
|
||||
if tag_filters:
|
||||
tag_filters_low = [tag.lower() for tag in tag_filters]
|
||||
valid = False
|
||||
for tag in repre_tags_low:
|
||||
if tag in tag_filters_low:
|
||||
valid = True
|
||||
break
|
||||
|
||||
if not valid:
|
||||
continue
|
||||
filtered_outputs = []
|
||||
repre_c_tags_low = [tag.lower() for tag in (custom_tags or [])]
|
||||
for output_def in outputs:
|
||||
tag_filters = output_def.get("filter", {}).get("custom_tags")
|
||||
|
||||
if not custom_tags and not tag_filters:
|
||||
# Definition is valid if both tags are empty
|
||||
valid = True
|
||||
|
||||
elif not custom_tags or not tag_filters:
|
||||
# Invalid if one is empty
|
||||
valid = False
|
||||
|
||||
else:
|
||||
# Check if output definition tags are in representation tags
|
||||
valid = False
|
||||
# lower all filter tags
|
||||
tag_filters_low = [tag.lower() for tag in tag_filters]
|
||||
# check if any repre tag is not in filter tags
|
||||
for tag in repre_c_tags_low:
|
||||
if tag in tag_filters_low:
|
||||
valid = True
|
||||
break
|
||||
|
||||
if valid:
|
||||
filtered_outputs.append(output_def)
|
||||
|
||||
self.log.debug("__ filtered_outputs: {}".format(
|
||||
[_o["filename_suffix"] for _o in filtered_outputs]
|
||||
))
|
||||
|
||||
return filtered_outputs
|
||||
|
||||
def add_video_filter_args(self, args, inserting_arg):
|
||||
|
|
|
|||
|
|
@ -22,10 +22,6 @@ FFMPEG = (
|
|||
'"{}"%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s'
|
||||
).format(ffmpeg_path)
|
||||
|
||||
FFPROBE = (
|
||||
'"{}" -v quiet -print_format json -show_format -show_streams "%(source)s"'
|
||||
).format(ffprobe_path)
|
||||
|
||||
DRAWTEXT = (
|
||||
"drawtext=fontfile='%(font)s':text=\\'%(text)s\\':"
|
||||
"x=%(x)s:y=%(y)s:fontcolor=%(color)s@%(opacity).1f:fontsize=%(size)d"
|
||||
|
|
@ -48,8 +44,15 @@ def _get_ffprobe_data(source):
|
|||
:param str source: source media file
|
||||
:rtype: [{}, ...]
|
||||
"""
|
||||
command = FFPROBE % {'source': source}
|
||||
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
|
||||
command = [
|
||||
ffprobe_path,
|
||||
"-v", "quiet",
|
||||
"-print_format", "json",
|
||||
"-show_format",
|
||||
"-show_streams",
|
||||
source
|
||||
]
|
||||
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
|
||||
out = proc.communicate()[0]
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError("Failed to run: %s" % command)
|
||||
|
|
@ -113,11 +116,20 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
if not ffprobe_data:
|
||||
ffprobe_data = _get_ffprobe_data(source)
|
||||
|
||||
# Validate 'streams' before calling super to raise more specific
|
||||
# error
|
||||
source_streams = ffprobe_data.get("streams")
|
||||
if not source_streams:
|
||||
raise ValueError((
|
||||
"Input file \"{}\" does not contain any streams"
|
||||
" with image/video content."
|
||||
).format(source))
|
||||
|
||||
self.ffprobe_data = ffprobe_data
|
||||
self.first_frame = first_frame
|
||||
self.input_args = []
|
||||
|
||||
super().__init__(source, ffprobe_data["streams"])
|
||||
super().__init__(source, source_streams)
|
||||
|
||||
if options_init:
|
||||
self.options_init.update(options_init)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@
|
|||
"review",
|
||||
"ftrack"
|
||||
],
|
||||
"subsets": []
|
||||
"subsets": [],
|
||||
"custom_tags": []
|
||||
},
|
||||
"overscan_crop": "",
|
||||
"overscan_color": [
|
||||
|
|
|
|||
|
|
@ -131,6 +131,16 @@
|
|||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateModel": {
|
||||
"enabled": true,
|
||||
"write_color_sets": false,
|
||||
"write_face_sets": false,
|
||||
"defaults": [
|
||||
"Main",
|
||||
"Proxy",
|
||||
"Sculpt"
|
||||
]
|
||||
},
|
||||
"CreatePointCache": {
|
||||
"enabled": true,
|
||||
"write_color_sets": false,
|
||||
|
|
@ -187,16 +197,6 @@
|
|||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateModel": {
|
||||
"enabled": true,
|
||||
"write_color_sets": false,
|
||||
"write_face_sets": false,
|
||||
"defaults": [
|
||||
"Main",
|
||||
"Proxy",
|
||||
"Sculpt"
|
||||
]
|
||||
},
|
||||
"CreateRenderSetup": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
|
|
@ -577,6 +577,10 @@
|
|||
"vrayproxy"
|
||||
]
|
||||
},
|
||||
"ExtractObj": {
|
||||
"enabled": false,
|
||||
"optional": true
|
||||
},
|
||||
"ValidateRigContents": {
|
||||
"enabled": false,
|
||||
"optional": true,
|
||||
|
|
|
|||
|
|
@ -434,7 +434,7 @@
|
|||
}
|
||||
],
|
||||
"extension": "mov",
|
||||
"add_tags": []
|
||||
"add_custom_tags": []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -295,6 +295,15 @@
|
|||
"label": "Subsets",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"key": "custom_tags",
|
||||
"label": "Custom Tags",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -657,6 +657,25 @@
|
|||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "ExtractObj",
|
||||
"label": "Extract OBJ",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "optional",
|
||||
"label": "Optional"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -296,8 +296,8 @@
|
|||
"label": "Write node file type"
|
||||
},
|
||||
{
|
||||
"key": "add_tags",
|
||||
"label": "Add additional tags to representations",
|
||||
"key": "add_custom_tags",
|
||||
"label": "Add custom tags",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,9 @@
|
|||
"overlay-messages": {
|
||||
"close-btn": "#D3D8DE",
|
||||
"bg-success": "#458056",
|
||||
"bg-success-hover": "#55a066"
|
||||
"bg-success-hover": "#55a066",
|
||||
"bg-error": "#AD2E2E",
|
||||
"bg-error-hover": "#C93636"
|
||||
},
|
||||
"tab-widget": {
|
||||
"bg": "#21252B",
|
||||
|
|
|
|||
|
|
@ -688,22 +688,23 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
|||
}
|
||||
|
||||
/* Messages overlay */
|
||||
#OverlayMessageWidget {
|
||||
OverlayMessageWidget {
|
||||
border-radius: 0.2em;
|
||||
background: {color:bg-buttons};
|
||||
}
|
||||
|
||||
#OverlayMessageWidget:hover {
|
||||
background: {color:bg-button-hover};
|
||||
}
|
||||
#OverlayMessageWidget {
|
||||
background: {color:overlay-messages:bg-success};
|
||||
}
|
||||
#OverlayMessageWidget:hover {
|
||||
|
||||
OverlayMessageWidget:hover {
|
||||
background: {color:overlay-messages:bg-success-hover};
|
||||
}
|
||||
|
||||
#OverlayMessageWidget QWidget {
|
||||
OverlayMessageWidget[type="error"] {
|
||||
background: {color:overlay-messages:bg-error};
|
||||
}
|
||||
OverlayMessageWidget[type="error"]:hover {
|
||||
background: {color:overlay-messages:bg-error-hover};
|
||||
}
|
||||
|
||||
OverlayMessageWidget QWidget {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ from Qt import QtCore
|
|||
# ID of context item in instance view
|
||||
CONTEXT_ID = "context"
|
||||
CONTEXT_LABEL = "Options"
|
||||
# Not showed anywhere - used as identifier
|
||||
CONTEXT_GROUP = "__ContextGroup__"
|
||||
|
||||
CONVERTOR_ITEM_GROUP = "Incompatible subsets"
|
||||
|
||||
# Allowed symbols for subset name (and variant)
|
||||
# - characters, numbers, unsercore and dash
|
||||
|
|
@ -17,6 +21,8 @@ SORT_VALUE_ROLE = QtCore.Qt.UserRole + 2
|
|||
IS_GROUP_ROLE = QtCore.Qt.UserRole + 3
|
||||
CREATOR_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 4
|
||||
FAMILY_ROLE = QtCore.Qt.UserRole + 5
|
||||
GROUP_ROLE = QtCore.Qt.UserRole + 6
|
||||
CONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 7
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
|
|||
|
|
@ -31,11 +31,20 @@ from openpype.pipeline.create import (
|
|||
HiddenCreator,
|
||||
Creator,
|
||||
)
|
||||
from openpype.pipeline.create.context import (
|
||||
CreatorsOperationFailed,
|
||||
ConvertorsOperationFailed,
|
||||
)
|
||||
|
||||
# Define constant for plugin orders offset
|
||||
PLUGIN_ORDER_OFFSET = 0.5
|
||||
|
||||
|
||||
class CardMessageTypes:
|
||||
standard = None
|
||||
error = "error"
|
||||
|
||||
|
||||
class MainThreadItem:
|
||||
"""Callback with args and kwargs."""
|
||||
|
||||
|
|
@ -299,8 +308,11 @@ class PublishReport:
|
|||
}
|
||||
|
||||
def _extract_context_data(self, context):
|
||||
context_label = "Context"
|
||||
if context is not None:
|
||||
context_label = context.data.get("label")
|
||||
return {
|
||||
"label": context.data.get("label")
|
||||
"label": context_label
|
||||
}
|
||||
|
||||
def _extract_instance_data(self, instance, exists):
|
||||
|
|
@ -1101,6 +1113,8 @@ class AbstractPublisherController(object):
|
|||
options (Dict[str, Any]): Data from pre-create attributes.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def save_changes(self):
|
||||
"""Save changes in create context."""
|
||||
|
||||
|
|
@ -1234,6 +1248,14 @@ class AbstractPublisherController(object):
|
|||
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def convertor_items(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def trigger_convertor_items(self, convertor_identifiers):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_comment(self, comment):
|
||||
"""Set comment on pyblish context.
|
||||
|
|
@ -1247,7 +1269,9 @@ class AbstractPublisherController(object):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def emit_card_message(self, message):
|
||||
def emit_card_message(
|
||||
self, message, message_type=CardMessageTypes.standard
|
||||
):
|
||||
"""Emit a card message which can have a lifetime.
|
||||
|
||||
This is for UI purposes. Method can be extended to more arguments
|
||||
|
|
@ -1598,6 +1622,10 @@ class PublisherController(BasePublisherController):
|
|||
"""Current instances in create context."""
|
||||
return self._create_context.instances_by_id
|
||||
|
||||
@property
|
||||
def convertor_items(self):
|
||||
return self._create_context.convertor_items_by_id
|
||||
|
||||
@property
|
||||
def _creators(self):
|
||||
"""All creators loaded in create context."""
|
||||
|
|
@ -1662,13 +1690,12 @@ class PublisherController(BasePublisherController):
|
|||
|
||||
def reset(self):
|
||||
"""Reset everything related to creation and publishing."""
|
||||
# Stop publishing
|
||||
self.stop_publish()
|
||||
|
||||
self.save_changes()
|
||||
|
||||
self.host_is_valid = self._create_context.host_is_valid
|
||||
|
||||
self._create_context.reset_preparation()
|
||||
|
||||
# Reset avalon context
|
||||
self._create_context.reset_avalon_context()
|
||||
|
||||
|
|
@ -1679,6 +1706,8 @@ class PublisherController(BasePublisherController):
|
|||
self._reset_publish()
|
||||
self._reset_instances()
|
||||
|
||||
self._create_context.reset_finalization()
|
||||
|
||||
self._emit_event("controller.reset.finished")
|
||||
|
||||
self.emit_card_message("Refreshed..")
|
||||
|
|
@ -1711,15 +1740,54 @@ class PublisherController(BasePublisherController):
|
|||
|
||||
self._create_context.reset_context_data()
|
||||
with self._create_context.bulk_instances_collection():
|
||||
self._create_context.reset_instances()
|
||||
self._create_context.execute_autocreators()
|
||||
try:
|
||||
self._create_context.reset_instances()
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.collection.failed",
|
||||
{
|
||||
"title": "Instance collection failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
self._create_context.find_convertor_items()
|
||||
except ConvertorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"convertors.find.failed",
|
||||
{
|
||||
"title": "Collection of unsupported subset failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
self._create_context.execute_autocreators()
|
||||
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.create.failed",
|
||||
{
|
||||
"title": "AutoCreation failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
self._resetting_instances = False
|
||||
|
||||
self._on_create_instance_change()
|
||||
|
||||
def emit_card_message(self, message):
|
||||
self._emit_event("show.card.message", {"message": message})
|
||||
def emit_card_message(
|
||||
self, message, message_type=CardMessageTypes.standard
|
||||
):
|
||||
self._emit_event(
|
||||
"show.card.message",
|
||||
{
|
||||
"message": message,
|
||||
"message_type": message_type
|
||||
}
|
||||
)
|
||||
|
||||
def get_creator_attribute_definitions(self, instances):
|
||||
"""Collect creator attribute definitions for multuple instances.
|
||||
|
|
@ -1837,27 +1905,76 @@ class PublisherController(BasePublisherController):
|
|||
variant, task_name, asset_doc, project_name, instance=instance
|
||||
)
|
||||
|
||||
def trigger_convertor_items(self, convertor_identifiers):
|
||||
self.save_changes()
|
||||
|
||||
success = True
|
||||
try:
|
||||
self._create_context.run_convertors(convertor_identifiers)
|
||||
|
||||
except ConvertorsOperationFailed as exc:
|
||||
success = False
|
||||
self._emit_event(
|
||||
"convertors.convert.failed",
|
||||
{
|
||||
"title": "Conversion failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
if success:
|
||||
self.emit_card_message("Conversion finished")
|
||||
else:
|
||||
self.emit_card_message("Conversion failed", CardMessageTypes.error)
|
||||
|
||||
self.reset()
|
||||
|
||||
def create(
|
||||
self, creator_identifier, subset_name, instance_data, options
|
||||
):
|
||||
"""Trigger creation and refresh of instances in UI."""
|
||||
creator = self._creators[creator_identifier]
|
||||
creator.create(subset_name, instance_data, options)
|
||||
|
||||
success = True
|
||||
try:
|
||||
self._create_context.create(
|
||||
creator_identifier, subset_name, instance_data, options
|
||||
)
|
||||
except CreatorsOperationFailed as exc:
|
||||
success = False
|
||||
self._emit_event(
|
||||
"instances.create.failed",
|
||||
{
|
||||
"title": "Creation failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
self._on_create_instance_change()
|
||||
return success
|
||||
|
||||
def save_changes(self):
|
||||
"""Save changes happened during creation."""
|
||||
if self._create_context.host_is_valid:
|
||||
if not self._create_context.host_is_valid:
|
||||
return
|
||||
|
||||
try:
|
||||
self._create_context.save_changes()
|
||||
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.save.failed",
|
||||
{
|
||||
"title": "Instances save failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
def remove_instances(self, instance_ids):
|
||||
"""Remove instances based on instance ids.
|
||||
|
||||
Args:
|
||||
instance_ids (List[str]): List of instance ids to remove.
|
||||
"""
|
||||
# TODO expect instance ids instead of instances
|
||||
# QUESTION Expect that instances are really removed? In that case save
|
||||
# reset is not required and save changes too.
|
||||
self.save_changes()
|
||||
|
|
@ -1872,7 +1989,16 @@ class PublisherController(BasePublisherController):
|
|||
instances_by_id[instance_id]
|
||||
for instance_id in instance_ids
|
||||
]
|
||||
self._create_context.remove_instances(instances)
|
||||
try:
|
||||
self._create_context.remove_instances(instances)
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.remove.failed",
|
||||
{
|
||||
"title": "Instance removement failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
def _on_create_instance_change(self):
|
||||
self._emit_event("instances.refresh.finished")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from Qt import QtWidgets
|
||||
|
||||
from .report_items import (
|
||||
PublishReport
|
||||
)
|
||||
|
|
@ -16,4 +18,13 @@ __all__ = (
|
|||
"PublishReportViewerWidget",
|
||||
|
||||
"PublishReportViewerWindow",
|
||||
|
||||
"main",
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
app = QtWidgets.QApplication([])
|
||||
window = PublishReportViewerWindow()
|
||||
window.show()
|
||||
return app.exec_()
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ from .widgets import (
|
|||
)
|
||||
from ..constants import (
|
||||
CONTEXT_ID,
|
||||
CONTEXT_LABEL
|
||||
CONTEXT_LABEL,
|
||||
CONTEXT_GROUP,
|
||||
CONVERTOR_ITEM_GROUP,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -57,15 +59,12 @@ class SelectionTypes:
|
|||
extend_to = SelectionType("extend_to")
|
||||
|
||||
|
||||
class GroupWidget(QtWidgets.QWidget):
|
||||
"""Widget wrapping instances under group."""
|
||||
|
||||
class BaseGroupWidget(QtWidgets.QWidget):
|
||||
selected = QtCore.Signal(str, str, SelectionType)
|
||||
active_changed = QtCore.Signal()
|
||||
removed_selected = QtCore.Signal()
|
||||
|
||||
def __init__(self, group_name, group_icons, parent):
|
||||
super(GroupWidget, self).__init__(parent)
|
||||
def __init__(self, group_name, parent):
|
||||
super(BaseGroupWidget, self).__init__(parent)
|
||||
|
||||
label_widget = QtWidgets.QLabel(group_name, self)
|
||||
|
||||
|
|
@ -86,10 +85,9 @@ class GroupWidget(QtWidgets.QWidget):
|
|||
layout.addLayout(label_layout, 0)
|
||||
|
||||
self._group = group_name
|
||||
self._group_icons = group_icons
|
||||
|
||||
self._widgets_by_id = {}
|
||||
self._ordered_instance_ids = []
|
||||
self._ordered_item_ids = []
|
||||
|
||||
self._label_widget = label_widget
|
||||
self._content_layout = layout
|
||||
|
|
@ -104,7 +102,12 @@ class GroupWidget(QtWidgets.QWidget):
|
|||
|
||||
return self._group
|
||||
|
||||
def get_selected_instance_ids(self):
|
||||
def get_widget_by_item_id(self, item_id):
|
||||
"""Get instance widget by it's id."""
|
||||
|
||||
return self._widgets_by_id.get(item_id)
|
||||
|
||||
def get_selected_item_ids(self):
|
||||
"""Selected instance ids.
|
||||
|
||||
Returns:
|
||||
|
|
@ -139,13 +142,80 @@ class GroupWidget(QtWidgets.QWidget):
|
|||
|
||||
return [
|
||||
self._widgets_by_id[instance_id]
|
||||
for instance_id in self._ordered_instance_ids
|
||||
for instance_id in self._ordered_item_ids
|
||||
]
|
||||
|
||||
def get_widget_by_instance_id(self, instance_id):
|
||||
"""Get instance widget by it's id."""
|
||||
def _remove_all_except(self, item_ids):
|
||||
item_ids = set(item_ids)
|
||||
# Remove instance widgets that are not in passed instances
|
||||
for item_id in tuple(self._widgets_by_id.keys()):
|
||||
if item_id in item_ids:
|
||||
continue
|
||||
|
||||
return self._widgets_by_id.get(instance_id)
|
||||
widget = self._widgets_by_id.pop(item_id)
|
||||
if widget.is_selected:
|
||||
self.removed_selected.emit()
|
||||
|
||||
widget.setVisible(False)
|
||||
self._content_layout.removeWidget(widget)
|
||||
widget.deleteLater()
|
||||
|
||||
def _update_ordered_item_ids(self):
|
||||
ordered_item_ids = []
|
||||
for idx in range(self._content_layout.count()):
|
||||
if idx > 0:
|
||||
item = self._content_layout.itemAt(idx)
|
||||
widget = item.widget()
|
||||
if widget is not None:
|
||||
ordered_item_ids.append(widget.id)
|
||||
|
||||
self._ordered_item_ids = ordered_item_ids
|
||||
|
||||
def _on_widget_selection(self, instance_id, group_id, selection_type):
|
||||
self.selected.emit(instance_id, group_id, selection_type)
|
||||
|
||||
|
||||
class ConvertorItemsGroupWidget(BaseGroupWidget):
|
||||
def update_items(self, items_by_id):
|
||||
items_by_label = collections.defaultdict(list)
|
||||
for item in items_by_id.values():
|
||||
items_by_label[item.label].append(item)
|
||||
|
||||
# Remove instance widgets that are not in passed instances
|
||||
self._remove_all_except(items_by_id.keys())
|
||||
|
||||
# Sort instances by subset name
|
||||
sorted_labels = list(sorted(items_by_label.keys()))
|
||||
|
||||
# Add new instances to widget
|
||||
widget_idx = 1
|
||||
for label in sorted_labels:
|
||||
for item in items_by_label[label]:
|
||||
if item.id in self._widgets_by_id:
|
||||
widget = self._widgets_by_id[item.id]
|
||||
widget.update_item(item)
|
||||
else:
|
||||
widget = ConvertorItemCardWidget(item, self)
|
||||
widget.selected.connect(self._on_widget_selection)
|
||||
self._widgets_by_id[item.id] = widget
|
||||
self._content_layout.insertWidget(widget_idx, widget)
|
||||
widget_idx += 1
|
||||
|
||||
self._update_ordered_item_ids()
|
||||
|
||||
|
||||
class InstanceGroupWidget(BaseGroupWidget):
|
||||
"""Widget wrapping instances under group."""
|
||||
|
||||
active_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, group_icons, *args, **kwargs):
|
||||
super(InstanceGroupWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
self._group_icons = group_icons
|
||||
|
||||
def update_icons(self, group_icons):
|
||||
self._group_icons = group_icons
|
||||
|
||||
def update_instance_values(self):
|
||||
"""Trigger update on instance widgets."""
|
||||
|
|
@ -153,14 +223,6 @@ class GroupWidget(QtWidgets.QWidget):
|
|||
for widget in self._widgets_by_id.values():
|
||||
widget.update_instance_values()
|
||||
|
||||
def confirm_remove_instance_id(self, instance_id):
|
||||
"""Delete widget by instance id."""
|
||||
|
||||
widget = self._widgets_by_id.pop(instance_id)
|
||||
widget.setVisible(False)
|
||||
self._content_layout.removeWidget(widget)
|
||||
widget.deleteLater()
|
||||
|
||||
def update_instances(self, instances):
|
||||
"""Update instances for the group.
|
||||
|
||||
|
|
@ -178,17 +240,7 @@ class GroupWidget(QtWidgets.QWidget):
|
|||
instances_by_subset_name[subset_name].append(instance)
|
||||
|
||||
# Remove instance widgets that are not in passed instances
|
||||
for instance_id in tuple(self._widgets_by_id.keys()):
|
||||
if instance_id in instances_by_id:
|
||||
continue
|
||||
|
||||
widget = self._widgets_by_id.pop(instance_id)
|
||||
if widget.is_selected:
|
||||
self.removed_selected.emit()
|
||||
|
||||
widget.setVisible(False)
|
||||
self._content_layout.removeWidget(widget)
|
||||
widget.deleteLater()
|
||||
self._remove_all_except(instances_by_id.keys())
|
||||
|
||||
# Sort instances by subset name
|
||||
sorted_subset_names = list(sorted(instances_by_subset_name.keys()))
|
||||
|
|
@ -211,18 +263,7 @@ class GroupWidget(QtWidgets.QWidget):
|
|||
self._content_layout.insertWidget(widget_idx, widget)
|
||||
widget_idx += 1
|
||||
|
||||
ordered_instance_ids = []
|
||||
for idx in range(self._content_layout.count()):
|
||||
if idx > 0:
|
||||
item = self._content_layout.itemAt(idx)
|
||||
widget = item.widget()
|
||||
if widget is not None:
|
||||
ordered_instance_ids.append(widget.id)
|
||||
|
||||
self._ordered_instance_ids = ordered_instance_ids
|
||||
|
||||
def _on_widget_selection(self, instance_id, group_id, selection_type):
|
||||
self.selected.emit(instance_id, group_id, selection_type)
|
||||
self._update_ordered_item_ids()
|
||||
|
||||
|
||||
class CardWidget(BaseClickableFrame):
|
||||
|
|
@ -284,7 +325,7 @@ class ContextCardWidget(CardWidget):
|
|||
super(ContextCardWidget, self).__init__(parent)
|
||||
|
||||
self._id = CONTEXT_ID
|
||||
self._group_identifier = ""
|
||||
self._group_identifier = CONTEXT_GROUP
|
||||
|
||||
icon_widget = PublishPixmapLabel(None, self)
|
||||
icon_widget.setObjectName("FamilyIconLabel")
|
||||
|
|
@ -304,6 +345,40 @@ class ContextCardWidget(CardWidget):
|
|||
self._label_widget = label_widget
|
||||
|
||||
|
||||
class ConvertorItemCardWidget(CardWidget):
|
||||
"""Card for global context.
|
||||
|
||||
Is not visually under group widget and is always at the top of card view.
|
||||
"""
|
||||
|
||||
def __init__(self, item, parent):
|
||||
super(ConvertorItemCardWidget, self).__init__(parent)
|
||||
|
||||
self._id = item.id
|
||||
self.identifier = item.identifier
|
||||
self._group_identifier = CONVERTOR_ITEM_GROUP
|
||||
|
||||
icon_widget = IconValuePixmapLabel("fa.magic", self)
|
||||
icon_widget.setObjectName("FamilyIconLabel")
|
||||
|
||||
label_widget = QtWidgets.QLabel(item.label, self)
|
||||
|
||||
icon_layout = QtWidgets.QHBoxLayout()
|
||||
icon_layout.setContentsMargins(10, 5, 5, 5)
|
||||
icon_layout.addWidget(icon_widget)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 5, 10, 5)
|
||||
layout.addLayout(icon_layout, 0)
|
||||
layout.addWidget(label_widget, 1)
|
||||
|
||||
self._icon_widget = icon_widget
|
||||
self._label_widget = label_widget
|
||||
|
||||
def update_instance_values(self):
|
||||
pass
|
||||
|
||||
|
||||
class InstanceCardWidget(CardWidget):
|
||||
"""Card widget representing instance."""
|
||||
|
||||
|
|
@ -481,6 +556,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
self._content_widget = content_widget
|
||||
|
||||
self._context_widget = None
|
||||
self._convertor_items_group = None
|
||||
self._widgets_by_group = {}
|
||||
self._ordered_groups = []
|
||||
|
||||
|
|
@ -513,6 +589,9 @@ class InstanceCardView(AbstractInstanceView):
|
|||
):
|
||||
output.append(self._context_widget)
|
||||
|
||||
if self._convertor_items_group is not None:
|
||||
output.extend(self._convertor_items_group.get_selected_widgets())
|
||||
|
||||
for group_widget in self._widgets_by_group.values():
|
||||
for widget in group_widget.get_selected_widgets():
|
||||
output.append(widget)
|
||||
|
|
@ -526,23 +605,19 @@ class InstanceCardView(AbstractInstanceView):
|
|||
):
|
||||
output.append(CONTEXT_ID)
|
||||
|
||||
if self._convertor_items_group is not None:
|
||||
output.extend(self._convertor_items_group.get_selected_item_ids())
|
||||
|
||||
for group_widget in self._widgets_by_group.values():
|
||||
output.extend(group_widget.get_selected_instance_ids())
|
||||
output.extend(group_widget.get_selected_item_ids())
|
||||
return output
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh instances in view based on CreatedContext."""
|
||||
# Create context item if is not already existing
|
||||
# - this must be as first thing to do as context item should be at the
|
||||
# top
|
||||
if self._context_widget is None:
|
||||
widget = ContextCardWidget(self._content_widget)
|
||||
widget.selected.connect(self._on_widget_selection)
|
||||
|
||||
self._context_widget = widget
|
||||
self._make_sure_context_widget_exists()
|
||||
|
||||
self.selection_changed.emit()
|
||||
self._content_layout.insertWidget(0, widget)
|
||||
self._update_convertor_items_group()
|
||||
|
||||
# Prepare instances by group and identifiers by group
|
||||
instances_by_group = collections.defaultdict(list)
|
||||
|
|
@ -573,17 +648,21 @@ class InstanceCardView(AbstractInstanceView):
|
|||
# Keep track of widget indexes
|
||||
# - we start with 1 because Context item as at the top
|
||||
widget_idx = 1
|
||||
if self._convertor_items_group is not None:
|
||||
widget_idx += 1
|
||||
|
||||
for group_name in sorted_group_names:
|
||||
group_icons = {
|
||||
idenfier: self._controller.get_creator_icon(idenfier)
|
||||
for idenfier in identifiers_by_group[group_name]
|
||||
}
|
||||
if group_name in self._widgets_by_group:
|
||||
group_widget = self._widgets_by_group[group_name]
|
||||
else:
|
||||
group_icons = {
|
||||
idenfier: self._controller.get_creator_icon(idenfier)
|
||||
for idenfier in identifiers_by_group[group_name]
|
||||
}
|
||||
group_widget.update_icons(group_icons)
|
||||
|
||||
group_widget = GroupWidget(
|
||||
group_name, group_icons, self._content_widget
|
||||
else:
|
||||
group_widget = InstanceGroupWidget(
|
||||
group_icons, group_name, self._content_widget
|
||||
)
|
||||
group_widget.active_changed.connect(self._on_active_changed)
|
||||
group_widget.selected.connect(self._on_widget_selection)
|
||||
|
|
@ -595,7 +674,10 @@ class InstanceCardView(AbstractInstanceView):
|
|||
instances_by_group[group_name]
|
||||
)
|
||||
|
||||
ordered_group_names = [""]
|
||||
self._update_ordered_group_nameS()
|
||||
|
||||
def _update_ordered_group_nameS(self):
|
||||
ordered_group_names = [CONTEXT_GROUP]
|
||||
for idx in range(self._content_layout.count()):
|
||||
if idx > 0:
|
||||
item = self._content_layout.itemAt(idx)
|
||||
|
|
@ -605,6 +687,43 @@ class InstanceCardView(AbstractInstanceView):
|
|||
|
||||
self._ordered_groups = ordered_group_names
|
||||
|
||||
def _make_sure_context_widget_exists(self):
|
||||
# Create context item if is not already existing
|
||||
# - this must be as first thing to do as context item should be at the
|
||||
# top
|
||||
if self._context_widget is not None:
|
||||
return
|
||||
|
||||
widget = ContextCardWidget(self._content_widget)
|
||||
widget.selected.connect(self._on_widget_selection)
|
||||
|
||||
self._context_widget = widget
|
||||
|
||||
self.selection_changed.emit()
|
||||
self._content_layout.insertWidget(0, widget)
|
||||
|
||||
def _update_convertor_items_group(self):
|
||||
convertor_items = self._controller.convertor_items
|
||||
if not convertor_items and self._convertor_items_group is None:
|
||||
return
|
||||
|
||||
if not convertor_items:
|
||||
self._convertor_items_group.setVisible(False)
|
||||
self._content_layout.removeWidget(self._convertor_items_group)
|
||||
self._convertor_items_group.deleteLater()
|
||||
self._convertor_items_group = None
|
||||
return
|
||||
|
||||
if self._convertor_items_group is None:
|
||||
group_widget = ConvertorItemsGroupWidget(
|
||||
CONVERTOR_ITEM_GROUP, self._content_widget
|
||||
)
|
||||
group_widget.selected.connect(self._on_widget_selection)
|
||||
self._content_layout.insertWidget(1, group_widget)
|
||||
self._convertor_items_group = group_widget
|
||||
|
||||
self._convertor_items_group.update_items(convertor_items)
|
||||
|
||||
def refresh_instance_states(self):
|
||||
"""Trigger update of instances on group widgets."""
|
||||
for widget in self._widgets_by_group.values():
|
||||
|
|
@ -621,9 +740,13 @@ class InstanceCardView(AbstractInstanceView):
|
|||
"""
|
||||
if instance_id == CONTEXT_ID:
|
||||
new_widget = self._context_widget
|
||||
|
||||
else:
|
||||
group_widget = self._widgets_by_group[group_name]
|
||||
new_widget = group_widget.get_widget_by_instance_id(instance_id)
|
||||
if group_name == CONVERTOR_ITEM_GROUP:
|
||||
group_widget = self._convertor_items_group
|
||||
else:
|
||||
group_widget = self._widgets_by_group[group_name]
|
||||
new_widget = group_widget.get_widget_by_item_id(instance_id)
|
||||
|
||||
if selection_type is SelectionTypes.clear:
|
||||
self._select_item_clear(instance_id, group_name, new_widget)
|
||||
|
|
@ -668,7 +791,10 @@ class InstanceCardView(AbstractInstanceView):
|
|||
if instance_id == CONTEXT_ID:
|
||||
remove_group = True
|
||||
else:
|
||||
group_widget = self._widgets_by_group[group_name]
|
||||
if group_name == CONVERTOR_ITEM_GROUP:
|
||||
group_widget = self._convertor_items_group
|
||||
else:
|
||||
group_widget = self._widgets_by_group[group_name]
|
||||
if not group_widget.get_selected_widgets():
|
||||
remove_group = True
|
||||
|
||||
|
|
@ -749,7 +875,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
|
||||
# If start group is not set then use context item group name
|
||||
if start_group is None:
|
||||
start_group = ""
|
||||
start_group = CONTEXT_GROUP
|
||||
|
||||
# If start instance id is not filled then use context id (similar to
|
||||
# group)
|
||||
|
|
@ -777,10 +903,13 @@ class InstanceCardView(AbstractInstanceView):
|
|||
# Go through ordered groups (from top to bottom) and change selection
|
||||
for name in self._ordered_groups:
|
||||
# Prepare sorted instance widgets
|
||||
if name == "":
|
||||
if name == CONTEXT_GROUP:
|
||||
sorted_widgets = [self._context_widget]
|
||||
else:
|
||||
group_widget = self._widgets_by_group[name]
|
||||
if name == CONVERTOR_ITEM_GROUP:
|
||||
group_widget = self._convertor_items_group
|
||||
else:
|
||||
group_widget = self._widgets_by_group[name]
|
||||
sorted_widgets = group_widget.get_ordered_widgets()
|
||||
|
||||
# Change selection based on explicit selection if start group
|
||||
|
|
@ -892,6 +1021,8 @@ class InstanceCardView(AbstractInstanceView):
|
|||
|
||||
def get_selected_items(self):
|
||||
"""Get selected instance ids and context."""
|
||||
|
||||
convertor_identifiers = []
|
||||
instances = []
|
||||
selected_widgets = self._get_selected_widgets()
|
||||
|
||||
|
|
@ -899,37 +1030,56 @@ class InstanceCardView(AbstractInstanceView):
|
|||
for widget in selected_widgets:
|
||||
if widget is self._context_widget:
|
||||
context_selected = True
|
||||
else:
|
||||
|
||||
elif isinstance(widget, InstanceCardWidget):
|
||||
instances.append(widget.id)
|
||||
|
||||
return instances, context_selected
|
||||
elif isinstance(widget, ConvertorItemCardWidget):
|
||||
convertor_identifiers.append(widget.identifier)
|
||||
|
||||
def set_selected_items(self, instance_ids, context_selected):
|
||||
return instances, context_selected, convertor_identifiers
|
||||
|
||||
def set_selected_items(
|
||||
self, instance_ids, context_selected, convertor_identifiers
|
||||
):
|
||||
s_instance_ids = set(instance_ids)
|
||||
cur_ids, cur_context = self.get_selected_items()
|
||||
s_convertor_identifiers = set(convertor_identifiers)
|
||||
cur_ids, cur_context, cur_convertor_identifiers = (
|
||||
self.get_selected_items()
|
||||
)
|
||||
if (
|
||||
set(cur_ids) == s_instance_ids
|
||||
and cur_context == context_selected
|
||||
and set(cur_convertor_identifiers) == s_convertor_identifiers
|
||||
):
|
||||
return
|
||||
|
||||
selected_groups = []
|
||||
selected_instances = []
|
||||
if context_selected:
|
||||
selected_groups.append("")
|
||||
selected_groups.append(CONTEXT_GROUP)
|
||||
selected_instances.append(CONTEXT_ID)
|
||||
|
||||
self._context_widget.set_selected(context_selected)
|
||||
|
||||
for group_name in self._ordered_groups:
|
||||
if group_name == "":
|
||||
if group_name == CONTEXT_GROUP:
|
||||
continue
|
||||
|
||||
group_widget = self._widgets_by_group[group_name]
|
||||
is_convertor_group = group_name == CONVERTOR_ITEM_GROUP
|
||||
if is_convertor_group:
|
||||
group_widget = self._convertor_items_group
|
||||
else:
|
||||
group_widget = self._widgets_by_group[group_name]
|
||||
|
||||
group_selected = False
|
||||
for widget in group_widget.get_ordered_widgets():
|
||||
select = False
|
||||
if widget.id in s_instance_ids:
|
||||
if is_convertor_group:
|
||||
is_in = widget.identifier in s_convertor_identifiers
|
||||
else:
|
||||
is_in = widget.id in s_instance_ids
|
||||
if is_in:
|
||||
selected_instances.append(widget.id)
|
||||
group_selected = True
|
||||
select = True
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from openpype.pipeline.create import (
|
|||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
TaskNotSetError,
|
||||
)
|
||||
from openpype.tools.utils import ErrorMessageBox
|
||||
|
||||
from .widgets import (
|
||||
IconValuePixmapLabel,
|
||||
|
|
@ -35,79 +34,6 @@ class VariantInputsWidget(QtWidgets.QWidget):
|
|||
self.resized.emit()
|
||||
|
||||
|
||||
class CreateErrorMessageBox(ErrorMessageBox):
|
||||
def __init__(
|
||||
self,
|
||||
creator_label,
|
||||
subset_name,
|
||||
asset_name,
|
||||
exc_msg,
|
||||
formatted_traceback,
|
||||
parent
|
||||
):
|
||||
self._creator_label = creator_label
|
||||
self._subset_name = subset_name
|
||||
self._asset_name = asset_name
|
||||
self._exc_msg = exc_msg
|
||||
self._formatted_traceback = formatted_traceback
|
||||
super(CreateErrorMessageBox, self).__init__("Creation failed", parent)
|
||||
|
||||
def _create_top_widget(self, parent_widget):
|
||||
label_widget = QtWidgets.QLabel(parent_widget)
|
||||
label_widget.setText(
|
||||
"<span style='font-size:18pt;'>Failed to create</span>"
|
||||
)
|
||||
return label_widget
|
||||
|
||||
def _get_report_data(self):
|
||||
report_message = (
|
||||
"{creator}: Failed to create Subset: \"{subset}\""
|
||||
" in Asset: \"{asset}\""
|
||||
"\n\nError: {message}"
|
||||
).format(
|
||||
creator=self._creator_label,
|
||||
subset=self._subset_name,
|
||||
asset=self._asset_name,
|
||||
message=self._exc_msg,
|
||||
)
|
||||
if self._formatted_traceback:
|
||||
report_message += "\n\n{}".format(self._formatted_traceback)
|
||||
return [report_message]
|
||||
|
||||
def _create_content(self, content_layout):
|
||||
item_name_template = (
|
||||
"<span style='font-weight:bold;'>Creator:</span> {}<br>"
|
||||
"<span style='font-weight:bold;'>Subset:</span> {}<br>"
|
||||
"<span style='font-weight:bold;'>Asset:</span> {}<br>"
|
||||
)
|
||||
exc_msg_template = "<span style='font-weight:bold'>{}</span>"
|
||||
|
||||
line = self._create_line()
|
||||
content_layout.addWidget(line)
|
||||
|
||||
item_name_widget = QtWidgets.QLabel(self)
|
||||
item_name_widget.setText(
|
||||
item_name_template.format(
|
||||
self._creator_label, self._subset_name, self._asset_name
|
||||
)
|
||||
)
|
||||
content_layout.addWidget(item_name_widget)
|
||||
|
||||
message_label_widget = QtWidgets.QLabel(self)
|
||||
message_label_widget.setText(
|
||||
exc_msg_template.format(self.convert_text_for_html(self._exc_msg))
|
||||
)
|
||||
content_layout.addWidget(message_label_widget)
|
||||
|
||||
if self._formatted_traceback:
|
||||
line_widget = self._create_line()
|
||||
tb_widget = self._create_traceback_widget(
|
||||
self._formatted_traceback
|
||||
)
|
||||
content_layout.addWidget(line_widget)
|
||||
content_layout.addWidget(tb_widget)
|
||||
|
||||
|
||||
# TODO add creator identifier/label to details
|
||||
class CreatorShortDescWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
|
|
@ -178,8 +104,6 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
|
||||
self._prereq_available = False
|
||||
|
||||
self._message_dialog = None
|
||||
|
||||
name_pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS)
|
||||
self._name_pattern = name_pattern
|
||||
self._compiled_name_pattern = re.compile(name_pattern)
|
||||
|
|
@ -769,7 +693,6 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
index = indexes[0]
|
||||
creator_label = index.data(QtCore.Qt.DisplayRole)
|
||||
creator_identifier = index.data(CREATOR_IDENTIFIER_ROLE)
|
||||
family = index.data(FAMILY_ROLE)
|
||||
variant = self.variant_input.text()
|
||||
|
|
@ -792,40 +715,13 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
"family": family
|
||||
}
|
||||
|
||||
error_msg = None
|
||||
formatted_traceback = None
|
||||
try:
|
||||
self._controller.create(
|
||||
creator_identifier,
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data
|
||||
)
|
||||
success = self._controller.create(
|
||||
creator_identifier,
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data
|
||||
)
|
||||
|
||||
except CreatorError as exc:
|
||||
error_msg = str(exc)
|
||||
|
||||
# Use bare except because some hosts raise their exceptions that
|
||||
# do not inherit from python's `BaseException`
|
||||
except:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
formatted_traceback = "".join(traceback.format_exception(
|
||||
exc_type, exc_value, exc_traceback
|
||||
))
|
||||
error_msg = str(exc_value)
|
||||
|
||||
if error_msg is None:
|
||||
if success:
|
||||
self._set_creator(self._selected_creator)
|
||||
self._controller.emit_card_message("Creation finished...")
|
||||
else:
|
||||
box = CreateErrorMessageBox(
|
||||
creator_label,
|
||||
subset_name,
|
||||
asset_name,
|
||||
error_msg,
|
||||
formatted_traceback,
|
||||
parent=self
|
||||
)
|
||||
box.show()
|
||||
# Store dialog so is not garbage collected before is shown
|
||||
self._message_dialog = box
|
||||
|
|
|
|||
|
|
@ -35,7 +35,10 @@ from ..constants import (
|
|||
SORT_VALUE_ROLE,
|
||||
IS_GROUP_ROLE,
|
||||
CONTEXT_ID,
|
||||
CONTEXT_LABEL
|
||||
CONTEXT_LABEL,
|
||||
GROUP_ROLE,
|
||||
CONVERTER_IDENTIFIER_ROLE,
|
||||
CONVERTOR_ITEM_GROUP,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -330,6 +333,9 @@ class InstanceTreeView(QtWidgets.QTreeView):
|
|||
"""Ids of selected instances."""
|
||||
instance_ids = set()
|
||||
for index in self.selectionModel().selectedIndexes():
|
||||
if index.data(CONVERTER_IDENTIFIER_ROLE) is not None:
|
||||
continue
|
||||
|
||||
instance_id = index.data(INSTANCE_ID_ROLE)
|
||||
if instance_id is not None:
|
||||
instance_ids.add(instance_id)
|
||||
|
|
@ -439,26 +445,35 @@ class InstanceListView(AbstractInstanceView):
|
|||
self._group_items = {}
|
||||
self._group_widgets = {}
|
||||
self._widgets_by_id = {}
|
||||
# Group by instance id for handling of active state
|
||||
self._group_by_instance_id = {}
|
||||
self._context_item = None
|
||||
self._context_widget = None
|
||||
|
||||
self._convertor_group_item = None
|
||||
self._convertor_group_widget = None
|
||||
self._convertor_items_by_id = {}
|
||||
|
||||
self._instance_view = instance_view
|
||||
self._instance_delegate = instance_delegate
|
||||
self._instance_model = instance_model
|
||||
self._proxy_model = proxy_model
|
||||
|
||||
def _on_expand(self, index):
|
||||
group_name = index.data(SORT_VALUE_ROLE)
|
||||
group_widget = self._group_widgets.get(group_name)
|
||||
if group_widget:
|
||||
group_widget.set_expanded(True)
|
||||
self._update_widget_expand_state(index, True)
|
||||
|
||||
def _on_collapse(self, index):
|
||||
group_name = index.data(SORT_VALUE_ROLE)
|
||||
group_widget = self._group_widgets.get(group_name)
|
||||
self._update_widget_expand_state(index, False)
|
||||
|
||||
def _update_widget_expand_state(self, index, expanded):
|
||||
group_name = index.data(GROUP_ROLE)
|
||||
if group_name == CONVERTOR_ITEM_GROUP:
|
||||
group_widget = self._convertor_group_widget
|
||||
else:
|
||||
group_widget = self._group_widgets.get(group_name)
|
||||
|
||||
if group_widget:
|
||||
group_widget.set_expanded(False)
|
||||
group_widget.set_expanded(expanded)
|
||||
|
||||
def _on_toggle_request(self, toggle):
|
||||
selected_instance_ids = self._instance_view.get_selected_instance_ids()
|
||||
|
|
@ -517,6 +532,16 @@ class InstanceListView(AbstractInstanceView):
|
|||
|
||||
def refresh(self):
|
||||
"""Refresh instances in the view."""
|
||||
# Sort view at the end of refresh
|
||||
# - is turned off until any change in view happens
|
||||
sort_at_the_end = False
|
||||
# Create or use already existing context item
|
||||
# - context widget does not change so we don't have to update anything
|
||||
if self._make_sure_context_item_exists():
|
||||
sort_at_the_end = True
|
||||
|
||||
self._update_convertor_items_group()
|
||||
|
||||
# Prepare instances by their groups
|
||||
instances_by_group_name = collections.defaultdict(list)
|
||||
group_names = set()
|
||||
|
|
@ -525,75 +550,12 @@ class InstanceListView(AbstractInstanceView):
|
|||
group_names.add(group_label)
|
||||
instances_by_group_name[group_label].append(instance)
|
||||
|
||||
# Sort view at the end of refresh
|
||||
# - is turned off until any change in view happens
|
||||
sort_at_the_end = False
|
||||
|
||||
# Access to root item of main model
|
||||
root_item = self._instance_model.invisibleRootItem()
|
||||
|
||||
# Create or use already existing context item
|
||||
# - context widget does not change so we don't have to update anything
|
||||
context_item = None
|
||||
if self._context_item is None:
|
||||
sort_at_the_end = True
|
||||
context_item = QtGui.QStandardItem()
|
||||
context_item.setData(0, SORT_VALUE_ROLE)
|
||||
context_item.setData(CONTEXT_ID, INSTANCE_ID_ROLE)
|
||||
|
||||
root_item.appendRow(context_item)
|
||||
|
||||
index = self._instance_model.index(
|
||||
context_item.row(), context_item.column()
|
||||
)
|
||||
proxy_index = self._proxy_model.mapFromSource(index)
|
||||
widget = ListContextWidget(self._instance_view)
|
||||
self._instance_view.setIndexWidget(proxy_index, widget)
|
||||
|
||||
self._context_widget = widget
|
||||
self._context_item = context_item
|
||||
|
||||
# Create new groups based on prepared `instances_by_group_name`
|
||||
new_group_items = []
|
||||
for group_name in group_names:
|
||||
if group_name in self._group_items:
|
||||
continue
|
||||
|
||||
group_item = QtGui.QStandardItem()
|
||||
group_item.setData(group_name, SORT_VALUE_ROLE)
|
||||
group_item.setData(True, IS_GROUP_ROLE)
|
||||
group_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
||||
self._group_items[group_name] = group_item
|
||||
new_group_items.append(group_item)
|
||||
|
||||
# Add new group items to root item if there are any
|
||||
if new_group_items:
|
||||
# Trigger sort at the end
|
||||
if self._make_sure_groups_exists(group_names):
|
||||
sort_at_the_end = True
|
||||
root_item.appendRows(new_group_items)
|
||||
|
||||
# Create widget for each new group item and store it for future usage
|
||||
for group_item in new_group_items:
|
||||
index = self._instance_model.index(
|
||||
group_item.row(), group_item.column()
|
||||
)
|
||||
proxy_index = self._proxy_model.mapFromSource(index)
|
||||
group_name = group_item.data(SORT_VALUE_ROLE)
|
||||
widget = InstanceListGroupWidget(group_name, self._instance_view)
|
||||
widget.expand_changed.connect(self._on_group_expand_request)
|
||||
widget.toggle_requested.connect(self._on_group_toggle_request)
|
||||
self._group_widgets[group_name] = widget
|
||||
self._instance_view.setIndexWidget(proxy_index, widget)
|
||||
|
||||
# Remove groups that are not available anymore
|
||||
for group_name in tuple(self._group_items.keys()):
|
||||
if group_name in group_names:
|
||||
continue
|
||||
|
||||
group_item = self._group_items.pop(group_name)
|
||||
root_item.removeRow(group_item.row())
|
||||
widget = self._group_widgets.pop(group_name)
|
||||
widget.deleteLater()
|
||||
self._remove_groups_except(group_names)
|
||||
|
||||
# Store which groups should be expanded at the end
|
||||
expand_groups = set()
|
||||
|
|
@ -652,6 +614,7 @@ class InstanceListView(AbstractInstanceView):
|
|||
# Create new item and store it as new
|
||||
item = QtGui.QStandardItem()
|
||||
item.setData(instance["subset"], SORT_VALUE_ROLE)
|
||||
item.setData(instance["subset"], GROUP_ROLE)
|
||||
item.setData(instance_id, INSTANCE_ID_ROLE)
|
||||
new_items.append(item)
|
||||
new_items_with_instance.append((item, instance))
|
||||
|
|
@ -717,13 +680,152 @@ class InstanceListView(AbstractInstanceView):
|
|||
|
||||
self._instance_view.expand(proxy_index)
|
||||
|
||||
def _make_sure_context_item_exists(self):
|
||||
if self._context_item is not None:
|
||||
return False
|
||||
|
||||
root_item = self._instance_model.invisibleRootItem()
|
||||
context_item = QtGui.QStandardItem()
|
||||
context_item.setData(0, SORT_VALUE_ROLE)
|
||||
context_item.setData(CONTEXT_ID, INSTANCE_ID_ROLE)
|
||||
|
||||
root_item.appendRow(context_item)
|
||||
|
||||
index = self._instance_model.index(
|
||||
context_item.row(), context_item.column()
|
||||
)
|
||||
proxy_index = self._proxy_model.mapFromSource(index)
|
||||
widget = ListContextWidget(self._instance_view)
|
||||
self._instance_view.setIndexWidget(proxy_index, widget)
|
||||
|
||||
self._context_widget = widget
|
||||
self._context_item = context_item
|
||||
return True
|
||||
|
||||
def _update_convertor_items_group(self):
|
||||
created_new_items = False
|
||||
convertor_items_by_id = self._controller.convertor_items
|
||||
group_item = self._convertor_group_item
|
||||
if not convertor_items_by_id and group_item is None:
|
||||
return created_new_items
|
||||
|
||||
root_item = self._instance_model.invisibleRootItem()
|
||||
if not convertor_items_by_id:
|
||||
root_item.removeRow(group_item.row())
|
||||
self._convertor_group_widget.deleteLater()
|
||||
self._convertor_group_widget = None
|
||||
self._convertor_items_by_id = {}
|
||||
return created_new_items
|
||||
|
||||
if group_item is None:
|
||||
created_new_items = True
|
||||
group_item = QtGui.QStandardItem()
|
||||
group_item.setData(CONVERTOR_ITEM_GROUP, GROUP_ROLE)
|
||||
group_item.setData(1, SORT_VALUE_ROLE)
|
||||
group_item.setData(True, IS_GROUP_ROLE)
|
||||
group_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
||||
|
||||
root_item.appendRow(group_item)
|
||||
|
||||
index = self._instance_model.index(
|
||||
group_item.row(), group_item.column()
|
||||
)
|
||||
proxy_index = self._proxy_model.mapFromSource(index)
|
||||
widget = InstanceListGroupWidget(
|
||||
CONVERTOR_ITEM_GROUP, self._instance_view
|
||||
)
|
||||
widget.toggle_checkbox.setVisible(False)
|
||||
widget.expand_changed.connect(
|
||||
self._on_convertor_group_expand_request
|
||||
)
|
||||
self._instance_view.setIndexWidget(proxy_index, widget)
|
||||
|
||||
self._convertor_group_item = group_item
|
||||
self._convertor_group_widget = widget
|
||||
|
||||
for row in reversed(range(group_item.rowCount())):
|
||||
child_item = group_item.child(row)
|
||||
child_identifier = child_item.data(CONVERTER_IDENTIFIER_ROLE)
|
||||
if child_identifier not in convertor_items_by_id:
|
||||
self._convertor_items_by_id.pop(child_identifier, None)
|
||||
group_item.removeRows(row, 1)
|
||||
|
||||
new_items = []
|
||||
for identifier, convertor_item in convertor_items_by_id.items():
|
||||
item = self._convertor_items_by_id.get(identifier)
|
||||
if item is None:
|
||||
created_new_items = True
|
||||
item = QtGui.QStandardItem(convertor_item.label)
|
||||
new_items.append(item)
|
||||
item.setData(convertor_item.id, INSTANCE_ID_ROLE)
|
||||
item.setData(convertor_item.label, SORT_VALUE_ROLE)
|
||||
item.setData(CONVERTOR_ITEM_GROUP, GROUP_ROLE)
|
||||
item.setData(
|
||||
convertor_item.identifier, CONVERTER_IDENTIFIER_ROLE
|
||||
)
|
||||
self._convertor_items_by_id[identifier] = item
|
||||
|
||||
if new_items:
|
||||
group_item.appendRows(new_items)
|
||||
|
||||
return created_new_items
|
||||
|
||||
def _make_sure_groups_exists(self, group_names):
|
||||
new_group_items = []
|
||||
for group_name in group_names:
|
||||
if group_name in self._group_items:
|
||||
continue
|
||||
|
||||
group_item = QtGui.QStandardItem()
|
||||
group_item.setData(group_name, GROUP_ROLE)
|
||||
group_item.setData(group_name, SORT_VALUE_ROLE)
|
||||
group_item.setData(True, IS_GROUP_ROLE)
|
||||
group_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
||||
self._group_items[group_name] = group_item
|
||||
new_group_items.append(group_item)
|
||||
|
||||
# Add new group items to root item if there are any
|
||||
if not new_group_items:
|
||||
return False
|
||||
|
||||
# Access to root item of main model
|
||||
root_item = self._instance_model.invisibleRootItem()
|
||||
root_item.appendRows(new_group_items)
|
||||
|
||||
# Create widget for each new group item and store it for future usage
|
||||
for group_item in new_group_items:
|
||||
index = self._instance_model.index(
|
||||
group_item.row(), group_item.column()
|
||||
)
|
||||
proxy_index = self._proxy_model.mapFromSource(index)
|
||||
group_name = group_item.data(GROUP_ROLE)
|
||||
widget = InstanceListGroupWidget(group_name, self._instance_view)
|
||||
widget.expand_changed.connect(self._on_group_expand_request)
|
||||
widget.toggle_requested.connect(self._on_group_toggle_request)
|
||||
self._group_widgets[group_name] = widget
|
||||
self._instance_view.setIndexWidget(proxy_index, widget)
|
||||
|
||||
return True
|
||||
|
||||
def _remove_groups_except(self, group_names):
|
||||
# Remove groups that are not available anymore
|
||||
root_item = self._instance_model.invisibleRootItem()
|
||||
for group_name in tuple(self._group_items.keys()):
|
||||
if group_name in group_names:
|
||||
continue
|
||||
|
||||
group_item = self._group_items.pop(group_name)
|
||||
root_item.removeRow(group_item.row())
|
||||
widget = self._group_widgets.pop(group_name)
|
||||
widget.deleteLater()
|
||||
|
||||
def refresh_instance_states(self):
|
||||
"""Trigger update of all instances."""
|
||||
for widget in self._widgets_by_id.values():
|
||||
widget.update_instance_values()
|
||||
|
||||
def _on_active_changed(self, changed_instance_id, new_value):
|
||||
selected_instance_ids, _ = self.get_selected_items()
|
||||
selected_instance_ids, _, _ = self.get_selected_items()
|
||||
|
||||
selected_ids = set()
|
||||
found = False
|
||||
|
|
@ -774,6 +876,16 @@ class InstanceListView(AbstractInstanceView):
|
|||
proxy_index = self._proxy_model.mapFromSource(group_index)
|
||||
self._instance_view.setExpanded(proxy_index, expanded)
|
||||
|
||||
def _on_convertor_group_expand_request(self, _, expanded):
|
||||
group_item = self._convertor_group_item
|
||||
if not group_item:
|
||||
return
|
||||
group_index = self._instance_model.index(
|
||||
group_item.row(), group_item.column()
|
||||
)
|
||||
proxy_index = self._proxy_model.mapFromSource(group_index)
|
||||
self._instance_view.setExpanded(proxy_index, expanded)
|
||||
|
||||
def _on_group_toggle_request(self, group_name, state):
|
||||
if state == QtCore.Qt.PartiallyChecked:
|
||||
return
|
||||
|
|
@ -807,10 +919,17 @@ class InstanceListView(AbstractInstanceView):
|
|||
tuple<list, bool>: Selected instance ids and boolean if context
|
||||
is selected.
|
||||
"""
|
||||
|
||||
instance_ids = []
|
||||
convertor_identifiers = []
|
||||
context_selected = False
|
||||
|
||||
for index in self._instance_view.selectionModel().selectedIndexes():
|
||||
convertor_identifier = index.data(CONVERTER_IDENTIFIER_ROLE)
|
||||
if convertor_identifier is not None:
|
||||
convertor_identifiers.append(convertor_identifier)
|
||||
continue
|
||||
|
||||
instance_id = index.data(INSTANCE_ID_ROLE)
|
||||
if not context_selected and instance_id == CONTEXT_ID:
|
||||
context_selected = True
|
||||
|
|
@ -818,14 +937,20 @@ class InstanceListView(AbstractInstanceView):
|
|||
elif instance_id is not None:
|
||||
instance_ids.append(instance_id)
|
||||
|
||||
return instance_ids, context_selected
|
||||
return instance_ids, context_selected, convertor_identifiers
|
||||
|
||||
def set_selected_items(self, instance_ids, context_selected):
|
||||
def set_selected_items(
|
||||
self, instance_ids, context_selected, convertor_identifiers
|
||||
):
|
||||
s_instance_ids = set(instance_ids)
|
||||
cur_ids, cur_context = self.get_selected_items()
|
||||
s_convertor_identifiers = set(convertor_identifiers)
|
||||
cur_ids, cur_context, cur_convertor_identifiers = (
|
||||
self.get_selected_items()
|
||||
)
|
||||
if (
|
||||
set(cur_ids) == s_instance_ids
|
||||
and cur_context == context_selected
|
||||
and set(cur_convertor_identifiers) == s_convertor_identifiers
|
||||
):
|
||||
return
|
||||
|
||||
|
|
@ -851,20 +976,35 @@ class InstanceListView(AbstractInstanceView):
|
|||
(item.child(row), list(new_parent_items))
|
||||
)
|
||||
|
||||
instance_id = item.data(INSTANCE_ID_ROLE)
|
||||
if not instance_id:
|
||||
convertor_identifier = item.data(CONVERTER_IDENTIFIER_ROLE)
|
||||
|
||||
select = False
|
||||
expand_parent = True
|
||||
if convertor_identifier is not None:
|
||||
if convertor_identifier in s_convertor_identifiers:
|
||||
select = True
|
||||
else:
|
||||
instance_id = item.data(INSTANCE_ID_ROLE)
|
||||
if instance_id == CONTEXT_ID:
|
||||
if context_selected:
|
||||
select = True
|
||||
expand_parent = False
|
||||
|
||||
elif instance_id in s_instance_ids:
|
||||
select = True
|
||||
|
||||
if not select:
|
||||
continue
|
||||
|
||||
if instance_id in s_instance_ids:
|
||||
select_indexes.append(item.index())
|
||||
for parent_item in parent_items:
|
||||
index = parent_item.index()
|
||||
proxy_index = proxy_model.mapFromSource(index)
|
||||
if not view.isExpanded(proxy_index):
|
||||
view.expand(proxy_index)
|
||||
select_indexes.append(item.index())
|
||||
if not expand_parent:
|
||||
continue
|
||||
|
||||
elif context_selected and instance_id == CONTEXT_ID:
|
||||
select_indexes.append(item.index())
|
||||
for parent_item in parent_items:
|
||||
index = parent_item.index()
|
||||
proxy_index = proxy_model.mapFromSource(index)
|
||||
if not view.isExpanded(proxy_index):
|
||||
view.expand(proxy_index)
|
||||
|
||||
selection_model = view.selectionModel()
|
||||
if not select_indexes:
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
main_layout.addWidget(subset_content_widget, 1)
|
||||
|
||||
change_anim = QtCore.QVariantAnimation()
|
||||
change_anim.setStartValue(0)
|
||||
change_anim.setEndValue(self.anim_end_value)
|
||||
change_anim.setStartValue(float(0))
|
||||
change_anim.setEndValue(float(self.anim_end_value))
|
||||
change_anim.setDuration(self.anim_duration)
|
||||
change_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad)
|
||||
|
||||
|
|
@ -124,6 +124,9 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
subset_attributes_widget.instance_context_changed.connect(
|
||||
self._on_instance_context_change
|
||||
)
|
||||
subset_attributes_widget.convert_requested.connect(
|
||||
self._on_convert_requested
|
||||
)
|
||||
|
||||
# --- Controller callbacks ---
|
||||
controller.event_system.add_callback(
|
||||
|
|
@ -201,7 +204,7 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
self.create_requested.emit()
|
||||
|
||||
def _on_delete_clicked(self):
|
||||
instance_ids, _ = self.get_selected_items()
|
||||
instance_ids, _, _ = self.get_selected_items()
|
||||
|
||||
# Ask user if he really wants to remove instances
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
|
|
@ -235,7 +238,9 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
if self._refreshing_instances:
|
||||
return
|
||||
|
||||
instance_ids, context_selected = self.get_selected_items()
|
||||
instance_ids, context_selected, convertor_identifiers = (
|
||||
self.get_selected_items()
|
||||
)
|
||||
|
||||
# Disable delete button if nothing is selected
|
||||
self._delete_btn.setEnabled(len(instance_ids) > 0)
|
||||
|
|
@ -246,7 +251,7 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
for instance_id in instance_ids
|
||||
]
|
||||
self._subset_attributes_widget.set_current_instances(
|
||||
instances, context_selected
|
||||
instances, context_selected, convertor_identifiers
|
||||
)
|
||||
|
||||
def _on_active_changed(self):
|
||||
|
|
@ -264,9 +269,10 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
+ (self._subset_content_layout.spacing() * 2)
|
||||
)
|
||||
)
|
||||
subset_attrs_width = int(float(width) / self.anim_end_value) * value
|
||||
subset_attrs_width = int((float(width) / self.anim_end_value) * value)
|
||||
if subset_attrs_width > width:
|
||||
subset_attrs_width = width
|
||||
|
||||
create_width = width - subset_attrs_width
|
||||
|
||||
self._create_widget.setMinimumWidth(create_width)
|
||||
|
|
@ -314,6 +320,10 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
|
||||
self.instance_context_changed.emit()
|
||||
|
||||
def _on_convert_requested(self):
|
||||
_, _, convertor_identifiers = self.get_selected_items()
|
||||
self._controller.trigger_convertor_items(convertor_identifiers)
|
||||
|
||||
def get_selected_items(self):
|
||||
view = self._subset_views_layout.currentWidget()
|
||||
return view.get_selected_items()
|
||||
|
|
@ -331,8 +341,12 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
else:
|
||||
new_view.refresh_instance_states()
|
||||
|
||||
instance_ids, context_selected = old_view.get_selected_items()
|
||||
new_view.set_selected_items(instance_ids, context_selected)
|
||||
instance_ids, context_selected, convertor_identifiers = (
|
||||
old_view.get_selected_items()
|
||||
)
|
||||
new_view.set_selected_items(
|
||||
instance_ids, context_selected, convertor_identifiers
|
||||
)
|
||||
|
||||
self._subset_views_layout.setCurrentIndex(new_idx)
|
||||
|
||||
|
|
|
|||
|
|
@ -248,13 +248,13 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
hint = self._top_content_widget.minimumSizeHint()
|
||||
end = hint.height()
|
||||
|
||||
self._shrunk_anim.setStartValue(start)
|
||||
self._shrunk_anim.setEndValue(end)
|
||||
self._shrunk_anim.setStartValue(float(start))
|
||||
self._shrunk_anim.setEndValue(float(end))
|
||||
if not anim_is_running:
|
||||
self._shrunk_anim.start()
|
||||
|
||||
def _on_shrunk_anim(self, value):
|
||||
diff = self._top_content_widget.height() - value
|
||||
diff = self._top_content_widget.height() - int(value)
|
||||
if not self._top_content_widget.isVisible():
|
||||
diff -= self._content_layout.spacing()
|
||||
|
||||
|
|
|
|||
|
|
@ -1461,6 +1461,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
|
|||
└───────────────────────────────┘
|
||||
"""
|
||||
instance_context_changed = QtCore.Signal()
|
||||
convert_requested = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(SubsetAttributesWidget, self).__init__(parent)
|
||||
|
|
@ -1479,9 +1480,53 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
|
|||
|
||||
# BOTTOM PART
|
||||
bottom_widget = QtWidgets.QWidget(self)
|
||||
creator_attrs_widget = CreatorAttrsWidget(
|
||||
controller, bottom_widget
|
||||
|
||||
# Wrap Creator attributes to widget to be able add convert button
|
||||
creator_widget = QtWidgets.QWidget(bottom_widget)
|
||||
|
||||
# Convert button widget (with layout to handle stretch)
|
||||
convert_widget = QtWidgets.QWidget(creator_widget)
|
||||
convert_label = QtWidgets.QLabel(creator_widget)
|
||||
# Set the label text with 'setText' to apply html
|
||||
convert_label.setText(
|
||||
(
|
||||
"Found old publishable subsets"
|
||||
" incompatible with new publisher."
|
||||
"<br/><br/>Press the <b>update subsets</b> button"
|
||||
" to automatically update them"
|
||||
" to be able to publish again."
|
||||
)
|
||||
)
|
||||
convert_label.setWordWrap(True)
|
||||
convert_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
convert_btn = QtWidgets.QPushButton(
|
||||
"Update subsets", convert_widget
|
||||
)
|
||||
convert_separator = QtWidgets.QFrame(convert_widget)
|
||||
convert_separator.setObjectName("Separator")
|
||||
convert_separator.setMinimumHeight(1)
|
||||
convert_separator.setMaximumHeight(1)
|
||||
|
||||
convert_layout = QtWidgets.QGridLayout(convert_widget)
|
||||
convert_layout.setContentsMargins(5, 0, 5, 0)
|
||||
convert_layout.setVerticalSpacing(10)
|
||||
convert_layout.addWidget(convert_label, 0, 0, 1, 3)
|
||||
convert_layout.addWidget(convert_btn, 1, 1)
|
||||
convert_layout.addWidget(convert_separator, 2, 0, 1, 3)
|
||||
convert_layout.setColumnStretch(0, 1)
|
||||
convert_layout.setColumnStretch(1, 0)
|
||||
convert_layout.setColumnStretch(2, 1)
|
||||
|
||||
# Creator attributes widget
|
||||
creator_attrs_widget = CreatorAttrsWidget(
|
||||
controller, creator_widget
|
||||
)
|
||||
creator_layout = QtWidgets.QVBoxLayout(creator_widget)
|
||||
creator_layout.setContentsMargins(0, 0, 0, 0)
|
||||
creator_layout.addWidget(convert_widget, 0)
|
||||
creator_layout.addWidget(creator_attrs_widget, 1)
|
||||
|
||||
publish_attrs_widget = PublishPluginAttrsWidget(
|
||||
controller, bottom_widget
|
||||
)
|
||||
|
|
@ -1492,7 +1537,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
|
|||
|
||||
bottom_layout = QtWidgets.QHBoxLayout(bottom_widget)
|
||||
bottom_layout.setContentsMargins(0, 0, 0, 0)
|
||||
bottom_layout.addWidget(creator_attrs_widget, 1)
|
||||
bottom_layout.addWidget(creator_widget, 1)
|
||||
bottom_layout.addWidget(bottom_separator, 0)
|
||||
bottom_layout.addWidget(publish_attrs_widget, 1)
|
||||
|
||||
|
|
@ -1505,6 +1550,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
|
|||
layout.addWidget(top_bottom, 0)
|
||||
layout.addWidget(bottom_widget, 1)
|
||||
|
||||
self._convertor_identifiers = None
|
||||
self._current_instances = None
|
||||
self._context_selected = False
|
||||
self._all_instances_valid = True
|
||||
|
|
@ -1512,9 +1558,12 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
|
|||
global_attrs_widget.instance_context_changed.connect(
|
||||
self._on_instance_context_changed
|
||||
)
|
||||
convert_btn.clicked.connect(self._on_convert_click)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
self._convert_widget = convert_widget
|
||||
|
||||
self.global_attrs_widget = global_attrs_widget
|
||||
|
||||
self.creator_attrs_widget = creator_attrs_widget
|
||||
|
|
@ -1537,7 +1586,12 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
|
|||
|
||||
self.instance_context_changed.emit()
|
||||
|
||||
def set_current_instances(self, instances, context_selected):
|
||||
def _on_convert_click(self):
|
||||
self.convert_requested.emit()
|
||||
|
||||
def set_current_instances(
|
||||
self, instances, context_selected, convertor_identifiers
|
||||
):
|
||||
"""Change currently selected items.
|
||||
|
||||
Args:
|
||||
|
|
@ -1551,10 +1605,13 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
|
|||
all_valid = False
|
||||
break
|
||||
|
||||
s_convertor_identifiers = set(convertor_identifiers)
|
||||
self._convertor_identifiers = s_convertor_identifiers
|
||||
self._current_instances = instances
|
||||
self._context_selected = context_selected
|
||||
self._all_instances_valid = all_valid
|
||||
|
||||
self._convert_widget.setVisible(len(s_convertor_identifiers) > 0)
|
||||
self.global_attrs_widget.set_current_instances(instances)
|
||||
self.creator_attrs_widget.set_current_instances(instances)
|
||||
self.publish_attrs_widget.set_current_instances(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import collections
|
||||
import copy
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype import (
|
||||
|
|
@ -5,6 +7,7 @@ from openpype import (
|
|||
style
|
||||
)
|
||||
from openpype.tools.utils import (
|
||||
ErrorMessageBox,
|
||||
PlaceholderLineEdit,
|
||||
MessageOverlayObject,
|
||||
PixmapLabel,
|
||||
|
|
@ -222,6 +225,12 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
# Floating publish frame
|
||||
publish_frame = PublishFrame(controller, self.footer_border, self)
|
||||
|
||||
errors_dialog_message_timer = QtCore.QTimer()
|
||||
errors_dialog_message_timer.setInterval(100)
|
||||
errors_dialog_message_timer.timeout.connect(
|
||||
self._on_errors_message_timeout
|
||||
)
|
||||
|
||||
help_btn.clicked.connect(self._on_help_click)
|
||||
tabs_widget.tab_changed.connect(self._on_tab_change)
|
||||
overview_widget.active_changed.connect(
|
||||
|
|
@ -259,6 +268,24 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
controller.event_system.add_callback(
|
||||
"show.card.message", self._on_overlay_message
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"instances.collection.failed", self._on_creator_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"instances.save.failed", self._on_creator_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"instances.remove.failed", self._on_creator_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"instances.create.failed", self._on_creator_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"convertors.convert.failed", self._on_convertor_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"convertors.find.failed", self._on_convertor_error
|
||||
)
|
||||
|
||||
# Store extra header widget for TrayPublisher
|
||||
# - can be used to add additional widgets to header between context
|
||||
|
|
@ -298,10 +325,16 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._controller = controller
|
||||
|
||||
self._first_show = True
|
||||
self._reset_on_show = reset_on_show
|
||||
# This is a little bit confusing but 'reset_on_first_show' is too long
|
||||
# forin init
|
||||
self._reset_on_first_show = reset_on_show
|
||||
self._reset_on_show = True
|
||||
self._restart_timer = None
|
||||
self._publish_frame_visible = None
|
||||
|
||||
self._error_messages_to_show = collections.deque()
|
||||
self._errors_dialog_message_timer = errors_dialog_message_timer
|
||||
|
||||
self._set_publish_visibility(False)
|
||||
|
||||
@property
|
||||
|
|
@ -314,19 +347,10 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._first_show = False
|
||||
self._on_first_show()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(PublisherWindow, self).resizeEvent(event)
|
||||
self._update_publish_frame_rect()
|
||||
|
||||
def _on_overlay_message(self, event):
|
||||
self._overlay_object.add_message(event["message"])
|
||||
|
||||
def _on_first_show(self):
|
||||
self.resize(self.default_width, self.default_height)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
if not self._reset_on_show:
|
||||
return
|
||||
|
||||
self._reset_on_show = False
|
||||
# Detach showing - give OS chance to draw the window
|
||||
timer = QtCore.QTimer()
|
||||
timer.setSingleShot(True)
|
||||
|
|
@ -335,6 +359,21 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._restart_timer = timer
|
||||
timer.start()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(PublisherWindow, self).resizeEvent(event)
|
||||
self._update_publish_frame_rect()
|
||||
|
||||
def _on_overlay_message(self, event):
|
||||
self._overlay_object.add_message(
|
||||
event["message"],
|
||||
event.get("message_type")
|
||||
)
|
||||
|
||||
def _on_first_show(self):
|
||||
self.resize(self.default_width, self.default_height)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
self._reset_on_show = self._reset_on_first_show
|
||||
|
||||
def _on_show_restart_timer(self):
|
||||
"""Callback for '_restart_timer' timer."""
|
||||
|
||||
|
|
@ -342,9 +381,13 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self.reset()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._controller.save_changes()
|
||||
self.save_changes()
|
||||
self._reset_on_show = True
|
||||
super(PublisherWindow, self).closeEvent(event)
|
||||
|
||||
def save_changes(self):
|
||||
self._controller.save_changes()
|
||||
|
||||
def reset(self):
|
||||
self._controller.reset()
|
||||
|
||||
|
|
@ -436,7 +479,8 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._update_publish_frame_rect()
|
||||
|
||||
def _on_reset_clicked(self):
|
||||
self._controller.reset()
|
||||
self.save_changes()
|
||||
self.reset()
|
||||
|
||||
def _on_stop_clicked(self):
|
||||
self._controller.stop_publish()
|
||||
|
|
@ -472,7 +516,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._update_publish_details_widget()
|
||||
if (
|
||||
not self._tabs_widget.is_current_tab("create")
|
||||
or not self._tabs_widget.is_current_tab("publish")
|
||||
and not self._tabs_widget.is_current_tab("publish")
|
||||
):
|
||||
self._tabs_widget.set_current_tab("publish")
|
||||
|
||||
|
|
@ -569,3 +613,144 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._publish_frame.move(
|
||||
0, window_size.height() - height
|
||||
)
|
||||
|
||||
def add_error_message_dialog(self, title, failed_info, message_start=None):
|
||||
self._error_messages_to_show.append(
|
||||
(title, failed_info, message_start)
|
||||
)
|
||||
self._errors_dialog_message_timer.start()
|
||||
|
||||
def _on_errors_message_timeout(self):
|
||||
if not self._error_messages_to_show:
|
||||
self._errors_dialog_message_timer.stop()
|
||||
return
|
||||
|
||||
item = self._error_messages_to_show.popleft()
|
||||
title, failed_info, message_start = item
|
||||
dialog = ErrorsMessageBox(
|
||||
title, failed_info, message_start, self
|
||||
)
|
||||
dialog.exec_()
|
||||
dialog.deleteLater()
|
||||
|
||||
def _on_creator_error(self, event):
|
||||
new_failed_info = []
|
||||
for item in event["failed_info"]:
|
||||
new_item = copy.deepcopy(item)
|
||||
new_item["label"] = new_item.pop("creator_label")
|
||||
new_item["identifier"] = new_item.pop("creator_identifier")
|
||||
new_failed_info.append(new_item)
|
||||
self.add_error_message_dialog(event["title"], new_failed_info, "Creator:")
|
||||
|
||||
def _on_convertor_error(self, event):
|
||||
new_failed_info = []
|
||||
for item in event["failed_info"]:
|
||||
new_item = copy.deepcopy(item)
|
||||
new_item["identifier"] = new_item.pop("convertor_identifier")
|
||||
new_failed_info.append(new_item)
|
||||
self.add_error_message_dialog(
|
||||
event["title"], new_failed_info, "Convertor:"
|
||||
)
|
||||
|
||||
|
||||
class ErrorsMessageBox(ErrorMessageBox):
|
||||
def __init__(self, error_title, failed_info, message_start, parent):
|
||||
self._failed_info = failed_info
|
||||
self._message_start = message_start
|
||||
self._info_with_id = [
|
||||
# Id must be string when used in tab widget
|
||||
{"id": str(idx), "info": info}
|
||||
for idx, info in enumerate(failed_info)
|
||||
]
|
||||
self._widgets_by_id = {}
|
||||
self._tabs_widget = None
|
||||
self._stack_layout = None
|
||||
|
||||
super(ErrorsMessageBox, self).__init__(error_title, parent)
|
||||
|
||||
layout = self.layout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
footer_layout = self._footer_widget.layout()
|
||||
footer_layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
def _create_top_widget(self, parent_widget):
|
||||
return None
|
||||
|
||||
def _get_report_data(self):
|
||||
output = []
|
||||
for info in self._failed_info:
|
||||
item_label = info.get("label")
|
||||
item_identifier = info["identifier"]
|
||||
if item_label:
|
||||
report_message = "{} ({})".format(
|
||||
item_label, item_identifier)
|
||||
else:
|
||||
report_message = "{}".format(item_identifier)
|
||||
|
||||
if self._message_start:
|
||||
report_message = "{} {}".format(
|
||||
self._message_start, report_message
|
||||
)
|
||||
|
||||
report_message += "\n\nError: {}".format(info["message"])
|
||||
formatted_traceback = info.get("traceback")
|
||||
if formatted_traceback:
|
||||
report_message += "\n\n{}".format(formatted_traceback)
|
||||
output.append(report_message)
|
||||
return output
|
||||
|
||||
def _create_content(self, content_layout):
|
||||
tabs_widget = PublisherTabsWidget(self)
|
||||
|
||||
stack_widget = QtWidgets.QFrame(self._content_widget)
|
||||
stack_layout = QtWidgets.QStackedLayout(stack_widget)
|
||||
|
||||
first = True
|
||||
for item in self._info_with_id:
|
||||
item_id = item["id"]
|
||||
info = item["info"]
|
||||
message = info["message"]
|
||||
formatted_traceback = info.get("traceback")
|
||||
item_label = info.get("label")
|
||||
if not item_label:
|
||||
item_label = info["identifier"]
|
||||
|
||||
msg_widget = QtWidgets.QWidget(stack_widget)
|
||||
msg_layout = QtWidgets.QVBoxLayout(msg_widget)
|
||||
|
||||
exc_msg_template = "<span style='font-weight:bold'>{}</span>"
|
||||
message_label_widget = QtWidgets.QLabel(msg_widget)
|
||||
message_label_widget.setText(
|
||||
exc_msg_template.format(self.convert_text_for_html(message))
|
||||
)
|
||||
msg_layout.addWidget(message_label_widget, 0)
|
||||
|
||||
if formatted_traceback:
|
||||
line_widget = self._create_line(msg_widget)
|
||||
tb_widget = self._create_traceback_widget(formatted_traceback)
|
||||
msg_layout.addWidget(line_widget, 0)
|
||||
msg_layout.addWidget(tb_widget, 0)
|
||||
|
||||
msg_layout.addStretch(1)
|
||||
|
||||
tabs_widget.add_tab(item_label, item_id)
|
||||
stack_layout.addWidget(msg_widget)
|
||||
if first:
|
||||
first = False
|
||||
stack_layout.setCurrentWidget(msg_widget)
|
||||
|
||||
self._widgets_by_id[item_id] = msg_widget
|
||||
|
||||
content_layout.addWidget(tabs_widget, 0)
|
||||
content_layout.addWidget(stack_widget, 1)
|
||||
|
||||
tabs_widget.tab_changed.connect(self._on_tab_change)
|
||||
|
||||
self._tabs_widget = tabs_widget
|
||||
self._stack_layout = stack_layout
|
||||
|
||||
def _on_tab_change(self, old_identifier, identifier):
|
||||
widget = self._widgets_by_id[identifier]
|
||||
self._stack_layout.setCurrentWidget(widget)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from .widgets import (
|
|||
ExpandBtn,
|
||||
PixmapLabel,
|
||||
IconButton,
|
||||
SeparatorWidget,
|
||||
)
|
||||
from .views import DeselectableTreeView
|
||||
from .error_dialog import ErrorMessageBox
|
||||
|
|
@ -37,6 +38,7 @@ __all__ = (
|
|||
"ExpandBtn",
|
||||
"PixmapLabel",
|
||||
"IconButton",
|
||||
"SeparatorWidget",
|
||||
|
||||
"DeselectableTreeView",
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from Qt import QtWidgets, QtCore
|
||||
|
||||
from .widgets import ClickableFrame, ExpandBtn
|
||||
from .widgets import ClickableFrame, ExpandBtn, SeparatorWidget
|
||||
|
||||
|
||||
def convert_text_for_html(text):
|
||||
def escape_text_for_html(text):
|
||||
return (
|
||||
text
|
||||
.replace("<", "<")
|
||||
|
|
@ -19,7 +19,7 @@ class TracebackWidget(QtWidgets.QWidget):
|
|||
|
||||
# Modify text to match html
|
||||
# - add more replacements when needed
|
||||
tb_text = convert_text_for_html(tb_text)
|
||||
tb_text = escape_text_for_html(tb_text)
|
||||
expand_btn = ExpandBtn(self)
|
||||
|
||||
clickable_frame = ClickableFrame(self)
|
||||
|
|
@ -85,17 +85,20 @@ class ErrorMessageBox(QtWidgets.QDialog):
|
|||
copy_report_btn = QtWidgets.QPushButton("Copy report", self)
|
||||
ok_btn = QtWidgets.QPushButton("OK", self)
|
||||
|
||||
footer_layout = QtWidgets.QHBoxLayout()
|
||||
footer_widget = QtWidgets.QWidget(self)
|
||||
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
footer_layout.addWidget(copy_report_btn, 0)
|
||||
footer_layout.addStretch(1)
|
||||
footer_layout.addWidget(ok_btn, 0)
|
||||
|
||||
bottom_line = self._create_line()
|
||||
body_layout = QtWidgets.QVBoxLayout(self)
|
||||
body_layout.addWidget(top_widget, 0)
|
||||
body_layout.addWidget(content_scroll, 1)
|
||||
body_layout.addWidget(bottom_line, 0)
|
||||
body_layout.addLayout(footer_layout, 0)
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
if top_widget is not None:
|
||||
main_layout.addWidget(top_widget, 0)
|
||||
main_layout.addWidget(content_scroll, 1)
|
||||
main_layout.addWidget(bottom_line, 0)
|
||||
main_layout.addWidget(footer_widget, 0)
|
||||
|
||||
copy_report_btn.clicked.connect(self._on_copy_report)
|
||||
ok_btn.clicked.connect(self._on_ok_clicked)
|
||||
|
|
@ -106,11 +109,13 @@ class ErrorMessageBox(QtWidgets.QDialog):
|
|||
if not report_data:
|
||||
copy_report_btn.setVisible(False)
|
||||
|
||||
self._content_scroll = content_scroll
|
||||
self._footer_widget = footer_widget
|
||||
self._report_data = report_data
|
||||
|
||||
@staticmethod
|
||||
def convert_text_for_html(text):
|
||||
return convert_text_for_html(text)
|
||||
return escape_text_for_html(text)
|
||||
|
||||
def _create_top_widget(self, parent_widget):
|
||||
label_widget = QtWidgets.QLabel(parent_widget)
|
||||
|
|
@ -131,7 +136,8 @@ class ErrorMessageBox(QtWidgets.QDialog):
|
|||
self.close()
|
||||
|
||||
def _on_copy_report(self):
|
||||
report_text = (10 * "*").join(self._report_data)
|
||||
sep = "\n{}\n".format(10 * "*")
|
||||
report_text = sep.join(self._report_data)
|
||||
|
||||
mime_data = QtCore.QMimeData()
|
||||
mime_data.setText(report_text)
|
||||
|
|
@ -139,12 +145,10 @@ class ErrorMessageBox(QtWidgets.QDialog):
|
|||
mime_data
|
||||
)
|
||||
|
||||
def _create_line(self):
|
||||
line = QtWidgets.QFrame(self)
|
||||
line.setObjectName("Separator")
|
||||
line.setMinimumHeight(2)
|
||||
line.setMaximumHeight(2)
|
||||
return line
|
||||
def _create_line(self, parent=None):
|
||||
if parent is None:
|
||||
parent = self
|
||||
return SeparatorWidget(2, parent=parent)
|
||||
|
||||
def _create_traceback_widget(self, traceback_text, parent=None):
|
||||
if parent is None:
|
||||
|
|
|
|||
|
|
@ -448,3 +448,57 @@ class OptionDialog(QtWidgets.QDialog):
|
|||
|
||||
def parse(self):
|
||||
return self._options.copy()
|
||||
|
||||
|
||||
class SeparatorWidget(QtWidgets.QFrame):
|
||||
"""Prepared widget that can be used as separator with predefined color.
|
||||
|
||||
Args:
|
||||
size (int): Size of separator (width or height).
|
||||
orientation (Qt.Horizontal|Qt.Vertical): Orintation of widget.
|
||||
parent (QtWidgets.QWidget): Parent widget.
|
||||
"""
|
||||
|
||||
def __init__(self, size=2, orientation=QtCore.Qt.Horizontal, parent=None):
|
||||
super(SeparatorWidget, self).__init__(parent)
|
||||
|
||||
self.setObjectName("Separator")
|
||||
|
||||
maximum_width = self.maximumWidth()
|
||||
maximum_height = self.maximumHeight()
|
||||
|
||||
self._size = None
|
||||
self._orientation = orientation
|
||||
self._maximum_width = maximum_width
|
||||
self._maximum_height = maximum_height
|
||||
self.set_size(size)
|
||||
|
||||
def set_size(self, size):
|
||||
if size == self._size:
|
||||
return
|
||||
if self._orientation == QtCore.Qt.Vertical:
|
||||
self.setMinimumWidth(size)
|
||||
self.setMaximumWidth(size)
|
||||
else:
|
||||
self.setMinimumHeight(size)
|
||||
self.setMaximumHeight(size)
|
||||
|
||||
self._size = size
|
||||
|
||||
def set_orientation(self, orientation):
|
||||
if self._orientation == orientation:
|
||||
return
|
||||
|
||||
# Reset min/max sizes in opossite direction
|
||||
if self._orientation == QtCore.Qt.Vertical:
|
||||
self.setMinimumHeight(0)
|
||||
self.setMaximumHeight(self._maximum_height)
|
||||
else:
|
||||
self.setMinimumWidth(0)
|
||||
self.setMaximumWidth(self._maximum_width)
|
||||
|
||||
self._orientation = orientation
|
||||
|
||||
size = self._size
|
||||
self._size = None
|
||||
self.set_size(size)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.14.4"
|
||||
__version__ = "3.14.6-nightly.1"
|
||||
|
|
|
|||
40
tools/run_publish_report_viewer.ps1
Normal file
40
tools/run_publish_report_viewer.ps1
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Helper script OpenPype Tray.
|
||||
|
||||
.DESCRIPTION
|
||||
|
||||
|
||||
.EXAMPLE
|
||||
|
||||
PS> .\run_tray.ps1
|
||||
|
||||
#>
|
||||
$current_dir = Get-Location
|
||||
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
|
||||
$openpype_root = (Get-Item $script_dir).parent.FullName
|
||||
|
||||
# Install PSWriteColor to support colorized output to terminal
|
||||
$env:PSModulePath = $env:PSModulePath + ";$($openpype_root)\tools\modules\powershell"
|
||||
|
||||
$env:_INSIDE_OPENPYPE_TOOL = "1"
|
||||
|
||||
# make sure Poetry is in PATH
|
||||
if (-not (Test-Path 'env:POETRY_HOME')) {
|
||||
$env:POETRY_HOME = "$openpype_root\.poetry"
|
||||
}
|
||||
$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin"
|
||||
|
||||
Set-Location -Path $openpype_root
|
||||
|
||||
Write-Color -Text ">>> ", "Reading Poetry ... " -Color Green, Gray -NoNewline
|
||||
if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) {
|
||||
Write-Color -Text "NOT FOUND" -Color Yellow
|
||||
Write-Color -Text "*** ", "We need to install Poetry create virtual env first ..." -Color Yellow, Gray
|
||||
& "$openpype_root\tools\create_env.ps1"
|
||||
} else {
|
||||
Write-Color -Text "OK" -Color Green
|
||||
}
|
||||
|
||||
& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\start.py" publish-report-viewer --debug
|
||||
Set-Location -Path $current_dir
|
||||
|
|
@ -47,10 +47,14 @@ Context discovers creator and publish plugins. Trigger collections of existing i
|
|||
|
||||
Creator plugins can call **creator_adds_instance** or **creator_removed_instance** to add/remove instances but these methods are not meant to be called directly out of the creator. The reason is that it is the creator's responsibility to remove metadata or decide if it should remove the instance.
|
||||
|
||||
#### Required functions in host implementation
|
||||
Host implementation **must** implement **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now only data about enabled/disabled optional publish plugins is stored there. When data is not stored and loaded properly, reset of publishing will cause that they will be set to default value. Context data also parsed to json string similarly as instance data.
|
||||
During reset are re-cached Creator plugins, re-collected instances, refreshed host context and more. Object of `CreateContext` supply shared data during the reset. They can be used by creators to share same data needed during collection phase or during creation for autocreators.
|
||||
|
||||
There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return a string shown in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/<b>{asset name}</b>/{task name}"`.
|
||||
#### Required functions in host implementation
|
||||
It is recommended to use `HostBase` class (`from openpype.host import HostBase`) as base for host implementation with combination of `IPublishHost` interface (`from openpype.host import IPublishHost`). These abstract classes should guide you to fill missing attributes and methods.
|
||||
|
||||
To sum them and in case host implementation is inheriting `HostBase` the implementation **must** implement **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now only data about enabled/disabled optional publish plugins is stored there. When data is not stored and loaded properly, reset of publishing will cause that they will be set to default value. Context data also parsed to json string similarly as instance data.
|
||||
|
||||
There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return a string shown in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/<b>{asset name}</b>/{task name}"` (this is default implementation in `HostBase`).
|
||||
|
||||
Another optional function is **get_current_context**. This function is handy in hosts where it is possible to open multiple workfiles in one process so using global context variables is not relevant because artists can switch between opened workfiles without being acknowledged. When a function is not implemented or won't return the right keys the global context is used.
|
||||
```json
|
||||
|
|
@ -68,6 +72,9 @@ Main responsibility of create plugin is to create, update, collect and remove in
|
|||
#### *BaseCreator*
|
||||
Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **HiddenCreator**, **AutoCreator** and **Creator** variants.
|
||||
|
||||
**Access to shared data**
|
||||
Functions to work with "Collection shared data" can be used during reset phase of `CreateContext`. Creators can cache there data that are common for them. For example list of nodes in scene. Methods are implemented on `CreateContext` but their usage is primarily for Create plugins as nothing else should use it. Each creator can access `collection_shared_data` attribute which is a dictionary where shared data can be stored.
|
||||
|
||||
**Abstractions**
|
||||
- **`family`** (class attr) - Tells what kind of instance will be created.
|
||||
```python
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue