Merge branch 'develop' of github.com:pypeclub/OpenPype into feature/OP-4180_Anatomy-not-using-settings-functions

This commit is contained in:
Petr Kalis 2022-10-13 15:33:49 +02:00
commit a13dc92178
64 changed files with 2845 additions and 1873 deletions

View file

@ -312,6 +312,8 @@ class IPublishHost:
required = [
"get_context_data",
"update_context_data",
"get_context_title",
"get_current_context",
]
missing = []
for name in required:

View file

@ -3,7 +3,7 @@ from typing import List
import bpy
import pyblish.api
import openpype.api
import openpype.hosts.blender.api.action
from openpype.pipeline.publish import ValidateContentsOrder

View file

@ -3,14 +3,15 @@ from typing import List
import bpy
import pyblish.api
import openpype.api
from openpype.pipeline.publish import ValidateContentsOrder
import openpype.hosts.blender.api.action
class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
"""Validate that the current mesh has UV's."""
order = openpype.api.ValidateContentsOrder
order = ValidateContentsOrder
hosts = ["blender"]
families = ["model"]
category = "geometry"

View file

@ -3,14 +3,15 @@ from typing import List
import bpy
import pyblish.api
import openpype.api
from openpype.pipeline.publish import ValidateContentsOrder
import openpype.hosts.blender.api.action
class ValidateMeshNoNegativeScale(pyblish.api.Validator):
"""Ensure that meshes don't have a negative scale."""
order = openpype.api.ValidateContentsOrder
order = ValidateContentsOrder
hosts = ["blender"]
families = ["model"]
category = "geometry"

View file

@ -3,7 +3,7 @@ from typing import List
import bpy
import pyblish.api
import openpype.api
import openpype.hosts.blender.api.action
from openpype.pipeline.publish import ValidateContentsOrder

View file

@ -4,7 +4,7 @@ import mathutils
import bpy
import pyblish.api
import openpype.api
import openpype.hosts.blender.api.action
from openpype.pipeline.publish import ValidateContentsOrder

View file

@ -73,7 +73,7 @@ class ImageLoader(load.LoaderPlugin):
# Imprint it manually
data = {
"schema": "avalon-core:container-2.0",
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": node_name,
"namespace": namespace,

View file

@ -43,7 +43,7 @@ class USDSublayerLoader(load.LoaderPlugin):
# Imprint it manually
data = {
"schema": "avalon-core:container-2.0",
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": node_name,
"namespace": namespace,

View file

@ -43,7 +43,7 @@ class USDReferenceLoader(load.LoaderPlugin):
# Imprint it manually
data = {
"schema": "avalon-core:container-2.0",
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": node_name,
"namespace": namespace,

View file

@ -28,13 +28,16 @@ class MayaAddon(OpenPypeModule, IHostAddon):
env["PYTHONPATH"] = os.pathsep.join(new_python_paths)
# Set default values if are not already set via settings
defaults = {
"OPENPYPE_LOG_NO_COLORS": "Yes"
# Set default environments
envs = {
"OPENPYPE_LOG_NO_COLORS": "Yes",
# For python module 'qtpy'
"QT_API": "PySide2",
# For python module 'Qt'
"QT_PREFERRED_BINDING": "PySide2"
}
for key, value in defaults.items():
if not env.get(key):
env[key] = value
for key, value in envs.items():
env[key] = value
def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:

View file

@ -8,7 +8,7 @@ from functools import partial
import maya.cmds as cmds
import maya.mel as mel
from openpype.api import resources
from openpype import resources
from openpype.tools.utils import host_tools
from .lib import get_main_window

View file

@ -2459,182 +2459,120 @@ def bake_to_world_space(nodes,
def load_capture_preset(data=None):
"""Convert OpenPype Extract Playblast settings to `capture` arguments
Input data is the settings from:
`project_settings/maya/publish/ExtractPlayblast/capture_preset`
Args:
data (dict): Capture preset settings from OpenPype settings
Returns:
dict: `capture.capture` compatible keyword arguments
"""
import capture
preset = data
options = dict()
viewport_options = dict()
viewport2_options = dict()
camera_options = dict()
# CODEC
id = 'Codec'
for key in preset[id]:
options[str(key)] = preset[id][key]
# Straight key-value match from settings to capture arguments
options.update(data["Codec"])
options.update(data["Generic"])
options.update(data["Resolution"])
# GENERIC
id = 'Generic'
for key in preset[id]:
options[str(key)] = preset[id][key]
# RESOLUTION
id = 'Resolution'
options['height'] = preset[id]['height']
options['width'] = preset[id]['width']
camera_options.update(data['Camera Options'])
viewport_options.update(data["Renderer"])
# DISPLAY OPTIONS
id = 'Display Options'
disp_options = {}
for key in preset[id]:
for key, value in data['Display Options'].items():
if key.startswith('background'):
disp_options[key] = preset['Display Options'][key]
if len(disp_options[key]) == 4:
disp_options[key][0] = (float(disp_options[key][0])/255)
disp_options[key][1] = (float(disp_options[key][1])/255)
disp_options[key][2] = (float(disp_options[key][2])/255)
disp_options[key].pop()
# Convert background, backgroundTop, backgroundBottom colors
if len(value) == 4:
# Ignore alpha + convert RGB to float
value = [
float(value[0]) / 255,
float(value[1]) / 255,
float(value[2]) / 255
]
disp_options[key] = value
else:
disp_options['displayGradient'] = True
options['display_options'] = disp_options
# VIEWPORT OPTIONS
temp_options = {}
id = 'Renderer'
for key in preset[id]:
temp_options[str(key)] = preset[id][key]
# Viewport Options has a mixture of Viewport2 Options and Viewport Options
# to pass along to capture. So we'll need to differentiate between the two
VIEWPORT2_OPTIONS = {
"textureMaxResolution",
"renderDepthOfField",
"ssaoEnable",
"ssaoSamples",
"ssaoAmount",
"ssaoRadius",
"ssaoFilterRadius",
"hwFogStart",
"hwFogEnd",
"hwFogAlpha",
"hwFogFalloff",
"hwFogColorR",
"hwFogColorG",
"hwFogColorB",
"hwFogDensity",
"motionBlurEnable",
"motionBlurSampleCount",
"motionBlurShutterOpenFraction",
"lineAAEnable"
}
for key, value in data['Viewport Options'].items():
temp_options2 = {}
id = 'Viewport Options'
for key in preset[id]:
# There are some keys we want to ignore
if key in {"override_viewport_options", "high_quality"}:
continue
# First handle special cases where we do value conversion to
# separate option values
if key == 'textureMaxResolution':
if preset[id][key] > 0:
temp_options2['textureMaxResolution'] = preset[id][key]
temp_options2['enableTextureMaxRes'] = True
temp_options2['textureMaxResMode'] = 1
viewport2_options['textureMaxResolution'] = value
if value > 0:
viewport2_options['enableTextureMaxRes'] = True
viewport2_options['textureMaxResMode'] = 1
else:
temp_options2['textureMaxResolution'] = preset[id][key]
temp_options2['enableTextureMaxRes'] = False
temp_options2['textureMaxResMode'] = 0
viewport2_options['enableTextureMaxRes'] = False
viewport2_options['textureMaxResMode'] = 0
if key == 'multiSample':
if preset[id][key] > 0:
temp_options2['multiSampleEnable'] = True
temp_options2['multiSampleCount'] = preset[id][key]
else:
temp_options2['multiSampleEnable'] = False
temp_options2['multiSampleCount'] = preset[id][key]
elif key == 'multiSample':
viewport2_options['multiSampleEnable'] = value > 0
viewport2_options['multiSampleCount'] = value
if key == 'renderDepthOfField':
temp_options2['renderDepthOfField'] = preset[id][key]
elif key == 'alphaCut':
viewport2_options['transparencyAlgorithm'] = 5
viewport2_options['transparencyQuality'] = 1
if key == 'ssaoEnable':
if preset[id][key] is True:
temp_options2['ssaoEnable'] = True
else:
temp_options2['ssaoEnable'] = False
elif key == 'hwFogFalloff':
# Settings enum value string to integer
viewport2_options['hwFogFalloff'] = int(value)
if key == 'ssaoSamples':
temp_options2['ssaoSamples'] = preset[id][key]
if key == 'ssaoAmount':
temp_options2['ssaoAmount'] = preset[id][key]
if key == 'ssaoRadius':
temp_options2['ssaoRadius'] = preset[id][key]
if key == 'hwFogDensity':
temp_options2['hwFogDensity'] = preset[id][key]
if key == 'ssaoFilterRadius':
temp_options2['ssaoFilterRadius'] = preset[id][key]
if key == 'alphaCut':
temp_options2['transparencyAlgorithm'] = 5
temp_options2['transparencyQuality'] = 1
if key == 'headsUpDisplay':
temp_options['headsUpDisplay'] = True
if key == 'fogging':
temp_options['fogging'] = preset[id][key] or False
if key == 'hwFogStart':
temp_options2['hwFogStart'] = preset[id][key]
if key == 'hwFogEnd':
temp_options2['hwFogEnd'] = preset[id][key]
if key == 'hwFogAlpha':
temp_options2['hwFogAlpha'] = preset[id][key]
if key == 'hwFogFalloff':
temp_options2['hwFogFalloff'] = int(preset[id][key])
if key == 'hwFogColorR':
temp_options2['hwFogColorR'] = preset[id][key]
if key == 'hwFogColorG':
temp_options2['hwFogColorG'] = preset[id][key]
if key == 'hwFogColorB':
temp_options2['hwFogColorB'] = preset[id][key]
if key == 'motionBlurEnable':
if preset[id][key] is True:
temp_options2['motionBlurEnable'] = True
else:
temp_options2['motionBlurEnable'] = False
if key == 'motionBlurSampleCount':
temp_options2['motionBlurSampleCount'] = preset[id][key]
if key == 'motionBlurShutterOpenFraction':
temp_options2['motionBlurShutterOpenFraction'] = preset[id][key]
if key == 'lineAAEnable':
if preset[id][key] is True:
temp_options2['lineAAEnable'] = True
else:
temp_options2['lineAAEnable'] = False
# Then handle Viewport 2.0 Options
elif key in VIEWPORT2_OPTIONS:
viewport2_options[key] = value
# Then assume remainder is Viewport Options
else:
temp_options[str(key)] = preset[id][key]
viewport_options[key] = value
for key in ['override_viewport_options',
'high_quality',
'alphaCut',
'gpuCacheDisplayFilter',
'multiSample',
'ssaoEnable',
'ssaoSamples',
'ssaoAmount',
'ssaoFilterRadius',
'ssaoRadius',
'hwFogStart',
'hwFogEnd',
'hwFogAlpha',
'hwFogFalloff',
'hwFogColorR',
'hwFogColorG',
'hwFogColorB',
'hwFogDensity',
'textureMaxResolution',
'motionBlurEnable',
'motionBlurSampleCount',
'motionBlurShutterOpenFraction',
'lineAAEnable',
'renderDepthOfField'
]:
temp_options.pop(key, None)
options['viewport_options'] = temp_options
options['viewport2_options'] = temp_options2
options['viewport_options'] = viewport_options
options['viewport2_options'] = viewport2_options
options['camera_options'] = camera_options
# use active sound track
scene = capture.parse_active_scene()
options['sound'] = scene['sound']
# options['display_options'] = temp_options
return options

View file

@ -80,7 +80,7 @@ IMAGE_PREFIXES = {
"mayahardware2": "defaultRenderGlobals.imageFilePrefix"
}
RENDERMAN_IMAGE_DIR = "maya/<scene>/<layer>"
RENDERMAN_IMAGE_DIR = "<scene>/<layer>"
def has_tokens(string, tokens):

View file

@ -29,7 +29,7 @@ class RenderSettings(object):
_image_prefixes = {
'vray': get_current_project_settings()["maya"]["RenderSettings"]["vray_renderer"]["image_prefix"], # noqa
'arnold': get_current_project_settings()["maya"]["RenderSettings"]["arnold_renderer"]["image_prefix"], # noqa
'renderman': 'maya/<Scene>/<layer>/<layer>{aov_separator}<aov>',
'renderman': '<Scene>/<layer>/<layer>{aov_separator}<aov>',
'redshift': get_current_project_settings()["maya"]["RenderSettings"]["redshift_renderer"]["image_prefix"] # noqa
}

View file

@ -34,14 +34,15 @@ class ExtractLayout(publish.Extractor):
for asset in cmds.sets(str(instance), query=True):
# Find the container
grp_name = asset.split(':')[0]
containers = cmds.ls(f"{grp_name}*_CON")
containers = cmds.ls("{}*_CON".format(grp_name))
assert len(containers) == 1, \
f"More than one container found for {asset}"
"More than one container found for {}".format(asset)
container = containers[0]
representation_id = cmds.getAttr(f"{container}.representation")
representation_id = cmds.getAttr(
"{}.representation".format(container))
representation = get_representation_by_id(
project_name,
@ -56,7 +57,8 @@ class ExtractLayout(publish.Extractor):
json_element = {
"family": family,
"instance_name": cmds.getAttr(f"{container}.name"),
"instance_name": cmds.getAttr(
"{}.namespace".format(container)),
"representation": str(representation_id),
"version": str(version_id)
}

View file

@ -77,8 +77,10 @@ class ExtractPlayblast(publish.Extractor):
preset['height'] = asset_height
preset['start_frame'] = start
preset['end_frame'] = end
camera_option = preset.get("camera_option", {})
camera_option["depthOfField"] = cmds.getAttr(
# Enforce persisting camera depth of field
camera_options = preset.setdefault("camera_options", {})
camera_options["depthOfField"] = cmds.getAttr(
"{0}.depthOfField".format(camera))
stagingdir = self.staging_dir(instance)

View file

@ -1,5 +1,6 @@
import os
import glob
import tempfile
import capture
@ -81,9 +82,17 @@ class ExtractThumbnail(publish.Extractor):
elif asset_width and asset_height:
preset['width'] = asset_width
preset['height'] = asset_height
stagingDir = self.staging_dir(instance)
# Create temp directory for thumbnail
# - this is to avoid "override" of source file
dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_")
self.log.debug(
"Create temp directory {} for thumbnail".format(dst_staging)
)
# Store new staging to cleanup paths
instance.context.data["cleanupFullPaths"].append(dst_staging)
filename = "{0}".format(instance.name)
path = os.path.join(stagingDir, filename)
path = os.path.join(dst_staging, filename)
self.log.info("Outputting images to %s" % path)
@ -137,7 +146,7 @@ class ExtractThumbnail(publish.Extractor):
'name': 'thumbnail',
'ext': 'jpg',
'files': thumbnail,
"stagingDir": stagingDir,
"stagingDir": dst_staging,
"thumbnail": True
}
instance.data["representations"].append(representation)

View file

@ -118,7 +118,7 @@ def preview_fname(folder, scene, layer, padding, ext):
"""
# Following hardcoded "<Scene>/<Scene>_<Layer>/<Layer>"
output = "maya/{scene}/{layer}/{layer}.{number}.{ext}".format(
output = "{scene}/{layer}/{layer}.{number}.{ext}".format(
scene=scene,
layer=layer,
number="#" * padding,

View file

@ -22,10 +22,10 @@ def get_redshift_image_format_labels():
class ValidateRenderSettings(pyblish.api.InstancePlugin):
"""Validates the global render settings
* File Name Prefix must start with: `maya/<Scene>`
* File Name Prefix must start with: `<Scene>`
all other token are customizable but sane values for Arnold are:
`maya/<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>`
`<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>`
<Camera> token is supported also, useful for multiple renderable
cameras per render layer.
@ -64,12 +64,12 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
}
ImagePrefixTokens = {
'mentalray': 'maya/<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>', # noqa: E501
'arnold': 'maya/<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>', # noqa: E501
'redshift': 'maya/<Scene>/<RenderLayer>/<RenderLayer>',
'vray': 'maya/<Scene>/<Layer>/<Layer>',
'mentalray': '<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>', # noqa: E501
'arnold': '<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>', # noqa: E501
'redshift': '<Scene>/<RenderLayer>/<RenderLayer>',
'vray': '<Scene>/<Layer>/<Layer>',
'renderman': '<layer>{aov_separator}<aov>.<f4>.<ext>',
'mayahardware2': 'maya/<Scene>/<RenderLayer>/<RenderLayer>',
'mayahardware2': '<Scene>/<RenderLayer>/<RenderLayer>',
}
_aov_chars = {
@ -80,7 +80,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
redshift_AOV_prefix = "<BeautyPath>/<BeautyFile>{aov_separator}<RenderPass>" # noqa: E501
renderman_dir_prefix = "maya/<scene>/<layer>"
renderman_dir_prefix = "<scene>/<layer>"
R_AOV_TOKEN = re.compile(
r'%a|<aov>|<renderpass>', re.IGNORECASE)
@ -90,8 +90,8 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
R_SCENE_TOKEN = re.compile(r'%s|<scene>', re.IGNORECASE)
DEFAULT_PADDING = 4
VRAY_PREFIX = "maya/<Scene>/<Layer>/<Layer>"
DEFAULT_PREFIX = "maya/<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>"
VRAY_PREFIX = "<Scene>/<Layer>/<Layer>"
DEFAULT_PREFIX = "<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>"
def process(self, instance):
@ -123,7 +123,6 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
prefix = prefix.replace(
"{aov_separator}", instance.data.get("aovSeparator", "_"))
required_prefix = "maya/<scene>"
default_prefix = cls.ImagePrefixTokens[renderer]
if not anim_override:
@ -131,15 +130,6 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
cls.log.error("Animation needs to be enabled. Use the same "
"frame for start and end to render single frame")
if renderer != "renderman" and not prefix.lower().startswith(
required_prefix):
invalid = True
cls.log.error(
("Wrong image prefix [ {} ] "
" - doesn't start with: '{}'").format(
prefix, required_prefix)
)
if not re.search(cls.R_LAYER_TOKEN, prefix):
invalid = True
cls.log.error("Wrong image prefix [ {} ] - "

View file

@ -1,7 +1,7 @@
import os
import nuke
from openpype.api import resources
from openpype import resources
from .lib import maintained_selection

View file

@ -20,15 +20,11 @@ class StaticMeshAlembicLoader(plugin.Loader):
icon = "cube"
color = "orange"
def get_task(self, filename, asset_dir, asset_name, replace):
@staticmethod
def get_task(filename, asset_dir, asset_name, replace, default_conversion):
task = unreal.AssetImportTask()
options = unreal.AbcImportSettings()
sm_settings = unreal.AbcStaticMeshSettings()
conversion_settings = unreal.AbcConversionSettings(
preset=unreal.AbcConversionPreset.CUSTOM,
flip_u=False, flip_v=False,
rotation=[0.0, 0.0, 0.0],
scale=[1.0, 1.0, 1.0])
task.set_editor_property('filename', filename)
task.set_editor_property('destination_path', asset_dir)
@ -44,13 +40,20 @@ class StaticMeshAlembicLoader(plugin.Loader):
sm_settings.set_editor_property('merge_meshes', True)
if not default_conversion:
conversion_settings = unreal.AbcConversionSettings(
preset=unreal.AbcConversionPreset.CUSTOM,
flip_u=False, flip_v=False,
rotation=[0.0, 0.0, 0.0],
scale=[1.0, 1.0, 1.0])
options.conversion_settings = conversion_settings
options.static_mesh_settings = sm_settings
options.conversion_settings = conversion_settings
task.options = options
return task
def load(self, context, name, namespace, data):
def load(self, context, name, namespace, options):
"""Load and containerise representation into Content Browser.
This is two step process. First, import FBX to temporary path and
@ -82,6 +85,10 @@ class StaticMeshAlembicLoader(plugin.Loader):
asset_name = "{}".format(name)
version = context.get('version').get('name')
default_conversion = False
if options.get("default_conversion"):
default_conversion = options.get("default_conversion")
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
f"{root}/{asset}/{name}_v{version:03d}", suffix="")
@ -91,7 +98,8 @@ class StaticMeshAlembicLoader(plugin.Loader):
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
unreal.EditorAssetLibrary.make_directory(asset_dir)
task = self.get_task(self.fname, asset_dir, asset_name, False)
task = self.get_task(
self.fname, asset_dir, asset_name, False, default_conversion)
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501

View file

@ -0,0 +1,418 @@
import json
from pathlib import Path
import unreal
from unreal import EditorLevelLibrary
from bson.objectid import ObjectId
from openpype import pipeline
from openpype.pipeline import (
discover_loader_plugins,
loaders_from_representation,
load_container,
get_representation_path,
AVALON_CONTAINER_ID,
legacy_io,
)
from openpype.api import get_current_project_settings
from openpype.hosts.unreal.api import plugin
from openpype.hosts.unreal.api import pipeline as upipeline
class ExistingLayoutLoader(plugin.Loader):
"""
Load Layout for an existing scene, and match the existing assets.
"""
families = ["layout"]
representations = ["json"]
label = "Load Layout on Existing Scene"
icon = "code-fork"
color = "orange"
ASSET_ROOT = "/Game/OpenPype"
@staticmethod
def _create_container(
asset_name, asset_dir, asset, representation, parent, family
):
container_name = f"{asset_name}_CON"
container = None
if not unreal.EditorAssetLibrary.does_asset_exist(
f"{asset_dir}/{container_name}"
):
container = upipeline.create_container(container_name, asset_dir)
else:
ar = unreal.AssetRegistryHelpers.get_asset_registry()
obj = ar.get_asset_by_object_path(
f"{asset_dir}/{container_name}.{container_name}")
container = obj.get_asset()
data = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"asset": asset,
"namespace": asset_dir,
"container_name": container_name,
"asset_name": asset_name,
# "loader": str(self.__class__.__name__),
"representation": representation,
"parent": parent,
"family": family
}
upipeline.imprint(
"{}/{}".format(asset_dir, container_name), data)
return container.get_path_name()
@staticmethod
def _get_current_level():
ue_version = unreal.SystemLibrary.get_engine_version().split('.')
ue_major = ue_version[0]
if ue_major == '4':
return EditorLevelLibrary.get_editor_world()
elif ue_major == '5':
return unreal.LevelEditorSubsystem().get_current_level()
raise NotImplementedError(
f"Unreal version {ue_major} not supported")
def _get_transform(self, ext, import_data, lasset):
conversion = unreal.Matrix.IDENTITY.transform()
fbx_tuning = unreal.Matrix.IDENTITY.transform()
basis = unreal.Matrix(
lasset.get('basis')[0],
lasset.get('basis')[1],
lasset.get('basis')[2],
lasset.get('basis')[3]
).transform()
transform = unreal.Matrix(
lasset.get('transform_matrix')[0],
lasset.get('transform_matrix')[1],
lasset.get('transform_matrix')[2],
lasset.get('transform_matrix')[3]
).transform()
# Check for the conversion settings. We cannot access
# the alembic conversion settings, so we assume that
# the maya ones have been applied.
if ext == '.fbx':
loc = import_data.import_translation
rot = import_data.import_rotation.to_vector()
scale = import_data.import_uniform_scale
conversion = unreal.Transform(
location=[loc.x, loc.y, loc.z],
rotation=[rot.x, rot.y, rot.z],
scale=[-scale, scale, scale]
)
fbx_tuning = unreal.Transform(
rotation=[180.0, 0.0, 90.0],
scale=[1.0, 1.0, 1.0]
)
elif ext == '.abc':
# This is the standard conversion settings for
# alembic files from Maya.
conversion = unreal.Transform(
location=[0.0, 0.0, 0.0],
rotation=[0.0, 0.0, 0.0],
scale=[1.0, -1.0, 1.0]
)
new_transform = (basis.inverse() * transform * basis)
return fbx_tuning * conversion.inverse() * new_transform
def _spawn_actor(self, obj, lasset):
actor = EditorLevelLibrary.spawn_actor_from_object(
obj, unreal.Vector(0.0, 0.0, 0.0)
)
actor.set_actor_label(lasset.get('instance_name'))
smc = actor.get_editor_property('static_mesh_component')
mesh = smc.get_editor_property('static_mesh')
import_data = mesh.get_editor_property('asset_import_data')
filename = import_data.get_first_filename()
path = Path(filename)
transform = self._get_transform(
path.suffix, import_data, lasset)
actor.set_actor_transform(transform, False, True)
@staticmethod
def _get_fbx_loader(loaders, family):
name = ""
if family == 'rig':
name = "SkeletalMeshFBXLoader"
elif family == 'model' or family == 'staticMesh':
name = "StaticMeshFBXLoader"
elif family == 'camera':
name = "CameraLoader"
if name == "":
return None
for loader in loaders:
if loader.__name__ == name:
return loader
return None
@staticmethod
def _get_abc_loader(loaders, family):
name = ""
if family == 'rig':
name = "SkeletalMeshAlembicLoader"
elif family == 'model':
name = "StaticMeshAlembicLoader"
if name == "":
return None
for loader in loaders:
if loader.__name__ == name:
return loader
return None
def _load_asset(self, representation, version, instance_name, family):
valid_formats = ['fbx', 'abc']
repr_data = legacy_io.find_one({
"type": "representation",
"parent": ObjectId(version),
"name": {"$in": valid_formats}
})
repr_format = repr_data.get('name')
all_loaders = discover_loader_plugins()
loaders = loaders_from_representation(
all_loaders, representation)
loader = None
if repr_format == 'fbx':
loader = self._get_fbx_loader(loaders, family)
elif repr_format == 'abc':
loader = self._get_abc_loader(loaders, family)
if not loader:
self.log.error(f"No valid loader found for {representation}")
return []
# This option is necessary to avoid importing the assets with a
# different conversion compared to the other assets. For ABC files,
# it is in fact impossible to access the conversion settings. So,
# we must assume that the Maya conversion settings have been applied.
options = {
"default_conversion": True
}
assets = load_container(
loader,
representation,
namespace=instance_name,
options=options
)
return assets
def _process(self, lib_path):
data = get_current_project_settings()
delete_unmatched = data["unreal"]["delete_unmatched_assets"]
ar = unreal.AssetRegistryHelpers.get_asset_registry()
actors = EditorLevelLibrary.get_all_level_actors()
with open(lib_path, "r") as fp:
data = json.load(fp)
layout_data = []
# Get all the representations in the JSON from the database.
for element in data:
if element.get('representation'):
layout_data.append((
pipeline.legacy_io.find_one({
"_id": ObjectId(element.get('representation'))
}),
element
))
containers = []
actors_matched = []
for (repr_data, lasset) in layout_data:
if not repr_data:
raise AssertionError("Representation not found")
if not (repr_data.get('data') or
repr_data.get('data').get('path')):
raise AssertionError("Representation does not have path")
if not repr_data.get('context'):
raise AssertionError("Representation does not have context")
# For every actor in the scene, check if it has a representation in
# those we got from the JSON. If so, create a container for it.
# Otherwise, remove it from the scene.
found = False
for actor in actors:
if not actor.get_class().get_name() == 'StaticMeshActor':
continue
if actor in actors_matched:
continue
# Get the original path of the file from which the asset has
# been imported.
smc = actor.get_editor_property('static_mesh_component')
mesh = smc.get_editor_property('static_mesh')
import_data = mesh.get_editor_property('asset_import_data')
filename = import_data.get_first_filename()
path = Path(filename)
if (not path.name or
path.name not in repr_data.get('data').get('path')):
continue
actor.set_actor_label(lasset.get('instance_name'))
mesh_path = Path(mesh.get_path_name()).parent.as_posix()
# Create the container for the asset.
asset = repr_data.get('context').get('asset')
subset = repr_data.get('context').get('subset')
container = self._create_container(
f"{asset}_{subset}", mesh_path, asset,
repr_data.get('_id'), repr_data.get('parent'),
repr_data.get('context').get('family')
)
containers.append(container)
# Set the transform for the actor.
transform = self._get_transform(
path.suffix, import_data, lasset)
actor.set_actor_transform(transform, False, True)
actors_matched.append(actor)
found = True
break
# If an actor has not been found for this representation,
# we check if it has been loaded already by checking all the
# loaded containers. If so, we add it to the scene. Otherwise,
# we load it.
if found:
continue
all_containers = upipeline.ls()
loaded = False
for container in all_containers:
repr = container.get('representation')
if not repr == str(repr_data.get('_id')):
continue
asset_dir = container.get('namespace')
filter = unreal.ARFilter(
class_names=["StaticMesh"],
package_paths=[asset_dir],
recursive_paths=False)
assets = ar.get_assets(filter)
for asset in assets:
obj = asset.get_asset()
self._spawn_actor(obj, lasset)
loaded = True
break
# If the asset has not been loaded yet, we load it.
if loaded:
continue
assets = self._load_asset(
lasset.get('representation'),
lasset.get('version'),
lasset.get('instance_name'),
lasset.get('family')
)
for asset in assets:
obj = ar.get_asset_by_object_path(asset).get_asset()
if not obj.get_class().get_name() == 'StaticMesh':
continue
self._spawn_actor(obj, lasset)
break
# Check if an actor was not matched to a representation.
# If so, remove it from the scene.
for actor in actors:
if not actor.get_class().get_name() == 'StaticMeshActor':
continue
if actor not in actors_matched:
self.log.warning(f"Actor {actor.get_name()} not matched.")
if delete_unmatched:
EditorLevelLibrary.destroy_actor(actor)
return containers
def load(self, context, name, namespace, options):
print("Loading Layout and Match Assets")
asset = context.get('asset').get('name')
asset_name = f"{asset}_{name}" if asset else name
container_name = f"{asset}_{name}_CON"
curr_level = self._get_current_level()
if not curr_level:
raise AssertionError("Current level not saved")
containers = self._process(self.fname)
curr_level_path = Path(
curr_level.get_outer().get_path_name()).parent.as_posix()
if not unreal.EditorAssetLibrary.does_asset_exist(
f"{curr_level_path}/{container_name}"
):
upipeline.create_container(
container=container_name, path=curr_level_path)
data = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"asset": asset,
"namespace": curr_level_path,
"container_name": container_name,
"asset_name": asset_name,
"loader": str(self.__class__.__name__),
"representation": context["representation"]["_id"],
"parent": context["representation"]["parent"],
"family": context["representation"]["context"]["family"],
"loaded_assets": containers
}
upipeline.imprint(f"{curr_level_path}/{container_name}", data)
def update(self, container, representation):
asset_dir = container.get('namespace')
source_path = get_representation_path(representation)
containers = self._process(source_path)
data = {
"representation": str(representation["_id"]),
"parent": str(representation["parent"]),
"loaded_assets": containers
}
upipeline.imprint(
"{}/{}".format(asset_dir, container.get('container_name')), data)

View file

@ -37,6 +37,15 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
This is not applicable for 'studio' processing where host application is
called to process uploaded workfile and render frames itself.
For each task configure what properties should resulting instance have
based on uploaded files:
- uploading sequence of 'png' >> create instance of 'render' family,
by adding 'review' to 'Families' and 'Create review' to Tags it will
produce review.
There might be difference between single(>>image) and sequence(>>render)
uploaded files.
"""
# must be really early, context values are only in json file
order = pyblish.api.CollectorOrder - 0.490
@ -46,6 +55,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
# from Settings
task_type_to_family = []
sync_next_version = False # find max version to be published, use for all
def process(self, context):
batch_dir = context.data["batchDir"]
@ -64,6 +74,9 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
task_type = context.data["taskType"]
project_name = context.data["project_name"]
variant = context.data["variant"]
next_versions = []
instances = []
for task_dir in task_subfolders:
task_data = parse_json(os.path.join(task_dir,
"manifest.json"))
@ -90,11 +103,14 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
version = self._get_next_version(
project_name, asset_doc, subset_name
)
next_versions.append(version)
instance = context.create_instance(subset_name)
instance.data["asset"] = asset_name
instance.data["subset"] = subset_name
# set configurable result family
instance.data["family"] = family
# set configurable additional families
instance.data["families"] = families
instance.data["version"] = version
instance.data["stagingDir"] = tempfile.mkdtemp()
@ -137,8 +153,18 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
instance.data["handleStart"] = asset_doc["data"]["handleStart"]
instance.data["handleEnd"] = asset_doc["data"]["handleEnd"]
instances.append(instance)
self.log.info("instance.data:: {}".format(instance.data))
if not self.sync_next_version:
return
# overwrite specific version with same version for all
max_next_version = max(next_versions)
for inst in instances:
inst.data["version"] = max_next_version
self.log.debug("overwritten version:: {}".format(max_next_version))
def _get_subset_name(self, family, subset_template, task_name, variant):
fill_pairs = {
"variant": variant,
@ -176,7 +202,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
"ext": ext[1:],
"files": files,
"stagingDir": task_dir,
"tags": tags
"tags": tags # configurable tags from Settings
}
self.log.info("sequences repre_data.data:: {}".format(repre_data))
return [repre_data]

View file

@ -48,7 +48,7 @@ def _validate_deadline_bool_value(instance, attribute, value):
@attr.s
class MayaPluginInfo:
class MayaPluginInfo(object):
SceneFile = attr.ib(default=None) # Input
OutputFilePath = attr.ib(default=None) # Output directory and filename
OutputFilePrefix = attr.ib(default=None)
@ -63,7 +63,7 @@ class MayaPluginInfo:
@attr.s
class PythonPluginInfo:
class PythonPluginInfo(object):
ScriptFile = attr.ib()
Version = attr.ib(default="3.6")
Arguments = attr.ib(default=None)
@ -71,7 +71,7 @@ class PythonPluginInfo:
@attr.s
class VRayPluginInfo:
class VRayPluginInfo(object):
InputFilename = attr.ib(default=None) # Input
SeparateFilesPerFrame = attr.ib(default=None)
VRayEngine = attr.ib(default="V-Ray")
@ -82,7 +82,7 @@ class VRayPluginInfo:
@attr.s
class ArnoldPluginInfo:
class ArnoldPluginInfo(object):
ArnoldFile = attr.ib(default=None)
@ -762,10 +762,10 @@ def _format_tiles(
Example::
Image prefix is:
`maya/<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>`
`<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>`
Result for tile 0 for 4x4 will be:
`maya/<Scene>/<RenderLayer>/_tile_1x1_4x4_<RenderLayer>_<RenderPass>`
`<Scene>/<RenderLayer>/_tile_1x1_4x4_<RenderLayer>_<RenderPass>`
Calculating coordinates is tricky as in Job they are defined as top,
left, bottom, right with zero being in top-left corner. But Assembler

View file

@ -30,7 +30,7 @@ from .workfile import (
from . import (
legacy_io,
register_loader_plugin_path,
register_inventory_action,
register_inventory_action_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
)
@ -197,7 +197,7 @@ def install_openpype_plugins(project_name=None, host_name=None):
pyblish.api.register_plugin_path(path)
register_loader_plugin_path(path)
register_creator_plugin_path(path)
register_inventory_action(path)
register_inventory_action_path(path)
def uninstall_host():

View file

@ -265,6 +265,10 @@ def get_last_workfile_with_version(
if not match:
continue
if not match.groups():
output_filenames.append(filename)
continue
file_version = int(match.group(1))
if version is None or file_version > version:
output_filenames[:] = []

View file

@ -418,6 +418,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
subset_group = instance.data.get("subsetGroup")
if subset_group:
data["subsetGroup"] = subset_group
elif existing_subset_doc:
# Preserve previous subset group if new version does not set it
if "subsetGroup" in existing_subset_doc.get("data", {}):
subset_group = existing_subset_doc["data"]["subsetGroup"]
data["subsetGroup"] = subset_group
subset_id = None
if existing_subset_doc:

View file

@ -1,3 +1,13 @@
""" Integrate Thumbnails for Openpype use in Loaders.
This thumbnail is different from 'thumbnail' representation which could
be uploaded to Ftrack, or used as any other representation in Loaders to
pull into a scene.
This one is used only as image describing content of published item and
shows up only in Loader in right column section.
"""
import os
import sys
import errno
@ -12,7 +22,7 @@ from openpype.client.operations import OperationsSession, new_thumbnail_doc
class IntegrateThumbnails(pyblish.api.InstancePlugin):
"""Integrate Thumbnails."""
"""Integrate Thumbnails for Openpype use in Loaders."""
label = "Integrate Thumbnails"
order = pyblish.api.IntegratorOrder + 0.01

View file

@ -0,0 +1,72 @@
""" Marks thumbnail representation for integrate to DB or not.
Some hosts produce thumbnail representation, most of them do not create
them explicitly, but they created during extract phase.
In some cases it might be useful to override implicit setting for host/task
This plugin needs to run after extract phase, but before integrate.py as
thumbnail is part of review family and integrated there.
It should be better to control integration of thumbnail in one place than
configure it in multiple places on host implementations.
"""
import pyblish.api
from openpype.lib.profiles_filtering import filter_profiles
class PreIntegrateThumbnails(pyblish.api.InstancePlugin):
"""Marks thumbnail representation for integrate to DB or not."""
label = "Override Integrate Thumbnail Representations"
order = pyblish.api.IntegratorOrder - 0.1
families = ["review"]
integrate_profiles = {}
def process(self, instance):
repres = instance.data.get("representations")
if not repres:
return
thumbnail_repre = None
for repre in repres:
if repre["name"] == "thumbnail":
thumbnail_repre = repre
break
if not thumbnail_repre:
return
family = instance.data["family"]
subset_name = instance.data["subset"]
host_name = instance.context.data["hostName"]
anatomy_data = instance.data["anatomyData"]
task = anatomy_data.get("task", {})
found_profile = filter_profiles(
self.integrate_profiles,
{
"hosts": host_name,
"task_names": task.get("name"),
"task_types": task.get("type"),
"families": family,
"subsets": subset_name,
},
logger=self.log
)
if not found_profile:
return
if not found_profile["integrate_thumbnail"]:
if "delete" not in thumbnail_repre["tags"]:
thumbnail_repre["tags"].append("delete")
else:
if "delete" in thumbnail_repre["tags"]:
thumbnail_repre["tags"].remove("delete")
self.log.debug(
"Thumbnail repre tags {}".format(thumbnail_repre["tags"]))

View file

@ -164,6 +164,10 @@
}
]
},
"PreIntegrateThumbnails": {
"enabled": true,
"integrate_profiles": []
},
"IntegrateSubsetGroup": {
"subset_grouping_profiles": [
{

View file

@ -21,7 +21,7 @@
"viewTransform": "sRGB gamma"
}
},
"mel_workspace": "workspace -fr \"shaders\" \"renderData/shaders\";\nworkspace -fr \"images\" \"renders\";\nworkspace -fr \"particles\" \"particles\";\nworkspace -fr \"mayaAscii\" \"\";\nworkspace -fr \"mayaBinary\" \"\";\nworkspace -fr \"scene\" \"\";\nworkspace -fr \"alembicCache\" \"cache/alembic\";\nworkspace -fr \"renderData\" \"renderData\";\nworkspace -fr \"sourceImages\" \"sourceimages\";\nworkspace -fr \"fileCache\" \"cache/nCache\";\n",
"mel_workspace": "workspace -fr \"shaders\" \"renderData/shaders\";\nworkspace -fr \"images\" \"renders/maya\";\nworkspace -fr \"particles\" \"particles\";\nworkspace -fr \"mayaAscii\" \"\";\nworkspace -fr \"mayaBinary\" \"\";\nworkspace -fr \"scene\" \"\";\nworkspace -fr \"alembicCache\" \"cache/alembic\";\nworkspace -fr \"renderData\" \"renderData\";\nworkspace -fr \"sourceImages\" \"sourceimages\";\nworkspace -fr \"fileCache\" \"cache/nCache\";\n",
"ext_mapping": {
"model": "ma",
"mayaAscii": "ma",
@ -56,12 +56,12 @@
},
"RenderSettings": {
"apply_render_settings": true,
"default_render_image_folder": "renders",
"default_render_image_folder": "renders/maya",
"enable_all_lights": true,
"aov_separator": "underscore",
"reset_current_frame": false,
"arnold_renderer": {
"image_prefix": "maya/<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>",
"image_prefix": "<Scene>/<RenderLayer>/<RenderLayer>_<RenderPass>",
"image_format": "exr",
"multilayer_exr": true,
"tiled": true,
@ -69,14 +69,14 @@
"additional_options": []
},
"vray_renderer": {
"image_prefix": "maya/<scene>/<Layer>/<Layer>",
"image_prefix": "<scene>/<Layer>/<Layer>",
"engine": "1",
"image_format": "exr",
"aov_list": [],
"additional_options": []
},
"redshift_renderer": {
"image_prefix": "maya/<Scene>/<RenderLayer>/<RenderLayer>",
"image_prefix": "<Scene>/<RenderLayer>/<RenderLayer>",
"primary_gi_engine": "0",
"secondary_gi_engine": "0",
"image_format": "exr",

View file

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

View file

@ -10,6 +10,7 @@
],
"publish": {
"CollectPublishedFiles": {
"sync_next_version": false,
"task_type_to_family": {
"Animation": [
{

View file

@ -10,6 +10,11 @@
"key": "level_sequences_for_layouts",
"label": "Generate level sequences when loading layouts"
},
{
"type": "boolean",
"key": "delete_unmatched_assets",
"label": "Delete assets that are not matched"
},
{
"type": "dict",
"collapsible": true,

View file

@ -49,6 +49,19 @@
"key": "CollectPublishedFiles",
"label": "Collect Published Files",
"children": [
{
"type": "label",
"label": "Select if all versions of published items should be kept same. (As max(published) + 1.)"
},
{
"type": "boolean",
"key": "sync_next_version",
"label": "Sync next publish version"
},
{
"type": "label",
"label": "Configure resulting family and tags on representation based on uploaded file and task. <br>Eg. '.png' is uploaded >> create instance of 'render' family<br>'Create review' in Tags >> mark representation to create review from."
},
{
"type": "dict-modifiable",
"collapsible": true,
@ -74,6 +87,9 @@
"label": "Extensions",
"object_type": "text"
},
{
"type": "separator"
},
{
"type": "list",
"key": "families",
@ -84,9 +100,6 @@
"type": "schema",
"name": "schema_representation_tags"
},
{
"type": "separator"
},
{
"type": "text",
"key": "result_family",

View file

@ -555,6 +555,73 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "PreIntegrateThumbnails",
"label": "Override Integrate Thumbnail Representations",
"is_group": true,
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "label",
"label": "Explicitly set if Thumbnail representation should be integrated into DB.<br> If no matching profile set, existing state from Host implementation is kept."
},
{
"type": "list",
"key": "integrate_profiles",
"label": "Integrate profiles",
"use_label_wrap": true,
"object_type": {
"type": "dict",
"children": [
{
"key": "families",
"label": "Families",
"type": "list",
"object_type": "text"
},
{
"type": "hosts-enum",
"key": "hosts",
"label": "Hosts",
"multiselection": true
},
{
"key": "task_types",
"label": "Task types",
"type": "task-types-enum"
},
{
"key": "task_names",
"label": "Task names",
"type": "list",
"object_type": "text"
},
{
"key": "subsets",
"label": "Subset names",
"type": "list",
"object_type": "text"
},
{
"type": "separator"
},
{
"type": "boolean",
"key": "integrate_thumbnail",
"label": "Integrate thumbnail"
}
]
}
}
]
},
{
"type": "dict",
"collapsible": true,

View file

@ -89,8 +89,10 @@
},
"publisher": {
"error": "#AA5050",
"crash": "#FF6432",
"success": "#458056",
"warning": "#ffc671",
"tab-bg": "#16191d",
"list-view-group": {
"bg": "#434a56",
"bg-hover": "rgba(168, 175, 189, 0.3)",

View file

@ -856,6 +856,33 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
}
/* New Create/Publish UI */
PublisherTabsWidget {
background: {color:publisher:tab-bg};
}
PublisherTabBtn {
border-radius: 0px;
background: {color:bg-inputs};
font-size: 9pt;
font-weight: regular;
padding: 0.5em 1em 0.5em 1em;
}
PublisherTabBtn:disabled {
background: {color:bg-inputs};
}
PublisherTabBtn:hover {
background: {color:bg-buttons};
}
PublisherTabBtn[active="1"] {
background: {color:bg};
}
PublisherTabBtn[active="1"]:hover {
background: {color:bg};
}
#CreatorDetailedDescription {
padding-left: 5px;
padding-right: 5px;
@ -865,18 +892,16 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
}
#CreateDialogHelpButton {
background: rgba(255, 255, 255, 31);
background: {color:bg-buttons};
border-top-left-radius: 0.2em;
border-bottom-left-radius: 0.2em;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
font-size: 10pt;
font-weight: bold;
padding: 0px;
}
#CreateDialogHelpButton:hover {
background: rgba(255, 255, 255, 63);
background: {color:bg-button-hover};
}
#CreateDialogHelpButton QWidget {
background: transparent;
@ -944,19 +969,8 @@ VariantInputsWidget QToolButton {
color: {color:publisher:error};
}
#PublishFrame {
background: rgba(0, 0, 0, 127);
}
#PublishFrame[state="1"] {
background: rgb(22, 25, 29);
}
#PublishFrame[state="2"] {
background: {color:bg};
}
#PublishInfoFrame {
background: {color:bg};
border: 2px solid black;
border-radius: 0.3em;
}
@ -965,7 +979,7 @@ VariantInputsWidget QToolButton {
}
#PublishInfoFrame[state="0"] {
background: {color:publisher:error};
background: {color:publisher:crash};
}
#PublishInfoFrame[state="1"] {
@ -989,6 +1003,11 @@ VariantInputsWidget QToolButton {
font-size: 13pt;
}
ValidationArtistMessage QLabel {
font-size: 20pt;
font-weight: bold;
}
#ValidationActionButton {
border-radius: 0.2em;
padding: 4px 6px 4px 6px;
@ -1005,17 +1024,16 @@ VariantInputsWidget QToolButton {
}
#ValidationErrorTitleFrame {
background: {color:bg-inputs};
border-left: 4px solid transparent;
border-radius: 0.2em;
background: {color:bg-buttons};
}
#ValidationErrorTitleFrame:hover {
border-left-color: {color:border};
background: {color:bg-buttons-hover};
}
#ValidationErrorTitleFrame[selected="1"] {
background: {color:bg};
border-left-color: {palette:blue-light};
background: {color:bg-view-selection};
}
#ValidationErrorInstanceList {

View file

@ -4,7 +4,7 @@ from Qt import QtWidgets, QtGui
from openpype import PLUGINS_DIR
from openpype import style
from openpype.api import resources
from openpype import resources
from openpype.lib import (
Logger,
ApplictionExecutableNotFound,

View file

@ -1,7 +1,7 @@
import os
from Qt import QtGui
import qtawesome
from openpype.api import resources
from openpype import resources
ICON_CACHE = {}
NOT_FOUND = type("NotFound", (object, ), {})

View file

@ -4,7 +4,7 @@ import logging
from Qt import QtWidgets, QtCore, QtGui
from openpype import style
from openpype.api import resources
from openpype import resources
from openpype.pipeline import AvalonMongoDB
import qtawesome

View file

@ -1,22 +1,17 @@
import os
import copy
import inspect
import logging
import traceback
import collections
import weakref
try:
from weakref import WeakMethod
except Exception:
from openpype.lib.python_2_comp import WeakMethod
import pyblish.api
from openpype.client import get_assets
from openpype.lib.events import EventSystem
from openpype.pipeline import (
PublishValidationError,
registered_host,
legacy_io,
)
from openpype.pipeline.create import CreateContext
@ -107,17 +102,13 @@ class AssetDocsCache:
self._asset_docs = None
self._task_names_by_asset_name = {}
@property
def dbcon(self):
return self._controller.dbcon
def reset(self):
self._asset_docs = None
self._task_names_by_asset_name = {}
def _query(self):
if self._asset_docs is None:
project_name = self.dbcon.active_project()
project_name = self._controller.project_name
asset_docs = get_assets(
project_name, fields=self.projection.keys()
)
@ -360,11 +351,15 @@ class PublisherController:
dbcon (AvalonMongoDB): Connection to mongo with context.
headless (bool): Headless publishing. ATM not implemented or used.
"""
def __init__(self, dbcon=None, headless=False):
self.log = logging.getLogger("PublisherController")
self.host = registered_host()
self.headless = headless
# Inner event system of controller
self._event_system = EventSystem()
self.create_context = CreateContext(
self.host, dbcon, headless=headless, reset=False
)
@ -405,18 +400,6 @@ class PublisherController:
# Plugin iterator
self._main_thread_iter = None
# Variables where callbacks are stored
self._instances_refresh_callback_refs = set()
self._plugins_refresh_callback_refs = set()
self._publish_reset_callback_refs = set()
self._publish_started_callback_refs = set()
self._publish_validated_callback_refs = set()
self._publish_stopped_callback_refs = set()
self._publish_instance_changed_callback_refs = set()
self._publish_plugin_changed_callback_refs = set()
# State flags to prevent executing method which is already in progress
self._resetting_plugins = False
self._resetting_instances = False
@ -426,13 +409,42 @@ class PublisherController:
@property
def project_name(self):
"""Current project context."""
return self.dbcon.Session["AVALON_PROJECT"]
"""Current project context defined by host.
Returns:
str: Project name.
"""
if not hasattr(self.host, "get_current_context"):
return legacy_io.active_project()
return self.host.get_current_context()["project_name"]
@property
def dbcon(self):
"""Pointer to AvalonMongoDB in creator context."""
return self.create_context.dbcon
def current_asset_name(self):
"""Current context asset name defined by host.
Returns:
Union[str, None]: Asset name or None if asset is not set.
"""
if not hasattr(self.host, "get_current_context"):
return legacy_io.Session["AVALON_ASSET"]
return self.host.get_current_context()["asset_name"]
@property
def current_task_name(self):
"""Current context task name defined by host.
Returns:
Union[str, None]: Task name or None if task is not set.
"""
if not hasattr(self.host, "get_current_context"):
return legacy_io.Session["AVALON_TASK"]
return self.host.get_current_context()["task_name"]
@property
def instances(self):
@ -464,58 +476,35 @@ class PublisherController:
"""Publish plugins with possible attribute definitions."""
return self.create_context.plugins_with_defs
def _create_reference(self, callback):
if inspect.ismethod(callback):
ref = WeakMethod(callback)
elif callable(callback):
ref = weakref.ref(callback)
else:
raise TypeError("Expected function or method got {}".format(
str(type(callback))
))
return ref
@property
def event_system(self):
"""Inner event system for publisher controller.
def add_instances_refresh_callback(self, callback):
"""Callbacks triggered on instances refresh."""
ref = self._create_reference(callback)
self._instances_refresh_callback_refs.add(ref)
Known topics:
"show.detailed.help" - Detailed help requested (UI related).
"show.card.message" - Show card message request (UI related).
"instances.refresh.finished" - Instances are refreshed.
"plugins.refresh.finished" - Plugins refreshed.
"publish.reset.finished" - Controller reset finished.
"publish.process.started" - Publishing started. Can be started from
paused state.
"publish.process.validated" - Publishing passed validation.
"publish.process.stopped" - Publishing stopped/paused process.
"publish.process.plugin.changed" - Plugin state has changed.
"publish.process.instance.changed" - Instance state has changed.
def add_plugins_refresh_callback(self, callback):
"""Callbacks triggered on plugins refresh."""
ref = self._create_reference(callback)
self._plugins_refresh_callback_refs.add(ref)
Returns:
EventSystem: Event system which can trigger callbacks for topics.
"""
return self._event_system
def _emit_event(self, topic, data=None):
if data is None:
data = {}
self._event_system.emit(topic, data, "controller")
# --- Publish specific callbacks ---
def add_publish_reset_callback(self, callback):
"""Callbacks triggered on publishing reset."""
ref = self._create_reference(callback)
self._publish_reset_callback_refs.add(ref)
def add_publish_started_callback(self, callback):
"""Callbacks triggered on publishing start."""
ref = self._create_reference(callback)
self._publish_started_callback_refs.add(ref)
def add_publish_validated_callback(self, callback):
"""Callbacks triggered on passing last possible validation order."""
ref = self._create_reference(callback)
self._publish_validated_callback_refs.add(ref)
def add_instance_change_callback(self, callback):
"""Callbacks triggered before next publish instance process."""
ref = self._create_reference(callback)
self._publish_instance_changed_callback_refs.add(ref)
def add_plugin_change_callback(self, callback):
"""Callbacks triggered before next plugin processing."""
ref = self._create_reference(callback)
self._publish_plugin_changed_callback_refs.add(ref)
def add_publish_stopped_callback(self, callback):
"""Callbacks triggered on publishing stop (any reason)."""
ref = self._create_reference(callback)
self._publish_stopped_callback_refs.add(ref)
def get_asset_docs(self):
"""Get asset documents from cache for whole project."""
return self._asset_docs_cache.get_asset_docs()
@ -556,20 +545,6 @@ class PublisherController:
)
return result
def _trigger_callbacks(self, callbacks, *args, **kwargs):
"""Helper method to trigger callbacks stored by their rerence."""
# Trigger reset callbacks
to_remove = set()
for ref in callbacks:
callback = ref()
if callback:
callback(*args, **kwargs)
else:
to_remove.add(ref)
for ref in to_remove:
callbacks.remove(ref)
def reset(self):
"""Reset everything related to creation and publishing."""
# Stop publishing
@ -585,6 +560,8 @@ class PublisherController:
self._reset_publish()
self._reset_instances()
self.emit_card_message("Refreshed..")
def _reset_plugins(self):
"""Reset to initial state."""
if self._resetting_plugins:
@ -596,7 +573,7 @@ class PublisherController:
self._resetting_plugins = False
self._trigger_callbacks(self._plugins_refresh_callback_refs)
self._emit_event("plugins.refresh.finished")
def _reset_instances(self):
"""Reset create instances."""
@ -612,7 +589,10 @@ class PublisherController:
self._resetting_instances = False
self._trigger_callbacks(self._instances_refresh_callback_refs)
self._emit_event("instances.refresh.finished")
def emit_card_message(self, message):
self._emit_event("show.card.message", {"message": message})
def get_creator_attribute_definitions(self, instances):
"""Collect creator attribute definitions for multuple instances.
@ -709,7 +689,7 @@ class PublisherController:
creator = self.creators[creator_identifier]
creator.create(subset_name, instance_data, options)
self._trigger_callbacks(self._instances_refresh_callback_refs)
self._emit_event("instances.refresh.finished")
def save_changes(self):
"""Save changes happened during creation."""
@ -724,7 +704,7 @@ class PublisherController:
self.create_context.remove_instances(instances)
self._trigger_callbacks(self._instances_refresh_callback_refs)
self._emit_event("instances.refresh.finished")
# --- Publish specific implementations ---
@property
@ -793,7 +773,7 @@ class PublisherController:
self._publish_max_progress = len(self.publish_plugins)
self._publish_progress = 0
self._trigger_callbacks(self._publish_reset_callback_refs)
self._emit_event("publish.reset.finished")
def set_comment(self, comment):
self._publish_context.data["comment"] = comment
@ -820,7 +800,8 @@ class PublisherController:
self.save_changes()
self._publish_is_running = True
self._trigger_callbacks(self._publish_started_callback_refs)
self._emit_event("publish.process.started")
self._main_thread_processor.start()
self._publish_next_process()
@ -828,10 +809,12 @@ class PublisherController:
"""Stop or pause publishing."""
self._publish_is_running = False
self._main_thread_processor.stop()
self._trigger_callbacks(self._publish_stopped_callback_refs)
self._emit_event("publish.process.stopped")
def stop_publish(self):
"""Stop publishing process (any reason)."""
if self._publish_is_running:
self._stop_publish()
@ -892,9 +875,7 @@ class PublisherController:
)
# Trigger callbacks when validation stage is passed
if self._publish_validated:
self._trigger_callbacks(
self._publish_validated_callback_refs
)
self._emit_event("publish.process.validated")
# Stop if plugin is over validation order and process
# should process up to validation.
@ -912,9 +893,14 @@ class PublisherController:
self._publish_report.add_plugin_iter(plugin, self._publish_context)
# Trigger callback that new plugin is going to be processed
self._trigger_callbacks(
self._publish_plugin_changed_callback_refs, plugin
plugin_label = plugin.__name__
if hasattr(plugin, "label") and plugin.label:
plugin_label = plugin.label
self._emit_event(
"publish.process.plugin.changed",
{"plugin_label": plugin_label}
)
# Plugin is instance plugin
if plugin.__instanceEnabled__:
instances = pyblish.logic.instances_by_plugin(
@ -928,11 +914,15 @@ class PublisherController:
if instance.data.get("publish") is False:
continue
self._trigger_callbacks(
self._publish_instance_changed_callback_refs,
self._publish_context,
instance
instance_label = (
instance.data.get("label")
or instance.data["name"]
)
self._emit_event(
"publish.process.instance.changed",
{"instance_label": instance_label}
)
yield MainThreadItem(
self._process_and_continue, plugin, instance
)
@ -944,10 +934,14 @@ class PublisherController:
[plugin], families
)
if plugins:
self._trigger_callbacks(
self._publish_instance_changed_callback_refs,
self._publish_context,
None
instance_label = (
self._publish_context.data.get("label")
or self._publish_context.data.get("name")
or "Context"
)
self._emit_event(
"publish.process.instance.changed",
{"instance_label": instance_label}
)
yield MainThreadItem(
self._process_and_continue, plugin, None

View file

@ -331,7 +331,7 @@ class DetailsPopup(QtWidgets.QDialog):
self.closed.emit()
class PublishReportViewerWidget(QtWidgets.QWidget):
class PublishReportViewerWidget(QtWidgets.QFrame):
def __init__(self, parent=None):
super(PublishReportViewerWidget, self).__init__(parent)

View file

@ -3,35 +3,20 @@ from .icons import (
get_pixmap,
get_icon
)
from .border_label_widget import (
BorderedLabelWidget
)
from .widgets import (
SubsetAttributesWidget,
StopBtn,
ResetBtn,
ValidateBtn,
PublishBtn,
CreateInstanceBtn,
RemoveInstanceBtn,
ChangeViewBtn
)
from .publish_widget import (
PublishFrame
)
from .create_dialog import (
CreateDialog
)
from .card_view_widgets import (
InstanceCardView
)
from .list_view_widgets import (
InstanceListView
from .help_widget import (
HelpButton,
HelpDialog,
)
from .publish_frame import PublishFrame
from .tabs_widget import PublisherTabsWidget
from .overview_widget import OverviewWidget
from .validations_widget import ValidationsWidget
__all__ = (
@ -39,22 +24,17 @@ __all__ = (
"get_pixmap",
"get_icon",
"SubsetAttributesWidget",
"BorderedLabelWidget",
"StopBtn",
"ResetBtn",
"ValidateBtn",
"PublishBtn",
"CreateInstanceBtn",
"RemoveInstanceBtn",
"ChangeViewBtn",
"HelpButton",
"HelpDialog",
"PublishFrame",
"CreateDialog",
"InstanceCardView",
"InstanceListView",
"PublisherTabsWidget",
"OverviewWidget",
"ValidationsWidget",
)

View file

@ -13,18 +13,17 @@ from openpype.tools.utils.assets_widget import (
)
class CreateDialogAssetsWidget(SingleSelectAssetsWidget):
class CreateWidgetAssetsWidget(SingleSelectAssetsWidget):
current_context_required = QtCore.Signal()
header_height_changed = QtCore.Signal(int)
def __init__(self, controller, parent):
self._controller = controller
super(CreateDialogAssetsWidget, self).__init__(None, parent)
super(CreateWidgetAssetsWidget, self).__init__(None, parent)
self.set_refresh_btn_visibility(False)
self.set_current_asset_btn_visibility(False)
self._current_asset_name = None
self._last_selection = None
self._enabled = None
@ -42,11 +41,11 @@ class CreateDialogAssetsWidget(SingleSelectAssetsWidget):
self.header_height_changed.emit(height)
def resizeEvent(self, event):
super(CreateDialogAssetsWidget, self).resizeEvent(event)
super(CreateWidgetAssetsWidget, self).resizeEvent(event)
self._check_header_height()
def showEvent(self, event):
super(CreateDialogAssetsWidget, self).showEvent(event)
super(CreateWidgetAssetsWidget, self).showEvent(event)
self._check_header_height()
def _on_current_asset_click(self):
@ -63,19 +62,19 @@ class CreateDialogAssetsWidget(SingleSelectAssetsWidget):
self.select_asset(self._last_selection)
def _select_indexes(self, *args, **kwargs):
super(CreateDialogAssetsWidget, self)._select_indexes(*args, **kwargs)
super(CreateWidgetAssetsWidget, self)._select_indexes(*args, **kwargs)
if self._enabled:
return
self._last_selection = self.get_selected_asset_id()
self._clear_selection()
def set_current_asset_name(self, asset_name):
self._current_asset_name = asset_name
def update_current_asset(self):
# Hide set current asset if there is no one
self.set_current_asset_btn_visibility(asset_name is not None)
asset_name = self._get_current_session_asset()
self.set_current_asset_btn_visibility(bool(asset_name))
def _get_current_session_asset(self):
return self._current_asset_name
return self._controller.current_asset_name
def _create_source_model(self):
return AssetsHierarchyModel(self._controller)

View file

@ -3,11 +3,6 @@ import re
import traceback
import copy
import qtawesome
try:
import commonmark
except Exception:
commonmark = None
from Qt import QtWidgets, QtCore, QtGui
from openpype.client import get_asset_by_name, get_subsets
@ -16,15 +11,14 @@ from openpype.pipeline.create import (
SUBSET_NAME_ALLOWED_SYMBOLS,
TaskNotSetError,
)
from openpype.tools.utils import (
ErrorMessageBox,
MessageOverlayObject,
ClickableFrame,
)
from openpype.tools.utils import ErrorMessageBox
from .widgets import IconValuePixmapLabel
from .assets_widget import CreateDialogAssetsWidget
from .tasks_widget import CreateDialogTasksWidget
from .widgets import (
IconValuePixmapLabel,
CreateBtn,
)
from .assets_widget import CreateWidgetAssetsWidget
from .tasks_widget import CreateWidgetTasksWidget
from .precreate_widget import PreCreateWidget
from ..constants import (
VARIANT_TOOLTIP,
@ -118,8 +112,6 @@ class CreateErrorMessageBox(ErrorMessageBox):
# TODO add creator identifier/label to details
class CreatorShortDescWidget(QtWidgets.QWidget):
height_changed = QtCore.Signal(int)
def __init__(self, parent=None):
super(CreatorShortDescWidget, self).__init__(parent=parent)
@ -158,22 +150,6 @@ class CreatorShortDescWidget(QtWidgets.QWidget):
self._family_label = family_label
self._description_label = description_label
self._last_height = None
def _check_height_change(self):
height = self.height()
if height != self._last_height:
self._last_height = height
self.height_changed.emit(height)
def showEvent(self, event):
super(CreatorShortDescWidget, self).showEvent(event)
self._check_height_change()
def resizeEvent(self, event):
super(CreatorShortDescWidget, self).resizeEvent(event)
self._check_height_change()
def set_plugin(self, plugin=None):
if not plugin:
self._icon_widget.set_icon_def(None)
@ -190,122 +166,14 @@ class CreatorShortDescWidget(QtWidgets.QWidget):
self._description_label.setText(description)
class HelpButton(ClickableFrame):
resized = QtCore.Signal(int)
question_mark_icon_name = "fa.question"
help_icon_name = "fa.question-circle"
hide_icon_name = "fa.angle-left"
def __init__(self, *args, **kwargs):
super(HelpButton, self).__init__(*args, **kwargs)
self.setObjectName("CreateDialogHelpButton")
question_mark_label = QtWidgets.QLabel(self)
help_widget = QtWidgets.QWidget(self)
help_question = QtWidgets.QLabel(help_widget)
help_label = QtWidgets.QLabel("Help", help_widget)
hide_icon = QtWidgets.QLabel(help_widget)
help_layout = QtWidgets.QHBoxLayout(help_widget)
help_layout.setContentsMargins(0, 0, 5, 0)
help_layout.addWidget(help_question, 0)
help_layout.addWidget(help_label, 0)
help_layout.addStretch(1)
help_layout.addWidget(hide_icon, 0)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(question_mark_label, 0)
layout.addWidget(help_widget, 1)
help_widget.setVisible(False)
self._question_mark_label = question_mark_label
self._help_widget = help_widget
self._help_question = help_question
self._hide_icon = hide_icon
self._expanded = None
self.set_expanded()
def set_expanded(self, expanded=None):
if self._expanded is expanded:
if expanded is not None:
return
expanded = False
self._expanded = expanded
self._help_widget.setVisible(expanded)
self._update_content()
def _update_content(self):
width = self.get_icon_width()
if self._expanded:
question_mark_pix = QtGui.QPixmap(width, width)
question_mark_pix.fill(QtCore.Qt.transparent)
else:
question_mark_icon = qtawesome.icon(
self.question_mark_icon_name, color=QtCore.Qt.white
)
question_mark_pix = question_mark_icon.pixmap(width, width)
hide_icon = qtawesome.icon(
self.hide_icon_name, color=QtCore.Qt.white
)
help_question_icon = qtawesome.icon(
self.help_icon_name, color=QtCore.Qt.white
)
self._question_mark_label.setPixmap(question_mark_pix)
self._question_mark_label.setMaximumWidth(width)
self._hide_icon.setPixmap(hide_icon.pixmap(width, width))
self._help_question.setPixmap(help_question_icon.pixmap(width, width))
def get_icon_width(self):
metrics = self.fontMetrics()
return metrics.height()
def set_pos_and_size(self, pos_x, pos_y, width, height):
update_icon = self.height() != height
self.move(pos_x, pos_y)
self.resize(width, height)
if update_icon:
self._update_content()
self.updateGeometry()
def showEvent(self, event):
super(HelpButton, self).showEvent(event)
self.resized.emit(self.height())
def resizeEvent(self, event):
super(HelpButton, self).resizeEvent(event)
self.resized.emit(self.height())
class CreateDialog(QtWidgets.QDialog):
default_size = (1000, 560)
def __init__(
self, controller, asset_name=None, task_name=None, parent=None
):
super(CreateDialog, self).__init__(parent)
class CreateWidget(QtWidgets.QWidget):
def __init__(self, controller, parent=None):
super(CreateWidget, self).__init__(parent)
self.setWindowTitle("Create new instance")
self.controller = controller
self._controller = controller
if asset_name is None:
asset_name = self.dbcon.Session.get("AVALON_ASSET")
if task_name is None:
task_name = self.dbcon.Session.get("AVALON_TASK")
self._asset_name = asset_name
self._task_name = task_name
self._last_pos = None
self._asset_doc = None
self._subset_names = None
self._selected_creator = None
@ -318,12 +186,12 @@ class CreateDialog(QtWidgets.QDialog):
self._name_pattern = name_pattern
self._compiled_name_pattern = re.compile(name_pattern)
overlay_object = MessageOverlayObject(self)
main_splitter_widget = QtWidgets.QSplitter(self)
context_widget = QtWidgets.QWidget(self)
context_widget = QtWidgets.QWidget(main_splitter_widget)
assets_widget = CreateDialogAssetsWidget(controller, context_widget)
tasks_widget = CreateDialogTasksWidget(controller, context_widget)
assets_widget = CreateWidgetAssetsWidget(controller, context_widget)
tasks_widget = CreateWidgetTasksWidget(controller, context_widget)
context_layout = QtWidgets.QVBoxLayout(context_widget)
context_layout.setContentsMargins(0, 0, 0, 0)
@ -332,21 +200,44 @@ class CreateDialog(QtWidgets.QDialog):
context_layout.addWidget(tasks_widget, 1)
# --- Creators view ---
creators_header_widget = QtWidgets.QWidget(self)
header_label_widget = QtWidgets.QLabel(
"Choose family:", creators_header_widget
)
creators_header_layout = QtWidgets.QHBoxLayout(creators_header_widget)
creators_header_layout.setContentsMargins(0, 0, 0, 0)
creators_header_layout.addWidget(header_label_widget, 1)
creators_widget = QtWidgets.QWidget(main_splitter_widget)
creators_view = QtWidgets.QListView(self)
creator_short_desc_widget = CreatorShortDescWidget(creators_widget)
attr_separator_widget = QtWidgets.QWidget(creators_widget)
attr_separator_widget.setObjectName("Separator")
attr_separator_widget.setMinimumHeight(1)
attr_separator_widget.setMaximumHeight(1)
creators_splitter = QtWidgets.QSplitter(creators_widget)
creators_view_widget = QtWidgets.QWidget(creators_splitter)
creator_view_label = QtWidgets.QLabel(
"Choose publish type", creators_view_widget
)
creators_view = QtWidgets.QListView(creators_view_widget)
creators_model = QtGui.QStandardItemModel()
creators_sort_model = QtCore.QSortFilterProxyModel()
creators_sort_model.setSourceModel(creators_model)
creators_view.setModel(creators_sort_model)
variant_widget = VariantInputsWidget(self)
creators_view_layout = QtWidgets.QVBoxLayout(creators_view_widget)
creators_view_layout.setContentsMargins(0, 0, 0, 0)
creators_view_layout.addWidget(creator_view_label, 0)
creators_view_layout.addWidget(creators_view, 1)
# --- Creator attr defs ---
creators_attrs_widget = QtWidgets.QWidget(creators_splitter)
variant_subset_label = QtWidgets.QLabel(
"Create options", creators_attrs_widget
)
variant_subset_widget = QtWidgets.QWidget(creators_attrs_widget)
# Variant and subset input
variant_widget = VariantInputsWidget(creators_attrs_widget)
variant_input = QtWidgets.QLineEdit(variant_widget)
variant_input.setObjectName("VariantInput")
@ -365,39 +256,20 @@ class CreateDialog(QtWidgets.QDialog):
variant_layout.addWidget(variant_input, 1)
variant_layout.addWidget(variant_hints_btn, 0, QtCore.Qt.AlignVCenter)
subset_name_input = QtWidgets.QLineEdit(self)
subset_name_input = QtWidgets.QLineEdit(variant_subset_widget)
subset_name_input.setEnabled(False)
form_layout = QtWidgets.QFormLayout()
form_layout.addRow("Variant:", variant_widget)
form_layout.addRow("Subset:", subset_name_input)
mid_widget = QtWidgets.QWidget(self)
mid_layout = QtWidgets.QVBoxLayout(mid_widget)
mid_layout.setContentsMargins(0, 0, 0, 0)
mid_layout.addWidget(creators_header_widget, 0)
mid_layout.addWidget(creators_view, 1)
mid_layout.addLayout(form_layout, 0)
# ------------
# --- Creator short info and attr defs ---
creator_attrs_widget = QtWidgets.QWidget(self)
creator_short_desc_widget = CreatorShortDescWidget(
creator_attrs_widget
)
attr_separator_widget = QtWidgets.QWidget(self)
attr_separator_widget.setObjectName("Separator")
attr_separator_widget.setMinimumHeight(1)
attr_separator_widget.setMaximumHeight(1)
variant_subset_layout = QtWidgets.QFormLayout(variant_subset_widget)
variant_subset_layout.setContentsMargins(0, 0, 0, 0)
variant_subset_layout.addRow("Variant", variant_widget)
variant_subset_layout.addRow("Subset", subset_name_input)
# Precreate attributes widget
pre_create_widget = PreCreateWidget(creator_attrs_widget)
pre_create_widget = PreCreateWidget(creators_attrs_widget)
# Create button
create_btn_wrapper = QtWidgets.QWidget(creator_attrs_widget)
create_btn = QtWidgets.QPushButton("Create", create_btn_wrapper)
create_btn_wrapper = QtWidgets.QWidget(creators_attrs_widget)
create_btn = CreateBtn(create_btn_wrapper)
create_btn.setEnabled(False)
create_btn_wrap_layout = QtWidgets.QHBoxLayout(create_btn_wrapper)
@ -405,79 +277,45 @@ class CreateDialog(QtWidgets.QDialog):
create_btn_wrap_layout.addStretch(1)
create_btn_wrap_layout.addWidget(create_btn, 0)
creator_attrs_layout = QtWidgets.QVBoxLayout(creator_attrs_widget)
creator_attrs_layout.setContentsMargins(0, 0, 0, 0)
creator_attrs_layout.addWidget(creator_short_desc_widget, 0)
creator_attrs_layout.addWidget(attr_separator_widget, 0)
creator_attrs_layout.addWidget(pre_create_widget, 1)
creator_attrs_layout.addWidget(create_btn_wrapper, 0)
# -------------------------------------
creators_attrs_layout = QtWidgets.QVBoxLayout(creators_attrs_widget)
creators_attrs_layout.setContentsMargins(0, 0, 0, 0)
creators_attrs_layout.addWidget(variant_subset_label, 0)
creators_attrs_layout.addWidget(variant_subset_widget, 0)
creators_attrs_layout.addWidget(pre_create_widget, 1)
creators_attrs_layout.addWidget(create_btn_wrapper, 0)
creators_splitter.addWidget(creators_view_widget)
creators_splitter.addWidget(creators_attrs_widget)
creators_splitter.setStretchFactor(0, 1)
creators_splitter.setStretchFactor(1, 2)
creators_layout = QtWidgets.QVBoxLayout(creators_widget)
creators_layout.setContentsMargins(0, 0, 0, 0)
creators_layout.addWidget(creator_short_desc_widget, 0)
creators_layout.addWidget(attr_separator_widget, 0)
creators_layout.addWidget(creators_splitter, 1)
# ------------
# --- Detailed information about creator ---
# Detailed description of creator
detail_description_widget = QtWidgets.QWidget(self)
detail_placoholder_widget = QtWidgets.QWidget(
detail_description_widget
)
detail_placoholder_widget.setAttribute(
QtCore.Qt.WA_TranslucentBackground
)
detail_description_input = QtWidgets.QTextEdit(
detail_description_widget
)
detail_description_input.setObjectName("CreatorDetailedDescription")
detail_description_input.setTextInteractionFlags(
QtCore.Qt.TextBrowserInteraction
)
detail_description_layout = QtWidgets.QVBoxLayout(
detail_description_widget
)
detail_description_layout.setContentsMargins(0, 0, 0, 0)
detail_description_layout.setSpacing(0)
detail_description_layout.addWidget(detail_placoholder_widget, 0)
detail_description_layout.addWidget(detail_description_input, 1)
detail_description_widget.setVisible(False)
# TODO this has no way how can be showed now
# -------------------------------------------
splitter_widget = QtWidgets.QSplitter(self)
splitter_widget.addWidget(context_widget)
splitter_widget.addWidget(mid_widget)
splitter_widget.addWidget(creator_attrs_widget)
splitter_widget.addWidget(detail_description_widget)
splitter_widget.setStretchFactor(0, 1)
splitter_widget.setStretchFactor(1, 1)
splitter_widget.setStretchFactor(2, 1)
splitter_widget.setStretchFactor(3, 1)
main_splitter_widget.addWidget(context_widget)
main_splitter_widget.addWidget(creators_widget)
main_splitter_widget.setStretchFactor(0, 1)
main_splitter_widget.setStretchFactor(1, 3)
layout = QtWidgets.QHBoxLayout(self)
layout.addWidget(splitter_widget, 1)
# Floating help button
# - Create this button as last to be fully visible
help_btn = HelpButton(self)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(main_splitter_widget, 1)
prereq_timer = QtCore.QTimer()
prereq_timer.setInterval(50)
prereq_timer.setSingleShot(True)
desc_width_anim_timer = QtCore.QTimer()
desc_width_anim_timer.setInterval(10)
prereq_timer.timeout.connect(self._invalidate_prereq)
desc_width_anim_timer.timeout.connect(self._on_desc_animation)
help_btn.clicked.connect(self._on_help_btn)
help_btn.resized.connect(self._on_help_btn_resize)
assets_widget.header_height_changed.connect(
self._on_asset_filter_height_change
)
create_btn.clicked.connect(self._on_create)
variant_widget.resized.connect(self._on_variant_widget_resize)
variant_input.returnPressed.connect(self._on_create)
@ -492,16 +330,14 @@ class CreateDialog(QtWidgets.QDialog):
self._on_current_session_context_request
)
tasks_widget.task_changed.connect(self._on_task_change)
creator_short_desc_widget.height_changed.connect(
self._on_description_height_change
controller.event_system.add_callback(
"plugins.refresh.finished", self._on_plugins_refresh
)
splitter_widget.splitterMoved.connect(self._on_splitter_move)
controller.add_plugins_refresh_callback(self._on_plugins_refresh)
self._main_splitter_widget = main_splitter_widget
self._overlay_object = overlay_object
self._splitter_widget = splitter_widget
self._creators_splitter = creators_splitter
self._context_widget = context_widget
self._assets_widget = assets_widget
@ -514,7 +350,6 @@ class CreateDialog(QtWidgets.QDialog):
self.variant_hints_menu = variant_hints_menu
self.variant_hints_group = variant_hints_group
self._creators_header_widget = creators_header_widget
self._creators_model = creators_model
self._creators_sort_model = creators_sort_model
self._creators_view = creators_view
@ -524,26 +359,16 @@ class CreateDialog(QtWidgets.QDialog):
self._pre_create_widget = pre_create_widget
self._attr_separator_widget = attr_separator_widget
self._detail_placoholder_widget = detail_placoholder_widget
self._detail_description_widget = detail_description_widget
self._detail_description_input = detail_description_input
self._help_btn = help_btn
self._prereq_timer = prereq_timer
self._first_show = True
# Description animation
self._description_size_policy = detail_description_widget.sizePolicy()
self._desc_width_anim_timer = desc_width_anim_timer
self._desc_widget_step = 0
self._last_description_width = None
self._last_full_width = 0
self._expected_description_width = 0
self._last_desc_max_width = None
self._other_widgets_widths = []
@property
def current_asset_name(self):
return self._controller.current_asset_name
def _emit_message(self, message):
self._overlay_object.add_message(message)
@property
def current_task_name(self):
return self._controller.current_task_name
def _context_change_is_enabled(self):
return self._context_widget.isEnabled()
@ -554,7 +379,7 @@ class CreateDialog(QtWidgets.QDialog):
asset_name = self._assets_widget.get_selected_asset_name()
if asset_name is None:
asset_name = self._asset_name
asset_name = self.current_asset_name
return asset_name
def _get_task_name(self):
@ -566,13 +391,9 @@ class CreateDialog(QtWidgets.QDialog):
task_name = self._tasks_widget.get_selected_task_name()
if not task_name:
task_name = self._task_name
task_name = self.current_task_name
return task_name
@property
def dbcon(self):
return self.controller.dbcon
def _set_context_enabled(self, enabled):
self._assets_widget.set_enabled(enabled)
self._tasks_widget.set_enabled(enabled)
@ -601,7 +422,7 @@ class CreateDialog(QtWidgets.QDialog):
# data
self._refresh_creators()
self._assets_widget.set_current_asset_name(self._asset_name)
self._assets_widget.update_current_asset()
self._assets_widget.select_asset_by_name(asset_name)
self._tasks_widget.set_asset_name(asset_name)
self._tasks_widget.select_task_name(task_name)
@ -611,10 +432,6 @@ class CreateDialog(QtWidgets.QDialog):
def _invalidate_prereq_deffered(self):
self._prereq_timer.start()
def _on_asset_filter_height_change(self, height):
self._creators_header_widget.setMinimumHeight(height)
self._creators_header_widget.setMaximumHeight(height)
def _invalidate_prereq(self):
prereq_available = True
creator_btn_tooltips = []
@ -660,7 +477,7 @@ class CreateDialog(QtWidgets.QDialog):
if asset_name is None:
return
project_name = self.dbcon.active_project()
project_name = self._controller.project_name
asset_doc = get_asset_by_name(project_name, asset_name)
self._asset_doc = asset_doc
@ -689,7 +506,7 @@ class CreateDialog(QtWidgets.QDialog):
# Add new families
new_creators = set()
for identifier, creator in self.controller.manual_creators.items():
for identifier, creator in self._controller.manual_creators.items():
# TODO add details about creator
new_creators.add(identifier)
if identifier in existing_items:
@ -729,8 +546,7 @@ class CreateDialog(QtWidgets.QDialog):
def _on_plugins_refresh(self):
# Trigger refresh only if is visible
if self.isVisible():
self.refresh()
self.refresh()
def _on_asset_change(self):
self._refresh_asset()
@ -746,14 +562,9 @@ class CreateDialog(QtWidgets.QDialog):
def _on_current_session_context_request(self):
self._assets_widget.set_current_session_asset()
if self._task_name:
self._tasks_widget.select_task_name(self._task_name)
def _on_description_height_change(self):
# Use separator's 'y' position as height
height = self._attr_separator_widget.y()
self._detail_placoholder_widget.setMinimumHeight(height)
self._detail_placoholder_widget.setMaximumHeight(height)
task_name = self.current_task_name
if task_name:
self._tasks_widget.select_task_name(task_name)
def _on_creator_item_change(self, new_index, _old_index):
identifier = None
@ -761,196 +572,21 @@ class CreateDialog(QtWidgets.QDialog):
identifier = new_index.data(CREATOR_IDENTIFIER_ROLE)
self._set_creator_by_identifier(identifier)
def _update_help_btn(self):
short_desc_rect = self._creator_short_desc_widget.rect()
# point = short_desc_rect.topRight()
point = short_desc_rect.center()
mapped_point = self._creator_short_desc_widget.mapTo(self, point)
# pos_y = mapped_point.y()
center_pos_y = mapped_point.y()
icon_width = self._help_btn.get_icon_width()
_height = int(icon_width * 2.5)
height = min(_height, short_desc_rect.height())
pos_y = center_pos_y - int(height / 2)
pos_x = self.width() - icon_width
if self._detail_placoholder_widget.isVisible():
pos_x -= (
self._detail_placoholder_widget.width()
+ self._splitter_widget.handle(3).width()
)
width = self.width() - pos_x
self._help_btn.set_pos_and_size(
max(0, pos_x), max(0, pos_y),
width, height
)
def _on_help_btn_resize(self, height):
if self._creator_short_desc_widget.height() != height:
self._update_help_btn()
def _on_splitter_move(self, *args):
self._update_help_btn()
def _on_help_btn(self):
if self._desc_width_anim_timer.isActive():
return
final_size = self.size()
cur_sizes = self._splitter_widget.sizes()
if self._desc_widget_step == 0:
now_visible = self._detail_description_widget.isVisible()
else:
now_visible = self._desc_widget_step > 0
sizes = []
for idx, value in enumerate(cur_sizes):
if idx < 3:
sizes.append(value)
self._last_full_width = final_size.width()
self._other_widgets_widths = list(sizes)
if now_visible:
cur_desc_width = self._detail_description_widget.width()
if cur_desc_width < 1:
cur_desc_width = 2
step_size = int(cur_desc_width / 5)
if step_size < 1:
step_size = 1
step_size *= -1
expected_width = 0
desc_width = cur_desc_width - 1
width = final_size.width() - 1
min_max = desc_width
self._last_description_width = cur_desc_width
else:
self._detail_description_widget.setVisible(True)
handle = self._splitter_widget.handle(3)
desc_width = handle.sizeHint().width()
if self._last_description_width:
expected_width = self._last_description_width
else:
hint = self._detail_description_widget.sizeHint()
expected_width = hint.width()
width = final_size.width() + desc_width
step_size = int(expected_width / 5)
if step_size < 1:
step_size = 1
min_max = 0
if self._last_desc_max_width is None:
self._last_desc_max_width = (
self._detail_description_widget.maximumWidth()
)
self._detail_description_widget.setMinimumWidth(min_max)
self._detail_description_widget.setMaximumWidth(min_max)
self._expected_description_width = expected_width
self._desc_widget_step = step_size
self._desc_width_anim_timer.start()
sizes.append(desc_width)
final_size.setWidth(width)
self._splitter_widget.setSizes(sizes)
self.resize(final_size)
self._help_btn.set_expanded(not now_visible)
def _on_desc_animation(self):
current_width = self._detail_description_widget.width()
desc_width = None
last_step = False
growing = self._desc_widget_step > 0
# Growing
if growing:
if current_width < self._expected_description_width:
desc_width = current_width + self._desc_widget_step
if desc_width >= self._expected_description_width:
desc_width = self._expected_description_width
last_step = True
# Decreasing
elif self._desc_widget_step < 0:
if current_width > self._expected_description_width:
desc_width = current_width + self._desc_widget_step
if desc_width <= self._expected_description_width:
desc_width = self._expected_description_width
last_step = True
if desc_width is None:
self._desc_widget_step = 0
self._desc_width_anim_timer.stop()
return
if last_step and not growing:
self._detail_description_widget.setVisible(False)
QtWidgets.QApplication.processEvents()
width = self._last_full_width
handle_width = self._splitter_widget.handle(3).width()
if growing:
width += (handle_width + desc_width)
else:
width -= self._last_description_width
if last_step:
width -= handle_width
else:
width += desc_width
if not last_step or growing:
self._detail_description_widget.setMaximumWidth(desc_width)
self._detail_description_widget.setMinimumWidth(desc_width)
window_size = self.size()
window_size.setWidth(width)
self.resize(window_size)
if not last_step:
return
self._desc_widget_step = 0
self._desc_width_anim_timer.stop()
if not growing:
return
self._detail_description_widget.setMinimumWidth(0)
self._detail_description_widget.setMaximumWidth(
self._last_desc_max_width
)
self._detail_description_widget.setSizePolicy(
self._description_size_policy
)
sizes = list(self._other_widgets_widths)
sizes.append(desc_width)
self._splitter_widget.setSizes(sizes)
def _set_creator_detailed_text(self, creator):
if not creator:
self._detail_description_input.setPlainText("")
return
detailed_description = creator.get_detail_description() or ""
if commonmark:
html = commonmark.commonmark(detailed_description)
self._detail_description_input.setHtml(html)
else:
self._detail_description_input.setMarkdown(detailed_description)
# TODO implement
description = ""
if creator is not None:
description = creator.get_detail_description() or description
self._controller.event_system.emit(
"show.detailed.help",
{
"message": description
},
"create.widget"
)
def _set_creator_by_identifier(self, identifier):
creator = self.controller.manual_creators.get(identifier)
creator = self._controller.manual_creators.get(identifier)
self._set_creator(creator)
def _set_creator(self, creator):
@ -1034,7 +670,7 @@ class CreateDialog(QtWidgets.QDialog):
self.subset_name_input.setText("< Valid variant >")
return
project_name = self.controller.project_name
project_name = self._controller.project_name
task_name = self._get_task_name()
asset_doc = copy.deepcopy(self._asset_doc)
@ -1116,41 +752,19 @@ class CreateDialog(QtWidgets.QDialog):
self.variant_input.style().polish(self.variant_input)
def _on_first_show(self):
center = self.rect().center()
width, height = self.default_size
self.resize(width, height)
part = int(width / 7)
self._splitter_widget.setSizes(
[part * 2, part * 2, width - (part * 4)]
)
new_pos = self.mapToGlobal(center)
new_pos.setX(new_pos.x() - int(self.width() / 2))
new_pos.setY(new_pos.y() - int(self.height() / 2))
self.move(new_pos)
def moveEvent(self, event):
super(CreateDialog, self).moveEvent(event)
self._last_pos = self.pos()
width = self.width()
part = int(width / 4)
rem_width = width - part
self._main_splitter_widget.setSizes([part, rem_width])
rem_width = rem_width - part
self._creators_splitter.setSizes([part, rem_width])
def showEvent(self, event):
super(CreateDialog, self).showEvent(event)
super(CreateWidget, self).showEvent(event)
if self._first_show:
self._first_show = False
self._on_first_show()
if self._last_pos is not None:
self.move(self._last_pos)
self._update_help_btn()
self.refresh()
def resizeEvent(self, event):
super(CreateDialog, self).resizeEvent(event)
self._update_help_btn()
def _on_create(self):
indexes = self._creators_view.selectedIndexes()
if not indexes or len(indexes) > 1:
@ -1186,7 +800,7 @@ class CreateDialog(QtWidgets.QDialog):
error_msg = None
formatted_traceback = None
try:
self.controller.create(
self._controller.create(
creator_identifier,
subset_name,
instance_data,
@ -1207,7 +821,7 @@ class CreateDialog(QtWidgets.QDialog):
if error_msg is None:
self._set_creator(self._selected_creator)
self._emit_message("Creation finished...")
self._controller.emit_card_message("Creation finished...")
else:
box = CreateErrorMessageBox(
creator_label,

View file

@ -0,0 +1,82 @@
try:
import commonmark
except Exception:
commonmark = None
from Qt import QtWidgets, QtCore
class HelpButton(QtWidgets.QPushButton):
"""Button used to trigger help dialog."""
def __init__(self, parent):
super(HelpButton, self).__init__(parent)
self.setObjectName("CreateDialogHelpButton")
self.setText("?")
class HelpWidget(QtWidgets.QWidget):
"""Widget showing help for single functionality."""
def __init__(self, parent):
super(HelpWidget, self).__init__(parent)
# TODO add hints what to help with?
detail_description_input = QtWidgets.QTextEdit(self)
detail_description_input.setObjectName("CreatorDetailedDescription")
detail_description_input.setTextInteractionFlags(
QtCore.Qt.TextBrowserInteraction
)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
main_layout.addWidget(detail_description_input, 1)
self._detail_description_input = detail_description_input
self.set_detailed_text()
def set_detailed_text(self, text=None):
if not text:
text = "We didn't prepare help for this part..."
if commonmark:
html = commonmark.commonmark(text)
self._detail_description_input.setHtml(html)
else:
self._detail_description_input.setMarkdown(text)
class HelpDialog(QtWidgets.QDialog):
default_width = 530
default_height = 340
def __init__(self, controller, parent):
super(HelpDialog, self).__init__(parent)
self.setWindowTitle("Help dialog")
help_content = HelpWidget(self)
main_layout = QtWidgets.QHBoxLayout(self)
main_layout.addWidget(help_content, 1)
controller.event_system.add_callback(
"show.detailed.help", self._on_help_request
)
self._controller = controller
self._help_content = help_content
def _on_help_request(self, event):
message = event.get("message")
self.set_detailed_text(message)
def set_detailed_text(self, text=None):
self._help_content.set_detailed_text(text)
def showEvent(self, event):
super(HelpDialog, self).showEvent(event)
self.resize(self.default_width, self.default_height)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

@ -0,0 +1,368 @@
from Qt import QtWidgets, QtCore
from .border_label_widget import BorderedLabelWidget
from .card_view_widgets import InstanceCardView
from .list_view_widgets import InstanceListView
from .widgets import (
SubsetAttributesWidget,
CreateInstanceBtn,
RemoveInstanceBtn,
ChangeViewBtn,
)
from .create_widget import CreateWidget
class OverviewWidget(QtWidgets.QFrame):
active_changed = QtCore.Signal()
instance_context_changed = QtCore.Signal()
create_requested = QtCore.Signal()
anim_end_value = 200
anim_duration = 200
def __init__(self, controller, parent):
super(OverviewWidget, self).__init__(parent)
self._refreshing_instances = False
self._controller = controller
create_widget = CreateWidget(controller, self)
# --- Created Subsets/Instances ---
# Common widget for creation and overview
subset_views_widget = BorderedLabelWidget(
"Subsets to publish", self
)
subset_view_cards = InstanceCardView(controller, subset_views_widget)
subset_list_view = InstanceListView(controller, subset_views_widget)
subset_views_layout = QtWidgets.QStackedLayout()
subset_views_layout.addWidget(subset_view_cards)
subset_views_layout.addWidget(subset_list_view)
subset_views_layout.setCurrentWidget(subset_view_cards)
# Buttons at the bottom of subset view
create_btn = CreateInstanceBtn(self)
delete_btn = RemoveInstanceBtn(self)
change_view_btn = ChangeViewBtn(self)
# --- Overview ---
# Subset details widget
subset_attributes_wrap = BorderedLabelWidget(
"Publish options", self
)
subset_attributes_widget = SubsetAttributesWidget(
controller, subset_attributes_wrap
)
subset_attributes_wrap.set_center_widget(subset_attributes_widget)
# Layout of buttons at the bottom of subset view
subset_view_btns_layout = QtWidgets.QHBoxLayout()
subset_view_btns_layout.setContentsMargins(0, 5, 0, 0)
subset_view_btns_layout.addWidget(create_btn)
subset_view_btns_layout.addSpacing(5)
subset_view_btns_layout.addWidget(delete_btn)
subset_view_btns_layout.addStretch(1)
subset_view_btns_layout.addWidget(change_view_btn)
# Layout of view and buttons
# - widget 'subset_view_widget' is necessary
# - only layout won't be resized automatically to minimum size hint
# on child resize request!
subset_view_widget = QtWidgets.QWidget(subset_views_widget)
subset_view_layout = QtWidgets.QVBoxLayout(subset_view_widget)
subset_view_layout.setContentsMargins(0, 0, 0, 0)
subset_view_layout.addLayout(subset_views_layout, 1)
subset_view_layout.addLayout(subset_view_btns_layout, 0)
subset_views_widget.set_center_widget(subset_view_widget)
# Whole subset layout with attributes and details
subset_content_widget = QtWidgets.QWidget(self)
subset_content_layout = QtWidgets.QHBoxLayout(subset_content_widget)
subset_content_layout.setContentsMargins(0, 0, 0, 0)
subset_content_layout.addWidget(create_widget, 7)
subset_content_layout.addWidget(subset_views_widget, 3)
subset_content_layout.addWidget(subset_attributes_wrap, 7)
# Subset frame layout
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(subset_content_widget, 1)
change_anim = QtCore.QVariantAnimation()
change_anim.setStartValue(0)
change_anim.setEndValue(self.anim_end_value)
change_anim.setDuration(self.anim_duration)
change_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad)
# --- Calbacks for instances/subsets view ---
create_btn.clicked.connect(self._on_create_clicked)
delete_btn.clicked.connect(self._on_delete_clicked)
change_view_btn.clicked.connect(self._on_change_view_clicked)
change_anim.valueChanged.connect(self._on_change_anim)
change_anim.finished.connect(self._on_change_anim_finished)
# Selection changed
subset_list_view.selection_changed.connect(
self._on_subset_change
)
subset_view_cards.selection_changed.connect(
self._on_subset_change
)
# Active instances changed
subset_list_view.active_changed.connect(
self._on_active_changed
)
subset_view_cards.active_changed.connect(
self._on_active_changed
)
# Instance context has changed
subset_attributes_widget.instance_context_changed.connect(
self._on_instance_context_change
)
# --- Controller callbacks ---
controller.event_system.add_callback(
"publish.process.started", self._on_publish_start
)
controller.event_system.add_callback(
"publish.reset.finished", self._on_publish_reset
)
controller.event_system.add_callback(
"instances.refresh.finished", self._on_instances_refresh
)
self._subset_content_widget = subset_content_widget
self._subset_content_layout = subset_content_layout
self._subset_view_cards = subset_view_cards
self._subset_list_view = subset_list_view
self._subset_views_layout = subset_views_layout
self._delete_btn = delete_btn
self._subset_attributes_widget = subset_attributes_widget
self._create_widget = create_widget
self._subset_views_widget = subset_views_widget
self._subset_attributes_wrap = subset_attributes_wrap
self._change_anim = change_anim
# Start in create mode
self._create_widget_policy = create_widget.sizePolicy()
self._subset_views_widget_policy = subset_views_widget.sizePolicy()
self._subset_attributes_wrap_policy = (
subset_attributes_wrap.sizePolicy()
)
self._max_widget_width = None
self._current_state = "create"
subset_attributes_wrap.setVisible(False)
def set_state(self, new_state, animate):
if new_state == self._current_state:
return
self._current_state = new_state
anim_is_running = (
self._change_anim.state() == self._change_anim.Running
)
if not animate:
self._change_visibility_for_state()
if anim_is_running:
self._change_anim.stop()
return
if self._max_widget_width is None:
self._max_widget_width = self._subset_views_widget.maximumWidth()
if new_state == "create":
direction = self._change_anim.Backward
else:
direction = self._change_anim.Forward
self._change_anim.setDirection(direction)
if not anim_is_running:
view_width = self._subset_views_widget.width()
self._subset_views_widget.setMinimumWidth(view_width)
self._subset_views_widget.setMaximumWidth(view_width)
self._change_anim.start()
def _on_create_clicked(self):
"""Pass signal to parent widget which should care about changing state.
We don't change anything here until the parent will care about it.
"""
self.create_requested.emit()
def _on_delete_clicked(self):
instances, _ = self.get_selected_items()
# Ask user if he really wants to remove instances
dialog = QtWidgets.QMessageBox(self)
dialog.setIcon(QtWidgets.QMessageBox.Question)
dialog.setWindowTitle("Are you sure?")
if len(instances) > 1:
msg = (
"Do you really want to remove {} instances?"
).format(len(instances))
else:
msg = (
"Do you really want to remove the instance?"
)
dialog.setText(msg)
dialog.setStandardButtons(
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel
)
dialog.setDefaultButton(QtWidgets.QMessageBox.Ok)
dialog.setEscapeButton(QtWidgets.QMessageBox.Cancel)
dialog.exec_()
# Skip if OK was not clicked
if dialog.result() == QtWidgets.QMessageBox.Ok:
self._controller.remove_instances(instances)
def _on_change_view_clicked(self):
self._change_view_type()
def _on_subset_change(self, *_args):
# Ignore changes if in middle of refreshing
if self._refreshing_instances:
return
instances, context_selected = self.get_selected_items()
# Disable delete button if nothing is selected
self._delete_btn.setEnabled(len(instances) > 0)
self._subset_attributes_widget.set_current_instances(
instances, context_selected
)
def _on_active_changed(self):
if self._refreshing_instances:
return
self.active_changed.emit()
def _on_change_anim(self, value):
self._create_widget.setVisible(True)
self._subset_attributes_wrap.setVisible(True)
width = (
self._subset_content_widget.width()
- (
self._subset_views_widget.width()
+ (self._subset_content_layout.spacing() * 2)
)
)
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)
self._create_widget.setMaximumWidth(create_width)
self._subset_attributes_wrap.setMinimumWidth(subset_attrs_width)
self._subset_attributes_wrap.setMaximumWidth(subset_attrs_width)
def _on_change_anim_finished(self):
self._change_visibility_for_state()
self._create_widget.setMinimumWidth(0)
self._create_widget.setMaximumWidth(self._max_widget_width)
self._subset_attributes_wrap.setMinimumWidth(0)
self._subset_attributes_wrap.setMaximumWidth(self._max_widget_width)
self._subset_views_widget.setMinimumWidth(0)
self._subset_views_widget.setMaximumWidth(self._max_widget_width)
self._create_widget.setSizePolicy(
self._create_widget_policy
)
self._subset_attributes_wrap.setSizePolicy(
self._subset_attributes_wrap_policy
)
self._subset_views_widget.setSizePolicy(
self._subset_views_widget_policy
)
def _change_visibility_for_state(self):
self._create_widget.setVisible(
self._current_state == "create"
)
self._subset_attributes_wrap.setVisible(
self._current_state == "publish"
)
def _on_instance_context_change(self):
current_idx = self._subset_views_layout.currentIndex()
for idx in range(self._subset_views_layout.count()):
if idx == current_idx:
continue
widget = self._subset_views_layout.widget(idx)
if widget.refreshed:
widget.set_refreshed(False)
current_widget = self._subset_views_layout.widget(current_idx)
current_widget.refresh_instance_states()
self.instance_context_changed.emit()
def get_selected_items(self):
view = self._subset_views_layout.currentWidget()
return view.get_selected_items()
def _change_view_type(self):
idx = self._subset_views_layout.currentIndex()
new_idx = (idx + 1) % self._subset_views_layout.count()
self._subset_views_layout.setCurrentIndex(new_idx)
new_view = self._subset_views_layout.currentWidget()
if not new_view.refreshed:
new_view.refresh()
new_view.set_refreshed(True)
else:
new_view.refresh_instance_states()
self._on_subset_change()
def _refresh_instances(self):
if self._refreshing_instances:
return
self._refreshing_instances = True
for idx in range(self._subset_views_layout.count()):
widget = self._subset_views_layout.widget(idx)
widget.set_refreshed(False)
view = self._subset_views_layout.currentWidget()
view.refresh()
view.set_refreshed(True)
self._refreshing_instances = False
# Force to change instance and refresh details
self._on_subset_change()
def _on_publish_start(self):
"""Publish started."""
self._subset_attributes_wrap.setEnabled(False)
def _on_publish_reset(self):
"""Context in controller has been refreshed."""
self._subset_attributes_wrap.setEnabled(True)
self._subset_content_widget.setEnabled(self._controller.host_is_valid)
def _on_instances_refresh(self):
"""Controller refreshed instances."""
self._refresh_instances()
# Give a change to process Resize Request
QtWidgets.QApplication.processEvents()
# Trigger update geometry of
widget = self._subset_views_layout.currentWidget()
widget.updateGeometry()

View file

@ -0,0 +1,520 @@
import os
import json
import time
from Qt import QtWidgets, QtCore
from openpype.pipeline import KnownPublishError
from .widgets import (
StopBtn,
ResetBtn,
ValidateBtn,
PublishBtn,
PublishReportBtn,
)
class PublishFrame(QtWidgets.QWidget):
"""Frame showed during publishing.
Shows all information related to publishing. Contains validation error
widget which is showed if only validation error happens during validation.
Processing layer is default layer. Validation error layer is shown if only
validation exception is raised during publishing. Report layer is available
only when publishing process is stopped and must be manually triggered to
change into that layer.
+------------------------------------------------------------------------+
| < Main label > |
| < Label top > |
| (#### 10% <Progress bar> ) |
| <Instance label> <Plugin label> |
| <Report> <Reset><Stop><Validate><Publish> |
+------------------------------------------------------------------------+
"""
details_page_requested = QtCore.Signal()
def __init__(self, controller, borders, parent):
super(PublishFrame, self).__init__(parent)
# Bottom part of widget where process and callback buttons are showed
# - QFrame used to be able set background using stylesheets easily
# and not override all children widgets style
content_frame = QtWidgets.QFrame(self)
content_frame.setObjectName("PublishInfoFrame")
top_content_widget = QtWidgets.QWidget(content_frame)
# Center widget displaying current state (without any specific info)
main_label = QtWidgets.QLabel(top_content_widget)
main_label.setObjectName("PublishInfoMainLabel")
main_label.setAlignment(QtCore.Qt.AlignCenter)
# Supporting labels for main label
# Top label is displayed just under main label
message_label_top = QtWidgets.QLabel(top_content_widget)
message_label_top.setAlignment(QtCore.Qt.AlignCenter)
# Label showing currently processed instance
progress_widget = QtWidgets.QWidget(top_content_widget)
instance_plugin_widget = QtWidgets.QWidget(progress_widget)
instance_label = QtWidgets.QLabel(
"<Instance name>", instance_plugin_widget
)
instance_label.setAlignment(
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
)
# Label showing currently processed plugin
plugin_label = QtWidgets.QLabel(
"<Plugin name>", instance_plugin_widget
)
plugin_label.setAlignment(
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
)
instance_plugin_layout = QtWidgets.QHBoxLayout(instance_plugin_widget)
instance_plugin_layout.setContentsMargins(0, 0, 0, 0)
instance_plugin_layout.addWidget(instance_label, 1)
instance_plugin_layout.addWidget(plugin_label, 1)
# Progress bar showing progress of publishing
progress_bar = QtWidgets.QProgressBar(progress_widget)
progress_bar.setObjectName("PublishProgressBar")
progress_layout = QtWidgets.QVBoxLayout(progress_widget)
progress_layout.setSpacing(5)
progress_layout.setContentsMargins(0, 0, 0, 0)
progress_layout.addWidget(instance_plugin_widget, 0)
progress_layout.addWidget(progress_bar, 0)
top_content_layout = QtWidgets.QVBoxLayout(top_content_widget)
top_content_layout.setContentsMargins(0, 0, 0, 0)
top_content_layout.setSpacing(5)
top_content_layout.setAlignment(QtCore.Qt.AlignCenter)
top_content_layout.addWidget(main_label)
# TODO stretches should be probably replaced by spacing...
# - stretch in floating frame doesn't make sense
top_content_layout.addWidget(message_label_top)
top_content_layout.addWidget(progress_widget)
# Publishing buttons to stop, reset or trigger publishing
footer_widget = QtWidgets.QWidget(content_frame)
report_btn = PublishReportBtn(footer_widget)
shrunk_main_label = QtWidgets.QLabel(footer_widget)
shrunk_main_label.setObjectName("PublishInfoMainLabel")
shrunk_main_label.setAlignment(
QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft
)
reset_btn = ResetBtn(footer_widget)
stop_btn = StopBtn(footer_widget)
validate_btn = ValidateBtn(footer_widget)
publish_btn = PublishBtn(footer_widget)
report_btn.add_action("Go to details", "go_to_report")
report_btn.add_action("Copy report", "copy_report")
report_btn.add_action("Export report", "export_report")
# Footer on info frame layout
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
footer_layout.setContentsMargins(0, 0, 0, 0)
footer_layout.addWidget(report_btn, 0)
footer_layout.addWidget(shrunk_main_label, 1)
footer_layout.addWidget(reset_btn, 0)
footer_layout.addWidget(stop_btn, 0)
footer_layout.addWidget(validate_btn, 0)
footer_layout.addWidget(publish_btn, 0)
# Info frame content
content_layout = QtWidgets.QVBoxLayout(content_frame)
content_layout.setSpacing(5)
content_layout.addWidget(top_content_widget)
content_layout.addWidget(footer_widget)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(borders, 0, borders, borders)
main_layout.addWidget(content_frame)
shrunk_anim = QtCore.QVariantAnimation()
shrunk_anim.setDuration(140)
shrunk_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad)
# Force translucent background for widgets
for widget in (
self,
top_content_widget,
footer_widget,
progress_widget,
instance_plugin_widget,
):
widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
report_btn.triggered.connect(self._on_report_triggered)
reset_btn.clicked.connect(self._on_reset_clicked)
stop_btn.clicked.connect(self._on_stop_clicked)
validate_btn.clicked.connect(self._on_validate_clicked)
publish_btn.clicked.connect(self._on_publish_clicked)
shrunk_anim.valueChanged.connect(self._on_shrunk_anim)
shrunk_anim.finished.connect(self._on_shrunk_anim_finish)
controller.event_system.add_callback(
"publish.reset.finished", self._on_publish_reset
)
controller.event_system.add_callback(
"publish.process.started", self._on_publish_start
)
controller.event_system.add_callback(
"publish.process.validated", self._on_publish_validated
)
controller.event_system.add_callback(
"publish.process.stopped", self._on_publish_stop
)
controller.event_system.add_callback(
"publish.process.instance.changed", self._on_instance_change
)
controller.event_system.add_callback(
"publish.process.plugin.changed", self._on_plugin_change
)
self._shrunk_anim = shrunk_anim
self.controller = controller
self._content_frame = content_frame
self._content_layout = content_layout
self._top_content_layout = top_content_layout
self._top_content_widget = top_content_widget
self._main_label = main_label
self._message_label_top = message_label_top
self._instance_label = instance_label
self._plugin_label = plugin_label
self._progress_bar = progress_bar
self._progress_widget = progress_widget
self._shrunk_main_label = shrunk_main_label
self._reset_btn = reset_btn
self._stop_btn = stop_btn
self._validate_btn = validate_btn
self._publish_btn = publish_btn
self._shrunken = False
self._top_widget_max_height = None
self._top_widget_size_policy = top_content_widget.sizePolicy()
self._last_instance_label = None
self._last_plugin_label = None
def mouseReleaseEvent(self, event):
super(PublishFrame, self).mouseReleaseEvent(event)
self._change_shrunk_state()
def _change_shrunk_state(self):
self.set_shrunk_state(not self._shrunken)
def set_shrunk_state(self, shrunk):
if shrunk is self._shrunken:
return
if self._top_widget_max_height is None:
self._top_widget_max_height = (
self._top_content_widget.maximumHeight()
)
self._shrunken = shrunk
anim_is_running = (
self._shrunk_anim.state() == self._shrunk_anim.Running
)
if not self.isVisible():
if anim_is_running:
self._shrunk_anim.stop()
self._on_shrunk_anim_finish()
return
start = 0
end = 0
if shrunk:
start = self._top_content_widget.height()
else:
if anim_is_running:
start = self._shrunk_anim.currentValue()
hint = self._top_content_widget.minimumSizeHint()
end = hint.height()
self._shrunk_anim.setStartValue(start)
self._shrunk_anim.setEndValue(end)
if not anim_is_running:
self._shrunk_anim.start()
def _on_shrunk_anim(self, value):
diff = self._top_content_widget.height() - value
if not self._top_content_widget.isVisible():
diff -= self._content_layout.spacing()
window_pos = self.pos()
window_pos_y = window_pos.y() + diff
window_height = self.height() - diff
self._top_content_widget.setMinimumHeight(value)
self._top_content_widget.setMaximumHeight(value)
self._top_content_widget.setVisible(True)
self.resize(self.width(), window_height)
self.move(window_pos.x(), window_pos_y)
def _on_shrunk_anim_finish(self):
self._top_content_widget.setVisible(not self._shrunken)
self._top_content_widget.setMinimumHeight(0)
self._top_content_widget.setMaximumHeight(
self._top_widget_max_height
)
self._top_content_widget.setSizePolicy(self._top_widget_size_policy)
if self._shrunken:
self._shrunk_main_label.setText(self._main_label.text())
else:
self._shrunk_main_label.setText("")
if self._shrunken:
content_frame_hint = self._content_frame.sizeHint()
layout = self.layout()
margins = layout.contentsMargins()
window_height = (
content_frame_hint.height()
+ margins.bottom()
+ margins.top()
)
diff = self.height() - window_height
window_pos = self.pos()
window_pos_y = window_pos.y() + diff
self.resize(self.width(), window_height)
self.move(window_pos.x(), window_pos_y)
def _set_main_label(self, message):
self._main_label.setText(message)
if self._shrunken:
self._shrunk_main_label.setText(message)
def _on_publish_reset(self):
self._last_instance_label = None
self._last_plugin_label = None
self._set_success_property()
self._set_progress_visibility(True)
self._main_label.setText("Hit publish (play button)! If you want")
self._message_label_top.setText("")
self._reset_btn.setEnabled(True)
self._stop_btn.setEnabled(False)
self._validate_btn.setEnabled(True)
self._publish_btn.setEnabled(True)
self._progress_bar.setValue(self.controller.publish_progress)
self._progress_bar.setMaximum(self.controller.publish_max_progress)
def _on_publish_start(self):
if self._last_plugin_label:
self._plugin_label.setText(self._last_plugin_label)
if self._last_instance_label:
self._instance_label.setText(self._last_instance_label)
self._set_success_property(-1)
self._set_progress_visibility(True)
self._set_main_label("Publishing...")
self._reset_btn.setEnabled(False)
self._stop_btn.setEnabled(True)
self._validate_btn.setEnabled(False)
self._publish_btn.setEnabled(False)
self.set_shrunk_state(False)
def _on_publish_validated(self):
self._validate_btn.setEnabled(False)
def _on_instance_change(self, event):
"""Change instance label when instance is going to be processed."""
self._last_instance_label = event["instance_label"]
self._instance_label.setText(event["instance_label"])
QtWidgets.QApplication.processEvents()
def _on_plugin_change(self, event):
"""Change plugin label when instance is going to be processed."""
self._last_plugin_label = event["plugin_label"]
self._progress_bar.setValue(self.controller.publish_progress)
self._plugin_label.setText(event["plugin_label"])
QtWidgets.QApplication.processEvents()
def _on_publish_stop(self):
self._progress_bar.setValue(self.controller.publish_progress)
self._reset_btn.setEnabled(True)
self._stop_btn.setEnabled(False)
self._instance_label.setText("")
self._plugin_label.setText("")
validate_enabled = not self.controller.publish_has_crashed
publish_enabled = not self.controller.publish_has_crashed
if validate_enabled:
validate_enabled = not self.controller.publish_has_validated
if publish_enabled:
if (
self.controller.publish_has_validated
and self.controller.publish_has_validation_errors
):
publish_enabled = False
else:
publish_enabled = not self.controller.publish_has_finished
self._validate_btn.setEnabled(validate_enabled)
self._publish_btn.setEnabled(publish_enabled)
error = self.controller.get_publish_crash_error()
validation_errors = self.controller.get_validation_errors()
if error:
self._set_error(error)
elif validation_errors:
self._set_progress_visibility(False)
self._set_validation_errors()
elif self.controller.publish_has_finished:
self._set_finished()
else:
self._set_stopped()
def _set_stopped(self):
main_label = "Publish paused"
if self.controller.publish_has_validated:
main_label += " - Validation passed"
self._set_main_label(main_label)
self._message_label_top.setText(
"Hit publish (play button) to continue."
)
self._set_success_property(-1)
def _set_error(self, error):
self._set_main_label("Error happened")
if isinstance(error, KnownPublishError):
msg = str(error)
else:
msg = (
"Something went wrong. Send report"
" to your supervisor or OpenPype."
)
self._message_label_top.setText(msg)
self._set_success_property(0)
def _set_validation_errors(self):
self._set_main_label("Your publish didn't pass studio validations")
self._message_label_top.setText("Check results above please")
self._set_success_property(2)
def _set_finished(self):
self._set_main_label("Finished")
self._message_label_top.setText("")
self._set_success_property(1)
def _set_progress_visibility(self, visible):
window_height = self.height()
self._progress_widget.setVisible(visible)
# Ignore rescaling and move of widget if is shrunken of progress bar
# should be visible
if self._shrunken or visible:
return
height = self._progress_widget.height()
diff = height + self._top_content_layout.spacing()
window_pos = self.pos()
window_pos_y = self.pos().y() + diff
window_height -= diff
self.resize(self.width(), window_height)
self.move(window_pos.x(), window_pos_y)
def _set_success_property(self, state=None):
if state is None:
state = ""
else:
state = str(state)
for widget in (self._progress_bar, self._content_frame):
if widget.property("state") != state:
widget.setProperty("state", state)
widget.style().polish(widget)
def _copy_report(self):
logs = self.controller.get_publish_report()
logs_string = json.dumps(logs, indent=4)
mime_data = QtCore.QMimeData()
mime_data.setText(logs_string)
QtWidgets.QApplication.instance().clipboard().setMimeData(
mime_data
)
def _export_report(self):
default_filename = "publish-report-{}".format(
time.strftime("%y%m%d-%H-%M")
)
default_filepath = os.path.join(
os.path.expanduser("~"),
default_filename
)
new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName(
self, "Save report", default_filepath, ".json"
)
if not ext or not new_filepath:
return
logs = self.controller.get_publish_report()
full_path = new_filepath + ext
dir_path = os.path.dirname(full_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
with open(full_path, "w") as file_stream:
json.dump(logs, file_stream)
def _on_report_triggered(self, identifier):
if identifier == "export_report":
self._export_report()
elif identifier == "copy_report":
self._copy_report()
elif identifier == "go_to_report":
self.details_page_requested.emit()
def _on_reset_clicked(self):
self.controller.reset()
def _on_stop_clicked(self):
self.controller.stop_publish()
def _on_validate_clicked(self):
self.controller.validate()
def _on_publish_clicked(self):
self.controller.publish()

View file

@ -1,519 +0,0 @@
import os
import json
import time
from Qt import QtWidgets, QtCore, QtGui
from openpype.pipeline import KnownPublishError
from .validations_widget import ValidationsWidget
from ..publish_report_viewer import PublishReportViewerWidget
from .widgets import (
StopBtn,
ResetBtn,
ValidateBtn,
PublishBtn,
CopyPublishReportBtn,
SavePublishReportBtn,
ShowPublishReportBtn
)
class ActionsButton(QtWidgets.QToolButton):
def __init__(self, parent=None):
super(ActionsButton, self).__init__(parent)
self.setText("< No action >")
self.setPopupMode(self.MenuButtonPopup)
menu = QtWidgets.QMenu(self)
self.setMenu(menu)
self._menu = menu
self._actions = []
self._current_action = None
self.clicked.connect(self._on_click)
def current_action(self):
return self._current_action
def add_action(self, action):
self._actions.append(action)
action.triggered.connect(self._on_action_trigger)
self._menu.addAction(action)
if self._current_action is None:
self._set_action(action)
def set_action(self, action):
if action not in self._actions:
self.add_action(action)
self._set_action(action)
def _set_action(self, action):
if action is self._current_action:
return
self._current_action = action
self.setText(action.text())
self.setIcon(action.icon())
def _on_click(self):
self._current_action.trigger()
def _on_action_trigger(self):
action = self.sender()
if action not in self._actions:
return
self._set_action(action)
class PublishFrame(QtWidgets.QFrame):
"""Frame showed during publishing.
Shows all information related to publishing. Contains validation error
widget which is showed if only validation error happens during validation.
Processing layer is default layer. Validation error layer is shown if only
validation exception is raised during publishing. Report layer is available
only when publishing process is stopped and must be manually triggered to
change into that layer.
+------------------------------------------------------------------------+
| |
| |
| |
| < Validation error widget > |
| |
| |
| |
| |
+------------------------------------------------------------------------+
| < Main label > |
| < Label top > |
| (#### 10% <Progress bar> ) |
| <Instance label> <Plugin label> |
| Report: <Copy><Save> <Label bottom> <Reset><Stop><Validate><Publish> |
+------------------------------------------------------------------------+
"""
def __init__(self, controller, parent):
super(PublishFrame, self).__init__(parent)
self.setObjectName("PublishFrame")
# Widget showing validation errors. Their details and action callbacks.
validation_errors_widget = ValidationsWidget(controller, self)
# Bottom part of widget where process and callback buttons are showed
# - QFrame used to be able set background using stylesheets easily
# and not override all children widgets style
info_frame = QtWidgets.QFrame(self)
info_frame.setObjectName("PublishInfoFrame")
# Content of info frame
# - separated into QFrame and QWidget (widget has transparent bg)
content_widget = QtWidgets.QWidget(info_frame)
content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
info_layout = QtWidgets.QVBoxLayout(info_frame)
info_layout.setContentsMargins(0, 0, 0, 0)
info_layout.addWidget(content_widget)
# Center widget displaying current state (without any specific info)
main_label = QtWidgets.QLabel(content_widget)
main_label.setObjectName("PublishInfoMainLabel")
main_label.setAlignment(QtCore.Qt.AlignCenter)
# Supporting labels for main label
# Top label is displayed just under main label
message_label_top = QtWidgets.QLabel(content_widget)
message_label_top.setAlignment(QtCore.Qt.AlignCenter)
# Bottom label is displayed between report and publish buttons
# at bottom part of info frame
message_label_bottom = QtWidgets.QLabel(content_widget)
message_label_bottom.setAlignment(QtCore.Qt.AlignCenter)
# Label showing currently processed instance
instance_label = QtWidgets.QLabel("<Instance name>", content_widget)
instance_label.setAlignment(
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
)
# Label showing currently processed plugin
plugin_label = QtWidgets.QLabel("<Plugin name>", content_widget)
plugin_label.setAlignment(
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
)
instance_plugin_layout = QtWidgets.QHBoxLayout()
instance_plugin_layout.addWidget(instance_label, 1)
instance_plugin_layout.addWidget(plugin_label, 1)
# Progress bar showing progress of publishing
progress_widget = QtWidgets.QProgressBar(content_widget)
progress_widget.setObjectName("PublishProgressBar")
# Report buttons to be able copy, save or see report
report_btns_widget = QtWidgets.QWidget(content_widget)
report_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
# Hidden by default
report_btns_widget.setVisible(False)
report_label = QtWidgets.QLabel("Report:", report_btns_widget)
copy_report_btn = CopyPublishReportBtn(report_btns_widget)
export_report_btn = SavePublishReportBtn(report_btns_widget)
show_details_btn = ShowPublishReportBtn(report_btns_widget)
report_btns_layout = QtWidgets.QHBoxLayout(report_btns_widget)
report_btns_layout.setContentsMargins(0, 0, 0, 0)
report_btns_layout.addWidget(report_label, 0)
report_btns_layout.addWidget(copy_report_btn, 0)
report_btns_layout.addWidget(export_report_btn, 0)
report_btns_layout.addWidget(show_details_btn, 0)
# Publishing buttons to stop, reset or trigger publishing
reset_btn = ResetBtn(content_widget)
stop_btn = StopBtn(content_widget)
validate_btn = ValidateBtn(content_widget)
publish_btn = PublishBtn(content_widget)
# Footer on info frame layout
info_footer_layout = QtWidgets.QHBoxLayout()
info_footer_layout.addWidget(report_btns_widget, 0)
info_footer_layout.addWidget(message_label_bottom, 1)
info_footer_layout.addWidget(reset_btn, 0)
info_footer_layout.addWidget(stop_btn, 0)
info_footer_layout.addWidget(validate_btn, 0)
info_footer_layout.addWidget(publish_btn, 0)
# Info frame content
content_layout = QtWidgets.QVBoxLayout(content_widget)
content_layout.setSpacing(5)
content_layout.setAlignment(QtCore.Qt.AlignCenter)
content_layout.addWidget(main_label)
content_layout.addStretch(1)
content_layout.addWidget(message_label_top)
content_layout.addStretch(1)
content_layout.addLayout(instance_plugin_layout)
content_layout.addWidget(progress_widget)
content_layout.addStretch(1)
content_layout.addLayout(info_footer_layout)
# Whole widget layout
publish_widget = QtWidgets.QWidget(self)
publish_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
publish_layout = QtWidgets.QVBoxLayout(publish_widget)
publish_layout.addWidget(validation_errors_widget, 1)
publish_layout.addWidget(info_frame, 0)
details_widget = QtWidgets.QWidget(self)
report_view = PublishReportViewerWidget(details_widget)
close_report_btn = QtWidgets.QPushButton(details_widget)
close_report_icon = self._get_report_close_icon()
close_report_btn.setIcon(close_report_icon)
details_layout = QtWidgets.QVBoxLayout(details_widget)
details_layout.addWidget(report_view)
details_layout.addWidget(close_report_btn)
main_layout = QtWidgets.QStackedLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setStackingMode(main_layout.StackOne)
main_layout.addWidget(publish_widget)
main_layout.addWidget(details_widget)
main_layout.setCurrentWidget(publish_widget)
show_details_btn.clicked.connect(self._on_show_details)
copy_report_btn.clicked.connect(self._on_copy_report)
export_report_btn.clicked.connect(self._on_export_report)
reset_btn.clicked.connect(self._on_reset_clicked)
stop_btn.clicked.connect(self._on_stop_clicked)
validate_btn.clicked.connect(self._on_validate_clicked)
publish_btn.clicked.connect(self._on_publish_clicked)
close_report_btn.clicked.connect(self._on_close_report_clicked)
controller.add_publish_reset_callback(self._on_publish_reset)
controller.add_publish_started_callback(self._on_publish_start)
controller.add_publish_validated_callback(self._on_publish_validated)
controller.add_publish_stopped_callback(self._on_publish_stop)
controller.add_instance_change_callback(self._on_instance_change)
controller.add_plugin_change_callback(self._on_plugin_change)
self.controller = controller
self._info_frame = info_frame
self._publish_widget = publish_widget
self._validation_errors_widget = validation_errors_widget
self._main_layout = main_layout
self._main_label = main_label
self._message_label_top = message_label_top
self._instance_label = instance_label
self._plugin_label = plugin_label
self._progress_widget = progress_widget
self._report_btns_widget = report_btns_widget
self._message_label_bottom = message_label_bottom
self._reset_btn = reset_btn
self._stop_btn = stop_btn
self._validate_btn = validate_btn
self._publish_btn = publish_btn
self._details_widget = details_widget
self._report_view = report_view
def _get_report_close_icon(self):
size = 100
pix = QtGui.QPixmap(size, size)
pix.fill(QtCore.Qt.transparent)
half_stroke_size = size / 12
stroke_size = 2 * half_stroke_size
size_part = size / 5
p1 = QtCore.QPoint(half_stroke_size, size_part)
p2 = QtCore.QPoint(size / 2, size_part * 4)
p3 = QtCore.QPoint(size - half_stroke_size, size_part)
painter = QtGui.QPainter(pix)
pen = QtGui.QPen(QtCore.Qt.white)
pen.setWidth(stroke_size)
pen.setCapStyle(QtCore.Qt.RoundCap)
painter.setPen(pen)
painter.setBrush(QtCore.Qt.transparent)
painter.drawLine(p1, p2)
painter.drawLine(p2, p3)
painter.end()
return QtGui.QIcon(pix)
def _on_publish_reset(self):
self._set_success_property()
self._change_bg_property()
self._set_progress_visibility(True)
self._main_label.setText("Hit publish (play button)! If you want")
self._message_label_top.setText("")
self._message_label_bottom.setText("")
self._report_btns_widget.setVisible(False)
self._reset_btn.setEnabled(True)
self._stop_btn.setEnabled(False)
self._validate_btn.setEnabled(True)
self._publish_btn.setEnabled(True)
self._progress_widget.setValue(self.controller.publish_progress)
self._progress_widget.setMaximum(self.controller.publish_max_progress)
def _on_publish_start(self):
self._validation_errors_widget.clear()
self._set_success_property(-1)
self._change_bg_property()
self._set_progress_visibility(True)
self._main_label.setText("Publishing...")
self._report_btns_widget.setVisible(False)
self._reset_btn.setEnabled(False)
self._stop_btn.setEnabled(True)
self._validate_btn.setEnabled(False)
self._publish_btn.setEnabled(False)
def _on_publish_validated(self):
self._validate_btn.setEnabled(False)
def _on_instance_change(self, context, instance):
"""Change instance label when instance is going to be processed."""
if instance is None:
new_name = (
context.data.get("label")
or context.data.get("name")
or "Context"
)
else:
new_name = (
instance.data.get("label")
or instance.data["name"]
)
self._instance_label.setText(new_name)
QtWidgets.QApplication.processEvents()
def _on_plugin_change(self, plugin):
"""Change plugin label when instance is going to be processed."""
plugin_name = plugin.__name__
if hasattr(plugin, "label") and plugin.label:
plugin_name = plugin.label
self._progress_widget.setValue(self.controller.publish_progress)
self._plugin_label.setText(plugin_name)
QtWidgets.QApplication.processEvents()
def _on_publish_stop(self):
self._progress_widget.setValue(self.controller.publish_progress)
self._report_btns_widget.setVisible(True)
self._reset_btn.setEnabled(True)
self._stop_btn.setEnabled(False)
validate_enabled = not self.controller.publish_has_crashed
publish_enabled = not self.controller.publish_has_crashed
if validate_enabled:
validate_enabled = not self.controller.publish_has_validated
if publish_enabled:
if (
self.controller.publish_has_validated
and self.controller.publish_has_validation_errors
):
publish_enabled = False
else:
publish_enabled = not self.controller.publish_has_finished
self._validate_btn.setEnabled(validate_enabled)
self._publish_btn.setEnabled(publish_enabled)
error = self.controller.get_publish_crash_error()
validation_errors = self.controller.get_validation_errors()
if error:
self._set_error(error)
elif validation_errors:
self._set_progress_visibility(False)
self._change_bg_property(1)
self._set_validation_errors(validation_errors)
elif self.controller.publish_has_finished:
self._set_finished()
else:
self._set_stopped()
def _set_stopped(self):
main_label = "Publish paused"
if self.controller.publish_has_validated:
main_label += " - Validation passed"
self._main_label.setText(main_label)
self._message_label_top.setText(
"Hit publish (play button) to continue."
)
self._set_success_property(-1)
def _set_error(self, error):
self._main_label.setText("Error happened")
if isinstance(error, KnownPublishError):
msg = str(error)
else:
msg = (
"Something went wrong. Send report"
" to your supervisor or OpenPype."
)
self._message_label_top.setText(msg)
self._message_label_bottom.setText("")
self._set_success_property(0)
def _set_validation_errors(self, validation_errors):
self._main_label.setText("Your publish didn't pass studio validations")
self._message_label_top.setText("")
self._message_label_bottom.setText("Check results above please")
self._set_success_property(2)
self._validation_errors_widget.set_errors(validation_errors)
def _set_finished(self):
self._main_label.setText("Finished")
self._message_label_top.setText("")
self._message_label_bottom.setText("")
self._set_success_property(1)
def _change_bg_property(self, state=None):
self.setProperty("state", str(state or ""))
self.style().polish(self)
def _set_progress_visibility(self, visible):
self._instance_label.setVisible(visible)
self._plugin_label.setVisible(visible)
self._progress_widget.setVisible(visible)
self._message_label_top.setVisible(visible)
def _set_success_property(self, state=None):
if state is None:
state = ""
else:
state = str(state)
for widget in (self._progress_widget, self._info_frame):
if widget.property("state") != state:
widget.setProperty("state", state)
widget.style().polish(widget)
def _on_copy_report(self):
logs = self.controller.get_publish_report()
logs_string = json.dumps(logs, indent=4)
mime_data = QtCore.QMimeData()
mime_data.setText(logs_string)
QtWidgets.QApplication.instance().clipboard().setMimeData(
mime_data
)
def _on_export_report(self):
default_filename = "publish-report-{}".format(
time.strftime("%y%m%d-%H-%M")
)
default_filepath = os.path.join(
os.path.expanduser("~"),
default_filename
)
new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName(
self, "Save report", default_filepath, ".json"
)
if not ext or not new_filepath:
return
logs = self.controller.get_publish_report()
full_path = new_filepath + ext
dir_path = os.path.dirname(full_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
with open(full_path, "w") as file_stream:
json.dump(logs, file_stream)
def _on_show_details(self):
self._change_bg_property(2)
self._main_layout.setCurrentWidget(self._details_widget)
report_data = self.controller.get_publish_report()
self._report_view.set_report_data(report_data)
def _on_close_report_clicked(self):
self._report_view.close_details_popup()
if self.controller.get_publish_crash_error():
self._change_bg_property()
elif self.controller.get_validation_errors():
self._change_bg_property(1)
else:
self._change_bg_property(2)
self._main_layout.setCurrentWidget(self._publish_widget)
def _on_reset_clicked(self):
self.controller.reset()
def _on_stop_clicked(self):
self.controller.stop_publish()
def _on_validate_clicked(self):
self.controller.validate()
def _on_publish_clicked(self):
self.controller.publish()

View file

@ -0,0 +1,95 @@
from Qt import QtWidgets, QtCore
from openpype.tools.utils import set_style_property
class PublisherTabBtn(QtWidgets.QPushButton):
tab_clicked = QtCore.Signal(str)
def __init__(self, identifier, label, parent):
super(PublisherTabBtn, self).__init__(label, parent)
self._identifier = identifier
self._active = False
self.clicked.connect(self._on_click)
def _on_click(self):
self.tab_clicked.emit(self.identifier)
@property
def identifier(self):
return self._identifier
def activate(self):
if self._active:
return
self._active = True
set_style_property(self, "active", "1")
def deactivate(self):
if not self._active:
return
self._active = False
set_style_property(self, "active", "")
class PublisherTabsWidget(QtWidgets.QFrame):
tab_changed = QtCore.Signal(str, str)
def __init__(self, parent=None):
super(PublisherTabsWidget, self).__init__(parent)
btns_widget = QtWidgets.QWidget(self)
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
btns_layout.setContentsMargins(0, 0, 0, 0)
btns_layout.setSpacing(0)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(btns_widget, 0)
layout.addStretch(1)
self._btns_layout = btns_layout
self._current_identifier = None
self._buttons_by_identifier = {}
def is_current_tab(self, identifier):
if isinstance(identifier, PublisherTabBtn):
identifier = identifier.identifier
return self._current_identifier == identifier
def add_tab(self, label, identifier):
button = PublisherTabBtn(identifier, label, self)
button.tab_clicked.connect(self._on_tab_click)
self._btns_layout.addWidget(button, 0)
self._buttons_by_identifier[identifier] = button
if self._current_identifier is None:
self.set_current_tab(identifier)
return button
def set_current_tab(self, identifier):
if isinstance(identifier, PublisherTabBtn):
identifier = identifier.identifier
if identifier == self._current_identifier:
return
new_btn = self._buttons_by_identifier.get(identifier)
if new_btn is None:
return
old_identifier = self._current_identifier
old_btn = self._buttons_by_identifier.get(old_identifier)
self._current_identifier = identifier
if old_btn is not None:
old_btn.deactivate()
new_btn.activate()
self.tab_changed.emit(old_identifier, identifier)
def current_tab(self):
return self._current_identifier
def _on_tab_click(self, identifier):
self.set_current_tab(identifier)

View file

@ -141,10 +141,10 @@ class TasksModel(QtGui.QStandardItemModel):
return super(TasksModel, self).headerData(section, orientation, role)
class CreateDialogTasksWidget(TasksWidget):
class CreateWidgetTasksWidget(TasksWidget):
def __init__(self, controller, parent):
self._controller = controller
super(CreateDialogTasksWidget, self).__init__(None, parent)
super(CreateWidgetTasksWidget, self).__init__(None, parent)
self._enabled = None
@ -164,7 +164,7 @@ class CreateDialogTasksWidget(TasksWidget):
self.task_changed.emit()
def select_task_name(self, task_name):
super(CreateDialogTasksWidget, self).select_task_name(task_name)
super(CreateWidgetTasksWidget, self).select_task_name(task_name)
if not self._enabled:
current = self.get_selected_task_name()
if current:

View file

@ -6,7 +6,7 @@ except Exception:
from Qt import QtWidgets, QtCore, QtGui
from openpype.tools.utils import BaseClickableFrame
from openpype.tools.utils import BaseClickableFrame, ClickableFrame
from .widgets import (
IconValuePixmapLabel
)
@ -60,9 +60,8 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
self._error_info = error_info
self._selected = False
title_frame = BaseClickableFrame(self)
title_frame = ClickableFrame(self)
title_frame.setObjectName("ValidationErrorTitleFrame")
title_frame._mouse_release_callback = self._mouse_release_callback
toggle_instance_btn = QtWidgets.QToolButton(title_frame)
toggle_instance_btn.setObjectName("ArrowBtn")
@ -72,14 +71,15 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
label_widget = QtWidgets.QLabel(error_info["title"], title_frame)
title_frame_layout = QtWidgets.QHBoxLayout(title_frame)
title_frame_layout.addWidget(toggle_instance_btn)
title_frame_layout.addWidget(label_widget)
title_frame_layout.addWidget(label_widget, 1)
title_frame_layout.addWidget(toggle_instance_btn, 0)
instances_model = QtGui.QStandardItemModel()
error_info = error_info["error_info"]
help_text_by_instance_id = {}
context_validation = False
items = []
if (
not error_info
or (len(error_info) == 1 and error_info[0][0] is None)
@ -88,8 +88,10 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow)
description = self._prepare_description(error_info[0][1])
help_text_by_instance_id[None] = description
# Add fake item to have minimum size hint of view widget
items.append(QtGui.QStandardItem("Context"))
else:
items = []
for instance, exception in error_info:
label = instance.data.get("label") or instance.data.get("name")
item = QtGui.QStandardItem(label)
@ -102,29 +104,33 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
description = self._prepare_description(exception)
help_text_by_instance_id[instance.id] = description
instances_model.invisibleRootItem().appendRows(items)
if items:
root_item = instances_model.invisibleRootItem()
root_item.appendRows(items)
instances_view = ValidationErrorInstanceList(self)
instances_view.setModel(instances_model)
instances_view.setVisible(False)
self.setLayoutDirection(QtCore.Qt.LeftToRight)
view_layout = QtWidgets.QHBoxLayout()
view_widget = QtWidgets.QWidget(self)
view_layout = QtWidgets.QHBoxLayout(view_widget)
view_layout.setContentsMargins(0, 0, 0, 0)
view_layout.setSpacing(0)
view_layout.addSpacing(14)
view_layout.addWidget(instances_view)
view_layout.addWidget(instances_view, 0)
layout = QtWidgets.QVBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(title_frame)
layout.addLayout(view_layout)
layout.addWidget(title_frame, 0)
layout.addWidget(view_widget, 0)
view_widget.setVisible(False)
if not context_validation:
toggle_instance_btn.clicked.connect(self._on_toggle_btn_click)
title_frame.clicked.connect(self._mouse_release_callback)
instances_view.selectionModel().selectionChanged.connect(
self._on_seleciton_change
)
@ -133,7 +139,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
self._toggle_instance_btn = toggle_instance_btn
self._view_layout = view_layout
self._view_widget = view_widget
self._instances_model = instances_model
self._instances_view = instances_view
@ -141,17 +147,21 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
self._context_validation = context_validation
self._help_text_by_instance_id = help_text_by_instance_id
self._expanded = False
def sizeHint(self):
result = super(ValidationErrorTitleWidget, self).sizeHint()
expected_width = 0
for idx in range(self._view_layout.count()):
expected_width += self._view_layout.itemAt(idx).sizeHint().width()
expected_width = max(
self._view_widget.minimumSizeHint().width(),
self._view_widget.sizeHint().width()
)
if expected_width < 200:
expected_width = 200
if result.width() < expected_width:
result.setWidth(expected_width)
return result
def minimumSizeHint(self):
@ -170,6 +180,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
def _mouse_release_callback(self):
"""Mark this widget as selected on click."""
self.set_selected(True)
def current_desctiption_text(self):
@ -208,25 +219,44 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
if selected is None:
selected = not self._selected
elif selected == self._selected:
if not selected:
self._instances_view.clearSelection()
if selected == self._selected:
return
self._selected = selected
self._change_style_property(selected)
if selected:
self.selected.emit(self._index)
self._set_expanded(True)
def _on_toggle_btn_click(self):
"""Show/hide instances list."""
new_visible = not self._instances_view.isVisible()
self._instances_view.setVisible(new_visible)
if new_visible:
self._set_expanded()
def _set_expanded(self, expanded=None):
if expanded is None:
expanded = not self._expanded
elif expanded is self._expanded:
return
if expanded and self._context_validation:
return
self._expanded = expanded
self._view_widget.setVisible(expanded)
if expanded:
self._toggle_instance_btn.setArrowType(QtCore.Qt.DownArrow)
else:
self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
def _on_seleciton_change(self):
self.instance_changed.emit(self._index)
sel_model = self._instances_view.selectionModel()
if sel_model.selectedIndexes():
self.instance_changed.emit(self._index)
class ActionButton(BaseClickableFrame):
@ -400,7 +430,21 @@ class VerticallScrollArea(QtWidgets.QScrollArea):
return super(VerticallScrollArea, self).eventFilter(obj, event)
class ValidationsWidget(QtWidgets.QWidget):
class ValidationArtistMessage(QtWidgets.QWidget):
def __init__(self, message, parent):
super(ValidationArtistMessage, self).__init__(parent)
artist_msg_label = QtWidgets.QLabel(message, self)
artist_msg_label.setAlignment(QtCore.Qt.AlignCenter)
main_layout = QtWidgets.QHBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(
artist_msg_label, 1, QtCore.Qt.AlignCenter
)
class ValidationsWidget(QtWidgets.QFrame):
"""Widgets showing validation error.
This widget is shown if validation error/s happened during validation part.
@ -414,16 +458,35 @@ class ValidationsWidget(QtWidgets.QWidget):
Error detail
Publish buttons
"""
def __init__(self, controller, parent):
super(ValidationsWidget, self).__init__(parent)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
# Before publishing
before_publish_widget = ValidationArtistMessage(
"Nothing to report until you run publish", self
)
# After success publishing
publish_started_widget = ValidationArtistMessage(
"Publishing went smoothly", self
)
# After success publishing
publish_stop_ok_widget = ValidationArtistMessage(
"Publishing finished successfully", self
)
# After failed publishing (not with validation error)
publish_stop_fail_widget = ValidationArtistMessage(
"This is not your fault...", self
)
errors_scroll = VerticallScrollArea(self)
# Validation errors
validations_widget = QtWidgets.QWidget(self)
content_widget = QtWidgets.QWidget(validations_widget)
errors_scroll = VerticallScrollArea(content_widget)
errors_scroll.setWidgetResizable(True)
errors_widget = QtWidgets.QWidget(errors_scroll)
@ -433,35 +496,64 @@ class ValidationsWidget(QtWidgets.QWidget):
errors_scroll.setWidget(errors_widget)
error_details_frame = QtWidgets.QFrame(self)
error_details_frame = QtWidgets.QFrame(content_widget)
error_details_input = QtWidgets.QTextEdit(error_details_frame)
error_details_input.setObjectName("InfoText")
error_details_input.setTextInteractionFlags(
QtCore.Qt.TextBrowserInteraction
)
actions_widget = ValidateActionsWidget(controller, self)
actions_widget = ValidateActionsWidget(controller, content_widget)
actions_widget.setMinimumWidth(140)
error_details_layout = QtWidgets.QHBoxLayout(error_details_frame)
error_details_layout.addWidget(error_details_input, 1)
error_details_layout.addWidget(actions_widget, 0)
content_layout = QtWidgets.QHBoxLayout()
content_layout = QtWidgets.QHBoxLayout(content_widget)
content_layout.setSpacing(0)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.addWidget(errors_scroll, 0)
content_layout.addWidget(error_details_frame, 1)
top_label = QtWidgets.QLabel("Publish validation report", self)
top_label = QtWidgets.QLabel(
"Publish validation report", content_widget
)
top_label.setObjectName("PublishInfoMainLabel")
top_label.setAlignment(QtCore.Qt.AlignCenter)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(top_label)
layout.addLayout(content_layout)
validation_layout = QtWidgets.QVBoxLayout(validations_widget)
validation_layout.setContentsMargins(0, 0, 0, 0)
validation_layout.addWidget(top_label, 0)
validation_layout.addWidget(content_widget, 1)
main_layout = QtWidgets.QStackedLayout(self)
main_layout.addWidget(before_publish_widget)
main_layout.addWidget(publish_started_widget)
main_layout.addWidget(publish_stop_ok_widget)
main_layout.addWidget(publish_stop_fail_widget)
main_layout.addWidget(validations_widget)
main_layout.setCurrentWidget(before_publish_widget)
controller.event_system.add_callback(
"publish.process.started", self._on_publish_start
)
controller.event_system.add_callback(
"publish.reset.finished", self._on_publish_reset
)
controller.event_system.add_callback(
"publish.process.stopped", self._on_publish_stop
)
self._main_layout = main_layout
self._before_publish_widget = before_publish_widget
self._publish_started_widget = publish_started_widget
self._publish_stop_ok_widget = publish_stop_ok_widget
self._publish_stop_fail_widget = publish_stop_fail_widget
self._validations_widget = validations_widget
self._top_label = top_label
self._errors_widget = errors_widget
@ -474,6 +566,8 @@ class ValidationsWidget(QtWidgets.QWidget):
self._error_info = {}
self._previous_select = None
self._controller = controller
def clear(self):
"""Delete all dynamic widgets and hide all wrappers."""
self._title_widgets = {}
@ -537,6 +631,32 @@ class ValidationsWidget(QtWidgets.QWidget):
self.updateGeometry()
def _set_current_widget(self, widget):
self._main_layout.setCurrentWidget(widget)
def _on_publish_start(self):
self._set_current_widget(self._publish_started_widget)
def _on_publish_reset(self):
self._set_current_widget(self._before_publish_widget)
def _on_publish_stop(self):
if self._controller.publish_has_crashed:
self._set_current_widget(self._publish_stop_fail_widget)
return
if self._controller.publish_has_validation_errors:
validation_errors = self._controller.get_validation_errors()
self._set_current_widget(self._validations_widget)
self.set_errors(validation_errors)
return
if self._contoller.publish_has_finished:
self._set_current_widget(self._publish_stop_ok_widget)
return
self._set_current_widget(self._publish_started_widget)
def _on_select(self, index):
if self._previous_select:
if self._previous_select.index == index:
@ -553,8 +673,9 @@ class ValidationsWidget(QtWidgets.QWidget):
def _on_instance_change(self, index):
if self._previous_select and self._previous_select.index != index:
return
self._update_description()
self._title_widgets[index].set_selected(True)
else:
self._update_description()
def _update_description(self):
description = self._previous_select.current_desctiption_text()

View file

@ -2,6 +2,7 @@
import os
import re
import copy
import functools
import collections
from Qt import QtWidgets, QtCore, QtGui
import qtawesome
@ -182,6 +183,16 @@ class PublishIconBtn(IconButton):
return pixmap
class CreateBtn(PublishIconBtn):
"""Create instance button."""
def __init__(self, parent=None):
icon_path = get_icon_path("create")
super(CreateBtn, self).__init__(icon_path, "Create", parent)
self.setToolTip("Create new subset/s")
self.setLayoutDirection(QtCore.Qt.RightToLeft)
class ResetBtn(PublishIconBtn):
"""Publish reset button."""
def __init__(self, parent=None):
@ -222,28 +233,34 @@ class CreateInstanceBtn(PublishIconBtn):
self.setToolTip("Create new instance")
class CopyPublishReportBtn(PublishIconBtn):
"""Copy report button."""
def __init__(self, parent=None):
icon_path = get_icon_path("copy")
super(CopyPublishReportBtn, self).__init__(icon_path, parent)
self.setToolTip("Copy report")
class PublishReportBtn(PublishIconBtn):
"""Publish report button."""
triggered = QtCore.Signal(str)
class SavePublishReportBtn(PublishIconBtn):
"""Save report button."""
def __init__(self, parent=None):
icon_path = get_icon_path("download_arrow")
super(SavePublishReportBtn, self).__init__(icon_path, parent)
self.setToolTip("Export and save report")
class ShowPublishReportBtn(PublishIconBtn):
"""Show report button."""
def __init__(self, parent=None):
icon_path = get_icon_path("view_report")
super(ShowPublishReportBtn, self).__init__(icon_path, parent)
self.setToolTip("Show details")
super(PublishReportBtn, self).__init__(icon_path, parent)
self.setToolTip("Copy report")
self._actions = []
def add_action(self, label, identifier):
action = QtWidgets.QAction(label)
action.setData(identifier)
action.triggered.connect(
functools.partial(self._on_action_trigger, action)
)
self._actions.append(action)
def _on_action_trigger(self, action):
identifier = action.data()
self.triggered.emit(identifier)
def mouseReleaseEvent(self, event):
super(PublishReportBtn, self).mouseReleaseEvent(event)
menu = QtWidgets.QMenu(self)
menu.addActions(self._actions)
menu.exec_(event.globalPos())
class RemoveInstanceBtn(PublishIconBtn):

View file

@ -6,32 +6,35 @@ from openpype import (
)
from openpype.tools.utils import (
PlaceholderLineEdit,
PixmapLabel
MessageOverlayObject,
PixmapLabel,
)
from .publish_report_viewer import PublishReportViewerWidget
from .control import PublisherController
from .widgets import (
BorderedLabelWidget,
OverviewWidget,
ValidationsWidget,
PublishFrame,
SubsetAttributesWidget,
InstanceCardView,
InstanceListView,
CreateDialog,
PublisherTabsWidget,
StopBtn,
ResetBtn,
ValidateBtn,
PublishBtn,
CreateInstanceBtn,
RemoveInstanceBtn,
ChangeViewBtn
HelpButton,
HelpDialog,
)
class PublisherWindow(QtWidgets.QDialog):
"""Main window of publisher."""
default_width = 1200
default_height = 700
default_width = 1300
default_height = 800
footer_border = 8
publish_footer_spacer = 2
def __init__(self, parent=None, reset_on_show=None):
super(PublisherWindow, self).__init__(parent)
@ -58,122 +61,121 @@ class PublisherWindow(QtWidgets.QDialog):
| on_top_flag
)
self._reset_on_show = reset_on_show
self._first_show = True
self._refreshing_instances = False
controller = PublisherController()
help_dialog = HelpDialog(controller, self)
overlay_object = MessageOverlayObject(self)
# Header
header_widget = QtWidgets.QWidget(self)
icon_pixmap = QtGui.QPixmap(resources.get_openpype_icon_filepath())
icon_label = PixmapLabel(icon_pixmap, header_widget)
icon_label.setObjectName("PublishContextLabel")
context_label = QtWidgets.QLabel(header_widget)
context_label.setObjectName("PublishContextLabel")
header_extra_widget = QtWidgets.QWidget(header_widget)
help_btn = HelpButton(header_widget)
header_layout = QtWidgets.QHBoxLayout(header_widget)
header_layout.setContentsMargins(15, 15, 15, 15)
header_layout.setContentsMargins(15, 15, 0, 15)
header_layout.setSpacing(15)
header_layout.addWidget(icon_label, 0)
header_layout.addWidget(context_label, 1)
header_layout.addWidget(context_label, 0)
header_layout.addStretch(1)
header_layout.addWidget(header_extra_widget, 0)
header_layout.addWidget(help_btn, 0)
line_widget = QtWidgets.QWidget(self)
line_widget.setObjectName("Separator")
line_widget.setMinimumHeight(2)
# Tabs widget under header
tabs_widget = PublisherTabsWidget(self)
create_tab = tabs_widget.add_tab("Create", "create")
tabs_widget.add_tab("Publish", "publish")
tabs_widget.add_tab("Report", "report")
tabs_widget.add_tab("Details", "details")
# Content
content_stacked_widget = QtWidgets.QWidget(self)
# Subset widget
subset_frame = QtWidgets.QFrame(content_stacked_widget)
subset_views_widget = BorderedLabelWidget(
"Subsets to publish", subset_frame
)
subset_view_cards = InstanceCardView(controller, subset_views_widget)
subset_list_view = InstanceListView(controller, subset_views_widget)
subset_views_layout = QtWidgets.QStackedLayout()
subset_views_layout.addWidget(subset_view_cards)
subset_views_layout.addWidget(subset_list_view)
# Buttons at the bottom of subset view
create_btn = CreateInstanceBtn(subset_frame)
delete_btn = RemoveInstanceBtn(subset_frame)
change_view_btn = ChangeViewBtn(subset_frame)
# Subset details widget
subset_attributes_wrap = BorderedLabelWidget(
"Publish options", subset_frame
)
subset_attributes_widget = SubsetAttributesWidget(
controller, subset_attributes_wrap
)
subset_attributes_wrap.set_center_widget(subset_attributes_widget)
# Layout of buttons at the bottom of subset view
subset_view_btns_layout = QtWidgets.QHBoxLayout()
subset_view_btns_layout.setContentsMargins(0, 5, 0, 0)
subset_view_btns_layout.addWidget(create_btn)
subset_view_btns_layout.addSpacing(5)
subset_view_btns_layout.addWidget(delete_btn)
subset_view_btns_layout.addStretch(1)
subset_view_btns_layout.addWidget(change_view_btn)
# Layout of view and buttons
# - widget 'subset_view_widget' is necessary
# - only layout won't be resized automatically to minimum size hint
# on child resize request!
subset_view_widget = QtWidgets.QWidget(subset_views_widget)
subset_view_layout = QtWidgets.QVBoxLayout(subset_view_widget)
subset_view_layout.setContentsMargins(0, 0, 0, 0)
subset_view_layout.addLayout(subset_views_layout, 1)
subset_view_layout.addLayout(subset_view_btns_layout, 0)
subset_views_widget.set_center_widget(subset_view_widget)
# Whole subset layout with attributes and details
subset_content_widget = QtWidgets.QWidget(subset_frame)
subset_content_layout = QtWidgets.QHBoxLayout(subset_content_widget)
subset_content_layout.setContentsMargins(0, 0, 0, 0)
subset_content_layout.addWidget(subset_views_widget, 3)
subset_content_layout.addWidget(subset_attributes_wrap, 7)
# Widget where is stacked publish overlay and widgets that should be
# covered by it
under_publish_stack = QtWidgets.QWidget(self)
# Added wrap widget where all widgets under overlay are added
# - this is because footer is also under overlay and the top part
# is faked with floating frame
under_publish_widget = QtWidgets.QWidget(under_publish_stack)
# Footer
comment_input = PlaceholderLineEdit(subset_frame)
footer_widget = QtWidgets.QWidget(under_publish_widget)
footer_bottom_widget = QtWidgets.QWidget(footer_widget)
comment_input = PlaceholderLineEdit(footer_widget)
comment_input.setObjectName("PublishCommentInput")
comment_input.setPlaceholderText(
"Attach a comment to your publish"
)
reset_btn = ResetBtn(subset_frame)
stop_btn = StopBtn(subset_frame)
validate_btn = ValidateBtn(subset_frame)
publish_btn = PublishBtn(subset_frame)
reset_btn = ResetBtn(footer_widget)
stop_btn = StopBtn(footer_widget)
validate_btn = ValidateBtn(footer_widget)
publish_btn = PublishBtn(footer_widget)
footer_layout = QtWidgets.QHBoxLayout()
footer_layout.setContentsMargins(0, 0, 0, 0)
footer_layout.addWidget(comment_input, 1)
footer_layout.addWidget(reset_btn, 0)
footer_layout.addWidget(stop_btn, 0)
footer_layout.addWidget(validate_btn, 0)
footer_layout.addWidget(publish_btn, 0)
footer_bottom_layout = QtWidgets.QHBoxLayout(footer_bottom_widget)
footer_bottom_layout.setContentsMargins(0, 0, 0, 0)
footer_bottom_layout.addStretch(1)
footer_bottom_layout.addWidget(reset_btn, 0)
footer_bottom_layout.addWidget(stop_btn, 0)
footer_bottom_layout.addWidget(validate_btn, 0)
footer_bottom_layout.addWidget(publish_btn, 0)
# Subset frame layout
subset_layout = QtWidgets.QVBoxLayout(subset_frame)
marings = subset_layout.contentsMargins()
# Spacer helps keep distance of Publish Frame when comment input
# is hidden - so when is shrunken it is not overlaying pages
footer_spacer = QtWidgets.QWidget(footer_widget)
footer_spacer.setMinimumHeight(self.publish_footer_spacer)
footer_spacer.setMaximumHeight(self.publish_footer_spacer)
footer_spacer.setVisible(False)
footer_layout = QtWidgets.QVBoxLayout(footer_widget)
footer_margins = footer_layout.contentsMargins()
footer_layout.setContentsMargins(
footer_margins.left() + self.footer_border,
footer_margins.top(),
footer_margins.right() + self.footer_border,
footer_margins.bottom() + self.footer_border
)
footer_layout.addWidget(comment_input, 0)
footer_layout.addWidget(footer_spacer, 0)
footer_layout.addWidget(footer_bottom_widget, 0)
# Content
# - wrap stacked widget under one more widget to be able propagate
# margins (QStackedLayout can't have margins)
content_widget = QtWidgets.QWidget(under_publish_widget)
content_stacked_widget = QtWidgets.QWidget(content_widget)
content_layout = QtWidgets.QVBoxLayout(content_widget)
marings = content_layout.contentsMargins()
marings.setLeft(marings.left() * 2)
marings.setRight(marings.right() * 2)
marings.setTop(marings.top() * 2)
marings.setBottom(marings.bottom() * 2)
subset_layout.setContentsMargins(marings)
subset_layout.addWidget(subset_content_widget, 1)
subset_layout.addLayout(footer_layout, 0)
marings.setBottom(0)
content_layout.setContentsMargins(marings)
content_layout.addWidget(content_stacked_widget, 1)
# Create publish frame
publish_frame = PublishFrame(controller, content_stacked_widget)
# Overview - create and attributes part
overview_widget = OverviewWidget(
controller, content_stacked_widget
)
report_widget = ValidationsWidget(controller, parent)
# Details - Publish details
publish_details_widget = PublishReportViewerWidget(
content_stacked_widget
)
content_stacked_layout = QtWidgets.QStackedLayout(
content_stacked_widget
@ -182,282 +184,348 @@ class PublisherWindow(QtWidgets.QDialog):
content_stacked_layout.setStackingMode(
QtWidgets.QStackedLayout.StackAll
)
content_stacked_layout.addWidget(subset_frame)
content_stacked_layout.addWidget(publish_frame)
content_stacked_layout.addWidget(overview_widget)
content_stacked_layout.addWidget(report_widget)
content_stacked_layout.addWidget(publish_details_widget)
content_stacked_layout.setCurrentWidget(overview_widget)
under_publish_layout = QtWidgets.QVBoxLayout(under_publish_widget)
under_publish_layout.setContentsMargins(0, 0, 0, 0)
under_publish_layout.setSpacing(0)
under_publish_layout.addWidget(content_widget, 1)
under_publish_layout.addWidget(footer_widget, 0)
# Overlay which covers inputs during publishing
publish_overlay = QtWidgets.QFrame(under_publish_stack)
publish_overlay.setObjectName("OverlayFrame")
under_publish_stack_layout = QtWidgets.QStackedLayout(
under_publish_stack
)
under_publish_stack_layout.setContentsMargins(0, 0, 0, 0)
under_publish_stack_layout.setStackingMode(
QtWidgets.QStackedLayout.StackAll
)
under_publish_stack_layout.addWidget(under_publish_widget)
under_publish_stack_layout.addWidget(publish_overlay)
under_publish_stack_layout.setCurrentWidget(under_publish_widget)
# Add main frame to this window
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
main_layout.addWidget(header_widget, 0)
main_layout.addWidget(line_widget, 0)
main_layout.addWidget(content_stacked_widget, 1)
main_layout.addWidget(tabs_widget, 0)
main_layout.addWidget(under_publish_stack, 1)
creator_window = CreateDialog(controller, parent=self)
# Floating publish frame
publish_frame = PublishFrame(controller, self.footer_border, self)
create_btn.clicked.connect(self._on_create_clicked)
delete_btn.clicked.connect(self._on_delete_clicked)
change_view_btn.clicked.connect(self._on_change_view_clicked)
help_btn.clicked.connect(self._on_help_click)
tabs_widget.tab_changed.connect(self._on_tab_change)
overview_widget.active_changed.connect(
self._on_context_or_active_change
)
overview_widget.instance_context_changed.connect(
self._on_context_or_active_change
)
overview_widget.create_requested.connect(
self._on_create_request
)
reset_btn.clicked.connect(self._on_reset_clicked)
stop_btn.clicked.connect(self._on_stop_clicked)
validate_btn.clicked.connect(self._on_validate_clicked)
publish_btn.clicked.connect(self._on_publish_clicked)
# Selection changed
subset_list_view.selection_changed.connect(
self._on_subset_change
publish_frame.details_page_requested.connect(self._go_to_details_tab)
controller.event_system.add_callback(
"instances.refresh.finished", self._on_instances_refresh
)
subset_view_cards.selection_changed.connect(
self._on_subset_change
controller.event_system.add_callback(
"publish.reset.finished", self._on_publish_reset
)
# Active instances changed
subset_list_view.active_changed.connect(
self._on_active_changed
controller.event_system.add_callback(
"publish.process.started", self._on_publish_start
)
subset_view_cards.active_changed.connect(
self._on_active_changed
controller.event_system.add_callback(
"publish.process.validated", self._on_publish_validated
)
# Instance context has changed
subset_attributes_widget.instance_context_changed.connect(
self._on_instance_context_change
controller.event_system.add_callback(
"publish.process.stopped", self._on_publish_stop
)
controller.event_system.add_callback(
"show.card.message", self._on_overlay_message
)
controller.add_instances_refresh_callback(self._on_instances_refresh)
# Store extra header widget for TrayPublisher
# - can be used to add additional widgets to header between context
# label and help button
self._help_dialog = help_dialog
self._help_btn = help_btn
controller.add_publish_reset_callback(self._on_publish_reset)
controller.add_publish_started_callback(self._on_publish_start)
controller.add_publish_validated_callback(self._on_publish_validated)
controller.add_publish_stopped_callback(self._on_publish_stop)
self._header_extra_widget = header_extra_widget
# Store header for TrayPublisher
self._header_layout = header_layout
self._tabs_widget = tabs_widget
self._create_tab = create_tab
self._content_stacked_widget = content_stacked_widget
self.content_stacked_layout = content_stacked_layout
self.publish_frame = publish_frame
self.subset_frame = subset_frame
self.subset_content_widget = subset_content_widget
self._under_publish_stack_layout = under_publish_stack_layout
self.context_label = context_label
self._under_publish_widget = under_publish_widget
self._publish_overlay = publish_overlay
self._publish_frame = publish_frame
self.subset_view_cards = subset_view_cards
self.subset_list_view = subset_list_view
self.subset_views_layout = subset_views_layout
self._content_stacked_layout = content_stacked_layout
self.delete_btn = delete_btn
self._overview_widget = overview_widget
self._report_widget = report_widget
self._publish_details_widget = publish_details_widget
self.subset_attributes_widget = subset_attributes_widget
self._context_label = context_label
self.comment_input = comment_input
self._comment_input = comment_input
self._footer_spacer = footer_spacer
self.stop_btn = stop_btn
self.reset_btn = reset_btn
self.validate_btn = validate_btn
self.publish_btn = publish_btn
self._stop_btn = stop_btn
self._reset_btn = reset_btn
self._validate_btn = validate_btn
self._publish_btn = publish_btn
self.controller = controller
self._overlay_object = overlay_object
self.creator_window = creator_window
self._controller = controller
self._first_show = True
self._reset_on_show = reset_on_show
self._restart_timer = None
self._publish_frame_visible = None
self._set_publish_visibility(False)
@property
def controller(self):
return self._controller
def showEvent(self, event):
super(PublisherWindow, self).showEvent(event)
if self._first_show:
self._first_show = False
self.resize(self.default_width, self.default_height)
self.setStyleSheet(style.load_stylesheet())
if self._reset_on_show:
self.reset()
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
# Detach showing - give OS chance to draw the window
timer = QtCore.QTimer()
timer.setSingleShot(True)
timer.setInterval(1)
timer.timeout.connect(self._on_show_restart_timer)
self._restart_timer = timer
timer.start()
def _on_show_restart_timer(self):
"""Callback for '_restart_timer' timer."""
self._restart_timer = None
self.reset()
def closeEvent(self, event):
self.controller.save_changes()
self._controller.save_changes()
super(PublisherWindow, self).closeEvent(event)
def reset(self):
self.controller.reset()
self._controller.reset()
def set_context_label(self, label):
self.context_label.setText(label)
self._context_label.setText(label)
def get_selected_items(self):
view = self.subset_views_layout.currentWidget()
return view.get_selected_items()
def _on_instance_context_change(self):
current_idx = self.subset_views_layout.currentIndex()
for idx in range(self.subset_views_layout.count()):
if idx == current_idx:
continue
widget = self.subset_views_layout.widget(idx)
if widget.refreshed:
widget.set_refreshed(False)
current_widget = self.subset_views_layout.widget(current_idx)
current_widget.refresh_instance_states()
self._validate_create_instances()
def _change_view_type(self):
idx = self.subset_views_layout.currentIndex()
new_idx = (idx + 1) % self.subset_views_layout.count()
self.subset_views_layout.setCurrentIndex(new_idx)
new_view = self.subset_views_layout.currentWidget()
if not new_view.refreshed:
new_view.refresh()
new_view.set_refreshed(True)
else:
new_view.refresh_instance_states()
self._on_subset_change()
def _on_create_clicked(self):
self.creator_window.show()
def _on_delete_clicked(self):
instances, _ = self.get_selected_items()
# Ask user if he really wants to remove instances
dialog = QtWidgets.QMessageBox(self)
dialog.setIcon(QtWidgets.QMessageBox.Question)
dialog.setWindowTitle("Are you sure?")
if len(instances) > 1:
msg = (
"Do you really want to remove {} instances?"
).format(len(instances))
else:
msg = (
"Do you really want to remove the instance?"
)
dialog.setText(msg)
dialog.setStandardButtons(
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel
)
dialog.setDefaultButton(QtWidgets.QMessageBox.Ok)
dialog.setEscapeButton(QtWidgets.QMessageBox.Cancel)
dialog.exec_()
# Skip if OK was not clicked
if dialog.result() == QtWidgets.QMessageBox.Ok:
self.controller.remove_instances(instances)
def _on_change_view_clicked(self):
self._change_view_type()
def _set_publish_visibility(self, visible):
if visible:
widget = self.publish_frame
publish_frame_visible = True
else:
widget = self.subset_frame
publish_frame_visible = False
self.content_stacked_layout.setCurrentWidget(widget)
self._set_publish_frame_visible(publish_frame_visible)
def _set_publish_frame_visible(self, publish_frame_visible):
"""Publish frame visibility has changed.
Also used in TrayPublisher to be able handle start/end of publish
widget overlay.
"""
# Hide creator dialog if visible
if publish_frame_visible and self.creator_window.isVisible():
self.creator_window.close()
def _on_reset_clicked(self):
self.controller.reset()
def _on_stop_clicked(self):
self.controller.stop_publish()
def _set_publish_comment(self):
if self.controller.publish_comment_is_set:
def _update_publish_details_widget(self, force=False):
if not force and self._tabs_widget.current_tab() != "details":
return
comment = self.comment_input.text()
self.controller.set_comment(comment)
report_data = self.controller.get_publish_report()
self._publish_details_widget.set_report_data(report_data)
def _on_help_click(self):
if self._help_dialog.isVisible():
return
self._help_dialog.show()
window = self.window()
desktop = QtWidgets.QApplication.desktop()
screen_idx = desktop.screenNumber(window)
screen = desktop.screen(screen_idx)
screen_rect = screen.geometry()
window_geo = window.geometry()
dialog_x = window_geo.x() + window_geo.width()
dialog_right = (dialog_x + self._help_dialog.width()) - 1
diff = dialog_right - screen_rect.right()
if diff > 0:
dialog_x -= diff
self._help_dialog.setGeometry(
dialog_x, window_geo.y(),
self._help_dialog.width(), self._help_dialog.height()
)
def _on_tab_change(self, old_tab, new_tab):
if old_tab == "details":
self._publish_details_widget.close_details_popup()
if new_tab in ("create", "publish"):
animate = True
if old_tab not in ("create", "publish"):
animate = False
self._content_stacked_layout.setCurrentWidget(
self._overview_widget
)
self._overview_widget.set_state(new_tab, animate)
elif new_tab == "details":
self._content_stacked_layout.setCurrentWidget(
self._publish_details_widget
)
self._update_publish_details_widget()
elif new_tab == "report":
self._content_stacked_layout.setCurrentWidget(
self._report_widget
)
def _on_context_or_active_change(self):
self._validate_create_instances()
def _on_create_request(self):
self._go_to_create_tab()
def _go_to_create_tab(self):
self._tabs_widget.set_current_tab("create")
def _go_to_details_tab(self):
self._tabs_widget.set_current_tab("details")
def _go_to_report_tab(self):
self._tabs_widget.set_current_tab("report")
def _set_publish_overlay_visibility(self, visible):
if visible:
widget = self._publish_overlay
else:
widget = self._under_publish_widget
self._under_publish_stack_layout.setCurrentWidget(widget)
def _set_publish_visibility(self, visible):
if visible is self._publish_frame_visible:
return
self._publish_frame_visible = visible
self._publish_frame.setVisible(visible)
self._update_publish_frame_rect()
def _on_reset_clicked(self):
self._controller.reset()
def _on_stop_clicked(self):
self._controller.stop_publish()
def _set_publish_comment(self):
if self._controller.publish_comment_is_set:
return
comment = self._comment_input.text()
self._controller.set_comment(comment)
def _on_validate_clicked(self):
self._set_publish_comment()
self._set_publish_visibility(True)
self.controller.validate()
self._controller.validate()
def _on_publish_clicked(self):
self._set_publish_comment()
self._set_publish_visibility(True)
self.controller.publish()
def _refresh_instances(self):
if self._refreshing_instances:
return
self._refreshing_instances = True
for idx in range(self.subset_views_layout.count()):
widget = self.subset_views_layout.widget(idx)
widget.set_refreshed(False)
view = self.subset_views_layout.currentWidget()
view.refresh()
view.set_refreshed(True)
self._refreshing_instances = False
# Force to change instance and refresh details
self._on_subset_change()
def _on_instances_refresh(self):
self._refresh_instances()
self._validate_create_instances()
context_title = self.controller.get_context_title()
self.set_context_label(context_title)
# Give a change to process Resize Request
QtWidgets.QApplication.processEvents()
# Trigger update geometry of
widget = self.subset_views_layout.currentWidget()
widget.updateGeometry()
def _on_subset_change(self, *_args):
# Ignore changes if in middle of refreshing
if self._refreshing_instances:
return
instances, context_selected = self.get_selected_items()
# Disable delete button if nothing is selected
self.delete_btn.setEnabled(len(instances) > 0)
self.subset_attributes_widget.set_current_instances(
instances, context_selected
)
def _on_active_changed(self):
if self._refreshing_instances:
return
self._validate_create_instances()
self._controller.publish()
def _set_footer_enabled(self, enabled):
self.comment_input.setEnabled(enabled)
self.reset_btn.setEnabled(True)
self._reset_btn.setEnabled(True)
if enabled:
self.stop_btn.setEnabled(False)
self.validate_btn.setEnabled(True)
self.publish_btn.setEnabled(True)
self._stop_btn.setEnabled(False)
self._validate_btn.setEnabled(True)
self._publish_btn.setEnabled(True)
else:
self.stop_btn.setEnabled(enabled)
self.validate_btn.setEnabled(enabled)
self.publish_btn.setEnabled(enabled)
self._stop_btn.setEnabled(enabled)
self._validate_btn.setEnabled(enabled)
self._publish_btn.setEnabled(enabled)
def _on_publish_reset(self):
self._create_tab.setEnabled(True)
self._set_comment_input_visiblity(True)
self._set_publish_overlay_visibility(False)
self._set_publish_visibility(False)
self._set_footer_enabled(False)
self._update_publish_details_widget()
def _on_publish_start(self):
self._create_tab.setEnabled(False)
self._reset_btn.setEnabled(False)
self._stop_btn.setEnabled(True)
self._validate_btn.setEnabled(False)
self._publish_btn.setEnabled(False)
self._set_comment_input_visiblity(False)
self._set_publish_visibility(True)
self._set_publish_overlay_visibility(True)
self._publish_details_widget.close_details_popup()
if self._tabs_widget.is_current_tab(self._create_tab):
self._tabs_widget.set_current_tab("publish")
def _on_publish_validated(self):
self._validate_btn.setEnabled(False)
def _on_publish_stop(self):
self._set_publish_overlay_visibility(False)
self._reset_btn.setEnabled(True)
self._stop_btn.setEnabled(False)
validate_enabled = not self._controller.publish_has_crashed
publish_enabled = not self._controller.publish_has_crashed
if validate_enabled:
validate_enabled = not self._controller.publish_has_validated
if publish_enabled:
if (
self._controller.publish_has_validated
and self._controller.publish_has_validation_errors
):
publish_enabled = False
if self._tabs_widget.is_current_tab("publish"):
self._go_to_report_tab()
else:
publish_enabled = not self._controller.publish_has_finished
self._validate_btn.setEnabled(validate_enabled)
self._publish_btn.setEnabled(publish_enabled)
self._update_publish_details_widget()
def _validate_create_instances(self):
if not self.controller.host_is_valid:
if not self._controller.host_is_valid:
self._set_footer_enabled(True)
return
all_valid = None
for instance in self.controller.instances:
for instance in self._controller.instances:
if not instance["active"]:
continue
@ -470,38 +538,29 @@ class PublisherWindow(QtWidgets.QDialog):
self._set_footer_enabled(bool(all_valid))
def _on_publish_reset(self):
self._set_publish_visibility(False)
def _on_instances_refresh(self):
self._validate_create_instances()
self.subset_content_widget.setEnabled(self.controller.host_is_valid)
context_title = self.controller.get_context_title()
self.set_context_label(context_title)
self._update_publish_details_widget()
self._set_footer_enabled(False)
def _set_comment_input_visiblity(self, visible):
self._comment_input.setVisible(visible)
self._footer_spacer.setVisible(not visible)
def _on_publish_start(self):
self.reset_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
self.validate_btn.setEnabled(False)
self.publish_btn.setEnabled(False)
def _update_publish_frame_rect(self):
if not self._publish_frame_visible:
return
def _on_publish_validated(self):
self.validate_btn.setEnabled(False)
window_size = self.size()
size_hint = self._publish_frame.minimumSizeHint()
def _on_publish_stop(self):
self.reset_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
validate_enabled = not self.controller.publish_has_crashed
publish_enabled = not self.controller.publish_has_crashed
if validate_enabled:
validate_enabled = not self.controller.publish_has_validated
if publish_enabled:
if (
self.controller.publish_has_validated
and self.controller.publish_has_validation_errors
):
publish_enabled = False
width = window_size.width()
height = size_hint.height()
else:
publish_enabled = not self.controller.publish_has_finished
self._publish_frame.resize(width, height)
self.validate_btn.setEnabled(validate_enabled)
self.publish_btn.setEnabled(publish_enabled)
self._publish_frame.move(
0, window_size.height() - height
)

View file

@ -13,7 +13,7 @@ from .widgets import (
)
from .widgets.constants import HOST_NAME
from openpype import style
from openpype.api import resources
from openpype import resources
from openpype.pipeline import AvalonMongoDB
from openpype.modules import ModulesManager

View file

@ -5,7 +5,7 @@ import collections
from Qt import QtCore, QtGui, QtWidgets
from openpype import style
from openpype.api import resources
from openpype import resources
from openpype.settings.lib import get_local_settings
from openpype.lib.pype_info import (
get_all_current_info,

View file

@ -6,25 +6,23 @@ Tray publisher can be considered as host implementeation with creators and
publishing plugins.
"""
import platform
from Qt import QtWidgets, QtCore
import qtawesome
import appdirs
from openpype.pipeline import (
install_host,
AvalonMongoDB,
)
from openpype.lib import JSONSettingRegistry
from openpype.pipeline import install_host
from openpype.hosts.traypublisher.api import TrayPublisherHost
from openpype.tools.publisher import PublisherWindow
from openpype.tools.publisher.window import PublisherWindow
from openpype.tools.utils import PlaceholderLineEdit
from openpype.tools.utils.constants import PROJECT_NAME_ROLE
from openpype.tools.utils.models import (
ProjectModel,
ProjectSortFilterProxy
)
from openpype.tools.utils import PlaceholderLineEdit
import appdirs
from openpype.lib import JSONSettingRegistry
class TrayPublisherRegistry(JSONSettingRegistry):
"""Class handling OpenPype general settings registry.
@ -55,14 +53,10 @@ class StandaloneOverlayWidget(QtWidgets.QFrame):
content_widget = QtWidgets.QWidget(middle_frame)
# Create db connection for projects model
dbcon = AvalonMongoDB()
dbcon.install()
header_label = QtWidgets.QLabel("Choose project", content_widget)
header_label.setObjectName("ChooseProjectLabel")
# Create project models and view
projects_model = ProjectModel(dbcon)
projects_model = ProjectModel()
projects_proxy = ProjectSortFilterProxy()
projects_proxy.setSourceModel(projects_model)
projects_proxy.setFilterKeyColumn(0)
@ -137,11 +131,14 @@ class StandaloneOverlayWidget(QtWidgets.QFrame):
src_index = self._projects_model.find_project(project_name)
if src_index is not None:
index = self._projects_proxy.mapFromSource(src_index)
if index:
mode = (
QtCore.QItemSelectionModel.Select
| QtCore.QItemSelectionModel.Rows)
self._projects_view.selectionModel().select(index, mode)
if index is not None:
selection_model = self._projects_view.selectionModel()
selection_model.select(
index,
QtCore.QItemSelectionModel.SelectCurrent
)
self._projects_view.setCurrentIndex(index)
self._cancel_btn.setVisible(self._project_name is not None)
super(StandaloneOverlayWidget, self).showEvent(event)
@ -193,7 +190,7 @@ class TrayPublishWindow(PublisherWindow):
overlay_widget = StandaloneOverlayWidget(self)
btns_widget = QtWidgets.QWidget(self)
btns_widget = self._header_extra_widget
back_to_overlay_btn = QtWidgets.QPushButton(
"Change project", btns_widget
@ -208,8 +205,6 @@ class TrayPublishWindow(PublisherWindow):
btns_layout.addWidget(save_btn, 0)
btns_layout.addWidget(back_to_overlay_btn, 0)
self._header_layout.addWidget(btns_widget, 0)
overlay_widget.project_selected.connect(self._on_project_select)
back_to_overlay_btn.clicked.connect(self._on_back_to_overlay)
save_btn.clicked.connect(self._on_tray_publish_save)
@ -239,22 +234,32 @@ class TrayPublishWindow(PublisherWindow):
def _on_project_select(self, project_name):
# TODO register project specific plugin paths
self.controller.save_changes()
self.controller.reset_project_data_cache()
self._controller.save_changes()
self._controller.reset_project_data_cache()
self.reset()
if not self.controller.instances:
self._on_create_clicked()
if not self._controller.instances:
self._go_to_create_tab()
def _on_tray_publish_save(self):
self.controller.save_changes()
self._controller.save_changes()
print("NOT YET IMPLEMENTED")
def main():
host = TrayPublisherHost()
install_host(host)
app = QtWidgets.QApplication([])
app_instance = QtWidgets.QApplication.instance()
if app_instance is None:
app_instance = QtWidgets.QApplication([])
if platform.system().lower() == "windows":
import ctypes
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
u"traypublisher"
)
window = TrayPublishWindow()
window.show()
app.exec_()
app_instance.exec_()

View file

@ -122,6 +122,16 @@ class OverlayMessageWidget(QtWidgets.QFrame):
self._timeout_timer = timeout_timer
self._hover_timer = hover_timer
def update_message(self, message, message_type=None, timeout=None):
self._label_widget.setText(message)
if timeout:
self._timeout_timer.setInterval(timeout)
if message_type:
set_style_property(self, "type", message_type)
self._timeout_timer.start()
def size_hint_without_word_wrap(self):
"""Size hint in cases that word wrap of label is disabled."""
self._label_widget.setWordWrap(False)
@ -195,7 +205,9 @@ class MessageOverlayObject(QtCore.QObject):
self._move_size_remove = 8
self._default_timeout = default_timeout
def add_message(self, message, message_type=None, timeout=None):
def add_message(
self, message, message_type=None, timeout=None, message_id=None
):
"""Add single message into overlay.
Args:
@ -203,6 +215,12 @@ class MessageOverlayObject(QtCore.QObject):
timeout (int): Message timeout.
message_type (str): Message type can be used as property in
stylesheets.
message_id (str): UUID of already existing message to update
it's message and timeout. Is created with different id if is
not available anymore.
Returns:
str: UUID of message which can be used to update message.
"""
# Skip empty messages
if not message:
@ -212,31 +230,48 @@ class MessageOverlayObject(QtCore.QObject):
timeout = self._default_timeout
# Create unique id of message
label_id = str(uuid.uuid4())
# Create message widget
widget = OverlayMessageWidget(
label_id, message, self._widget, message_type, timeout
)
widget.close_requested.connect(self._on_message_close_request)
widget.show()
widget = None
if message_id is not None:
widget = self._messages.get(message_id)
if message_id is None:
message_id = str(uuid.uuid4())
elif message_id in self._messages_order:
self._messages_order.remove(message_id)
if widget is not None:
# NOTE: Update of message won't change paint order which should be
# ok in most of cases as it matters only when messages are
# animated
widget.update_message(message, message_type, timeout)
else:
# Create message widget
widget = OverlayMessageWidget(
message_id, message, self._widget, message_type, timeout
)
widget.close_requested.connect(self._on_message_close_request)
widget.show()
# Move widget outside of window
pos = widget.pos()
pos.setY(pos.y() - widget.height())
widget.move(pos)
# Store message
self._messages[label_id] = widget
self._messages_order.append(label_id)
self._messages[message_id] = widget
self._messages_order.append(message_id)
# Trigger recalculation timer
self._recalculate_timer.start()
def _on_message_close_request(self, label_id):
return message_id
def _on_message_close_request(self, message_id):
"""Message widget requested removement."""
widget = self._messages.get(label_id)
widget = self._messages.get(message_id)
if widget is not None:
# Add message to closing messages and start recalculation
self._closing_messages.add(label_id)
self._closing_messages.add(message_id)
self._recalculate_timer.start()
def _recalculate_positions(self):