Merge branch 'develop' into feature/OP-2361_Store-settings-by-OpenPype-version

This commit is contained in:
Jakub Trllo 2022-02-01 11:04:21 +01:00
commit 773d3aa176
113 changed files with 1697 additions and 358 deletions

View file

@ -1,21 +1,26 @@
# Changelog # Changelog
## [3.8.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) ## [3.8.1-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.0...HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.0...HEAD)
**🚀 Enhancements** **🚀 Enhancements**
- Webpublisher: Thumbnail extractor [\#2600](https://github.com/pypeclub/OpenPype/pull/2600)
- Loader: Allow to toggle default family filters between "include" or "exclude" filtering [\#2541](https://github.com/pypeclub/OpenPype/pull/2541) - Loader: Allow to toggle default family filters between "include" or "exclude" filtering [\#2541](https://github.com/pypeclub/OpenPype/pull/2541)
**🐛 Bug fixes** **🐛 Bug fixes**
- switch distutils to sysconfig for `get\_platform\(\)` [\#2594](https://github.com/pypeclub/OpenPype/pull/2594)
- Fix poetry index and speedcopy update [\#2589](https://github.com/pypeclub/OpenPype/pull/2589)
- Webpublisher: Fix - subset names from processed .psd used wrong value for task [\#2586](https://github.com/pypeclub/OpenPype/pull/2586) - Webpublisher: Fix - subset names from processed .psd used wrong value for task [\#2586](https://github.com/pypeclub/OpenPype/pull/2586)
- `vrscene` creator Deadline webservice URL handling [\#2580](https://github.com/pypeclub/OpenPype/pull/2580) - `vrscene` creator Deadline webservice URL handling [\#2580](https://github.com/pypeclub/OpenPype/pull/2580)
- global: track name was failing if duplicated root word in name [\#2568](https://github.com/pypeclub/OpenPype/pull/2568) - global: track name was failing if duplicated root word in name [\#2568](https://github.com/pypeclub/OpenPype/pull/2568)
- Validate Maya Rig produces no cycle errors [\#2484](https://github.com/pypeclub/OpenPype/pull/2484)
**Merged pull requests:** **Merged pull requests:**
- Bump pillow from 8.4.0 to 9.0.0 [\#2595](https://github.com/pypeclub/OpenPype/pull/2595)
- build\(deps\): bump pillow from 8.4.0 to 9.0.0 [\#2523](https://github.com/pypeclub/OpenPype/pull/2523) - build\(deps\): bump pillow from 8.4.0 to 9.0.0 [\#2523](https://github.com/pypeclub/OpenPype/pull/2523)
## [3.8.0](https://github.com/pypeclub/OpenPype/tree/3.8.0) (2022-01-24) ## [3.8.0](https://github.com/pypeclub/OpenPype/tree/3.8.0) (2022-01-24)
@ -49,7 +54,6 @@
- Slack: notifications are sent with Openpype logo and bot name [\#2499](https://github.com/pypeclub/OpenPype/pull/2499) - Slack: notifications are sent with Openpype logo and bot name [\#2499](https://github.com/pypeclub/OpenPype/pull/2499)
- Slack: Add review to notification message [\#2498](https://github.com/pypeclub/OpenPype/pull/2498) - Slack: Add review to notification message [\#2498](https://github.com/pypeclub/OpenPype/pull/2498)
- Maya: Collect 'fps' animation data only for "review" instances [\#2486](https://github.com/pypeclub/OpenPype/pull/2486) - Maya: Collect 'fps' animation data only for "review" instances [\#2486](https://github.com/pypeclub/OpenPype/pull/2486)
- General: Validate third party before build [\#2425](https://github.com/pypeclub/OpenPype/pull/2425)
**🐛 Bug fixes** **🐛 Bug fixes**
@ -87,15 +91,6 @@
**🚀 Enhancements** **🚀 Enhancements**
- General: Workdir extra folders [\#2462](https://github.com/pypeclub/OpenPype/pull/2462) - General: Workdir extra folders [\#2462](https://github.com/pypeclub/OpenPype/pull/2462)
- Photoshop: New style validations for New publisher [\#2429](https://github.com/pypeclub/OpenPype/pull/2429)
**🐛 Bug fixes**
- Short Pyblish plugin path [\#2428](https://github.com/pypeclub/OpenPype/pull/2428)
**Merged pull requests:**
- Forced cx\_freeze to include sqlite3 into build [\#2432](https://github.com/pypeclub/OpenPype/pull/2432)
## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23) ## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23)

View file

@ -6,6 +6,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
"""Add last workfile path to launch arguments. """Add last workfile path to launch arguments.
This is not possible to do for all applications the same way. This is not possible to do for all applications the same way.
Checks 'start_last_workfile', if set to False, it will not open last
workfile. This property is set explicitly in Launcher.
""" """
# Execute after workfile template copy # Execute after workfile template copy

View file

@ -43,6 +43,7 @@ class GlobalHostDataHook(PreLaunchHook):
"env": self.launch_context.env, "env": self.launch_context.env,
"start_last_workfile": self.data.get("start_last_workfile"),
"last_workfile_path": self.data.get("last_workfile_path"), "last_workfile_path": self.data.get("last_workfile_path"),
"log": self.log "log": self.log

View file

@ -40,7 +40,10 @@ class NonPythonHostHook(PreLaunchHook):
) )
# Add workfile path if exists # Add workfile path if exists
workfile_path = self.data["last_workfile_path"] workfile_path = self.data["last_workfile_path"]
if os.path.exists(workfile_path): if (
self.data.get("start_last_workfile")
and workfile_path
and os.path.exists(workfile_path)):
new_launch_args.append(workfile_path) new_launch_args.append(workfile_path)
# Append as whole list as these areguments should not be separated # Append as whole list as these areguments should not be separated

View file

@ -49,10 +49,13 @@ def get_unique_number(
return f"{count:0>2}" return f"{count:0>2}"
def prepare_data(data, container_name): def prepare_data(data, container_name=None):
name = data.name name = data.name
local_data = data.make_local() local_data = data.make_local()
local_data.name = f"{container_name}:{name}" if container_name:
local_data.name = f"{container_name}:{name}"
else:
local_data.name = f"{name}"
return local_data return local_data

View file

@ -7,6 +7,7 @@ from typing import Dict, List, Optional
import bpy import bpy
from avalon import api from avalon import api
from openpype import lib
from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api import plugin
from openpype.hosts.blender.api.pipeline import ( from openpype.hosts.blender.api.pipeline import (
AVALON_CONTAINERS, AVALON_CONTAINERS,
@ -61,7 +62,9 @@ class BlendLayoutLoader(plugin.AssetLoader):
library = bpy.data.libraries.get(bpy.path.basename(libpath)) library = bpy.data.libraries.get(bpy.path.basename(libpath))
bpy.data.libraries.remove(library) bpy.data.libraries.remove(library)
def _process(self, libpath, asset_group, group_name, actions): def _process(
self, libpath, asset_group, group_name, asset, representation, actions
):
with bpy.data.libraries.load( with bpy.data.libraries.load(
libpath, link=True, relative=False libpath, link=True, relative=False
) as (data_from, data_to): ) as (data_from, data_to):
@ -74,7 +77,8 @@ class BlendLayoutLoader(plugin.AssetLoader):
container = None container = None
for empty in empties: for empty in empties:
if empty.get(AVALON_PROPERTY): if (empty.get(AVALON_PROPERTY) and
empty.get(AVALON_PROPERTY).get('family') == 'layout'):
container = empty container = empty
break break
@ -85,12 +89,16 @@ class BlendLayoutLoader(plugin.AssetLoader):
objects = [] objects = []
nodes = list(container.children) nodes = list(container.children)
for obj in nodes: allowed_types = ['ARMATURE', 'MESH', 'EMPTY']
obj.parent = asset_group
for obj in nodes: for obj in nodes:
objects.append(obj) if obj.type in allowed_types:
nodes.extend(list(obj.children)) obj.parent = asset_group
for obj in nodes:
if obj.type in allowed_types:
objects.append(obj)
nodes.extend(list(obj.children))
objects.reverse() objects.reverse()
@ -108,7 +116,7 @@ class BlendLayoutLoader(plugin.AssetLoader):
parent.objects.link(obj) parent.objects.link(obj)
for obj in objects: for obj in objects:
local_obj = plugin.prepare_data(obj, group_name) local_obj = plugin.prepare_data(obj)
action = None action = None
@ -116,7 +124,7 @@ class BlendLayoutLoader(plugin.AssetLoader):
action = actions.get(local_obj.name, None) action = actions.get(local_obj.name, None)
if local_obj.type == 'MESH': if local_obj.type == 'MESH':
plugin.prepare_data(local_obj.data, group_name) plugin.prepare_data(local_obj.data)
if obj != local_obj: if obj != local_obj:
for constraint in constraints: for constraint in constraints:
@ -125,15 +133,18 @@ class BlendLayoutLoader(plugin.AssetLoader):
for material_slot in local_obj.material_slots: for material_slot in local_obj.material_slots:
if material_slot.material: if material_slot.material:
plugin.prepare_data(material_slot.material, group_name) plugin.prepare_data(material_slot.material)
elif local_obj.type == 'ARMATURE': elif local_obj.type == 'ARMATURE':
plugin.prepare_data(local_obj.data, group_name) plugin.prepare_data(local_obj.data)
if action is not None: if action is not None:
if local_obj.animation_data is None:
local_obj.animation_data_create()
local_obj.animation_data.action = action local_obj.animation_data.action = action
elif local_obj.animation_data.action is not None: elif (local_obj.animation_data and
local_obj.animation_data.action is not None):
plugin.prepare_data( plugin.prepare_data(
local_obj.animation_data.action, group_name) local_obj.animation_data.action)
# Set link the drivers to the local object # Set link the drivers to the local object
if local_obj.data.animation_data: if local_obj.data.animation_data:
@ -142,6 +153,21 @@ class BlendLayoutLoader(plugin.AssetLoader):
for t in v.targets: for t in v.targets:
t.id = local_obj t.id = local_obj
elif local_obj.type == 'EMPTY':
creator_plugin = lib.get_creator_by_name("CreateAnimation")
if not creator_plugin:
raise ValueError("Creator plugin \"CreateAnimation\" was "
"not found.")
api.create(
creator_plugin,
name=local_obj.name.split(':')[-1] + "_animation",
asset=asset,
options={"useSelection": False,
"asset_group": local_obj},
data={"dependencies": representation}
)
if not local_obj.get(AVALON_PROPERTY): if not local_obj.get(AVALON_PROPERTY):
local_obj[AVALON_PROPERTY] = dict() local_obj[AVALON_PROPERTY] = dict()
@ -150,7 +176,63 @@ class BlendLayoutLoader(plugin.AssetLoader):
objects.reverse() objects.reverse()
bpy.data.orphans_purge(do_local_ids=False) armatures = [
obj for obj in bpy.data.objects
if obj.type == 'ARMATURE' and obj.library is None]
arm_act = {}
# The armatures with an animation need to be at the center of the
# scene to be hooked correctly by the curves modifiers.
for armature in armatures:
if armature.animation_data and armature.animation_data.action:
arm_act[armature] = armature.animation_data.action
armature.animation_data.action = None
armature.location = (0.0, 0.0, 0.0)
for bone in armature.pose.bones:
bone.location = (0.0, 0.0, 0.0)
bone.rotation_euler = (0.0, 0.0, 0.0)
curves = [obj for obj in data_to.objects if obj.type == 'CURVE']
for curve in curves:
curve_name = curve.name.split(':')[0]
curve_obj = bpy.data.objects.get(curve_name)
local_obj = plugin.prepare_data(curve)
plugin.prepare_data(local_obj.data)
# Curves need to reset the hook, but to do that they need to be
# in the view layer.
parent.objects.link(local_obj)
plugin.deselect_all()
local_obj.select_set(True)
bpy.context.view_layer.objects.active = local_obj
if local_obj.library is None:
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.hook_reset()
bpy.ops.object.mode_set(mode='OBJECT')
parent.objects.unlink(local_obj)
local_obj.use_fake_user = True
for mod in local_obj.modifiers:
mod.object = bpy.data.objects.get(f"{mod.object.name}")
if not local_obj.get(AVALON_PROPERTY):
local_obj[AVALON_PROPERTY] = dict()
avalon_info = local_obj[AVALON_PROPERTY]
avalon_info.update({"container_name": group_name})
local_obj.parent = curve_obj
objects.append(local_obj)
for armature in armatures:
if arm_act.get(armature):
armature.animation_data.action = arm_act[armature]
while bpy.data.orphans_purge(do_local_ids=False):
pass
plugin.deselect_all() plugin.deselect_all()
@ -170,6 +252,7 @@ class BlendLayoutLoader(plugin.AssetLoader):
libpath = self.fname libpath = self.fname
asset = context["asset"]["name"] asset = context["asset"]["name"]
subset = context["subset"]["name"] subset = context["subset"]["name"]
representation = str(context["representation"]["_id"])
asset_name = plugin.asset_name(asset, subset) asset_name = plugin.asset_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset) unique_number = plugin.get_unique_number(asset, subset)
@ -185,7 +268,8 @@ class BlendLayoutLoader(plugin.AssetLoader):
asset_group.empty_display_type = 'SINGLE_ARROW' asset_group.empty_display_type = 'SINGLE_ARROW'
avalon_container.objects.link(asset_group) avalon_container.objects.link(asset_group)
objects = self._process(libpath, asset_group, group_name, None) objects = self._process(
libpath, asset_group, group_name, asset, representation, None)
for child in asset_group.children: for child in asset_group.children:
if child.get(AVALON_PROPERTY): if child.get(AVALON_PROPERTY):

View file

@ -94,6 +94,10 @@ class JsonLayoutLoader(plugin.AssetLoader):
'animation_asset': asset 'animation_asset': asset
} }
if element.get('animation'):
options['animation_file'] = str(Path(libpath).with_suffix(
'')) + "." + element.get('animation')
# This should return the loaded asset, but the load call will be # This should return the loaded asset, but the load call will be
# added to the queue to run in the Blender main thread, so # added to the queue to run in the Blender main thread, so
# at this time it will not return anything. The assets will be # at this time it will not return anything. The assets will be
@ -106,20 +110,22 @@ class JsonLayoutLoader(plugin.AssetLoader):
options=options options=options
) )
# Create the camera asset and the camera instance # Camera creation when loading a layout is not necessary for now,
creator_plugin = lib.get_creator_by_name("CreateCamera") # but the code is worth keeping in case we need it in the future.
if not creator_plugin: # # Create the camera asset and the camera instance
raise ValueError("Creator plugin \"CreateCamera\" was " # creator_plugin = lib.get_creator_by_name("CreateCamera")
"not found.") # if not creator_plugin:
# raise ValueError("Creator plugin \"CreateCamera\" was "
# "not found.")
api.create( # api.create(
creator_plugin, # creator_plugin,
name="camera", # name="camera",
# name=f"{unique_number}_{subset}_animation", # # name=f"{unique_number}_{subset}_animation",
asset=asset, # asset=asset,
options={"useSelection": False} # options={"useSelection": False}
# data={"dependencies": str(context["representation"]["_id"])} # # data={"dependencies": str(context["representation"]["_id"])}
) # )
def process_asset(self, def process_asset(self,
context: dict, context: dict,

View file

@ -83,7 +83,8 @@ class BlendModelLoader(plugin.AssetLoader):
plugin.prepare_data(local_obj.data, group_name) plugin.prepare_data(local_obj.data, group_name)
for material_slot in local_obj.material_slots: for material_slot in local_obj.material_slots:
plugin.prepare_data(material_slot.material, group_name) if material_slot.material:
plugin.prepare_data(material_slot.material, group_name)
if not local_obj.get(AVALON_PROPERTY): if not local_obj.get(AVALON_PROPERTY):
local_obj[AVALON_PROPERTY] = dict() local_obj[AVALON_PROPERTY] = dict()
@ -247,7 +248,8 @@ class BlendModelLoader(plugin.AssetLoader):
# If it is the last object to use that library, remove it # If it is the last object to use that library, remove it
if count == 1: if count == 1:
library = bpy.data.libraries.get(bpy.path.basename(group_libpath)) library = bpy.data.libraries.get(bpy.path.basename(group_libpath))
bpy.data.libraries.remove(library) if library:
bpy.data.libraries.remove(library)
self._process(str(libpath), asset_group, object_name) self._process(str(libpath), asset_group, object_name)
@ -255,6 +257,7 @@ class BlendModelLoader(plugin.AssetLoader):
metadata["libpath"] = str(libpath) metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"]) metadata["representation"] = str(representation["_id"])
metadata["parent"] = str(representation["parent"])
def exec_remove(self, container: Dict) -> bool: def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene. """Remove an existing container from a Blender scene.

View file

@ -7,6 +7,7 @@ from typing import Dict, List, Optional
import bpy import bpy
from avalon import api from avalon import api
from avalon.blender import lib as avalon_lib
from openpype import lib from openpype import lib
from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api import plugin
from openpype.hosts.blender.api.pipeline import ( from openpype.hosts.blender.api.pipeline import (
@ -112,6 +113,8 @@ class BlendRigLoader(plugin.AssetLoader):
plugin.prepare_data(local_obj.data, group_name) plugin.prepare_data(local_obj.data, group_name)
if action is not None: if action is not None:
if local_obj.animation_data is None:
local_obj.animation_data_create()
local_obj.animation_data.action = action local_obj.animation_data.action = action
elif (local_obj.animation_data and elif (local_obj.animation_data and
local_obj.animation_data.action is not None): local_obj.animation_data.action is not None):
@ -196,12 +199,14 @@ class BlendRigLoader(plugin.AssetLoader):
plugin.deselect_all() plugin.deselect_all()
create_animation = False create_animation = False
anim_file = None
if options is not None: if options is not None:
parent = options.get('parent') parent = options.get('parent')
transform = options.get('transform') transform = options.get('transform')
action = options.get('action') action = options.get('action')
create_animation = options.get('create_animation') create_animation = options.get('create_animation')
anim_file = options.get('animation_file')
if parent and transform: if parent and transform:
location = transform.get('translation') location = transform.get('translation')
@ -254,6 +259,26 @@ class BlendRigLoader(plugin.AssetLoader):
plugin.deselect_all() plugin.deselect_all()
if anim_file:
bpy.ops.import_scene.fbx(filepath=anim_file, anim_offset=0.0)
imported = avalon_lib.get_selection()
armature = [
o for o in asset_group.children if o.type == 'ARMATURE'][0]
imported_group = [
o for o in imported if o.type == 'EMPTY'][0]
for obj in imported:
if obj.type == 'ARMATURE':
if not armature.animation_data:
armature.animation_data_create()
armature.animation_data.action = obj.animation_data.action
self._remove(imported_group)
bpy.data.objects.remove(imported_group)
bpy.context.scene.collection.objects.link(asset_group) bpy.context.scene.collection.objects.link(asset_group)
asset_group[AVALON_PROPERTY] = { asset_group[AVALON_PROPERTY] = {
@ -350,6 +375,7 @@ class BlendRigLoader(plugin.AssetLoader):
metadata["libpath"] = str(libpath) metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"]) metadata["representation"] = str(representation["_id"])
metadata["parent"] = str(representation["parent"])
def exec_remove(self, container: Dict) -> bool: def exec_remove(self, container: Dict) -> bool:
"""Remove an existing asset group from a Blender scene. """Remove an existing asset group from a Blender scene.

View file

@ -29,12 +29,13 @@ class ExtractBlendAnimation(openpype.api.Extractor):
if isinstance(obj, bpy.types.Object) and obj.type == 'EMPTY': if isinstance(obj, bpy.types.Object) and obj.type == 'EMPTY':
child = obj.children[0] child = obj.children[0]
if child and child.type == 'ARMATURE': if child and child.type == 'ARMATURE':
if not obj.animation_data: if child.animation_data and child.animation_data.action:
obj.animation_data_create() if not obj.animation_data:
obj.animation_data.action = child.animation_data.action obj.animation_data_create()
obj.animation_data_clear() obj.animation_data.action = child.animation_data.action
data_blocks.add(child.animation_data.action) obj.animation_data_clear()
data_blocks.add(obj) data_blocks.add(child.animation_data.action)
data_blocks.add(obj)
bpy.data.libraries.write(filepath, data_blocks) bpy.data.libraries.write(filepath, data_blocks)

View file

@ -50,6 +50,9 @@ class ExtractFBX(api.Extractor):
new_materials.append(mat) new_materials.append(mat)
new_materials_objs.append(obj) new_materials_objs.append(obj)
scale_length = bpy.context.scene.unit_settings.scale_length
bpy.context.scene.unit_settings.scale_length = 0.01
# We export the fbx # We export the fbx
bpy.ops.export_scene.fbx( bpy.ops.export_scene.fbx(
context, context,
@ -60,6 +63,8 @@ class ExtractFBX(api.Extractor):
add_leaf_bones=False add_leaf_bones=False
) )
bpy.context.scene.unit_settings.scale_length = scale_length
plugin.deselect_all() plugin.deselect_all()
for mat in new_materials: for mat in new_materials:

View file

@ -37,13 +37,6 @@ class ExtractAnimationFBX(api.Extractor):
armature = [ armature = [
obj for obj in asset_group.children if obj.type == 'ARMATURE'][0] obj for obj in asset_group.children if obj.type == 'ARMATURE'][0]
asset_group_name = asset_group.name
asset_group.name = asset_group.get(AVALON_PROPERTY).get("asset_name")
armature_name = armature.name
original_name = armature_name.split(':')[1]
armature.name = original_name
object_action_pairs = [] object_action_pairs = []
original_actions = [] original_actions = []
@ -66,6 +59,13 @@ class ExtractAnimationFBX(api.Extractor):
self.log.info("Object have no animation.") self.log.info("Object have no animation.")
return return
asset_group_name = asset_group.name
asset_group.name = asset_group.get(AVALON_PROPERTY).get("asset_name")
armature_name = armature.name
original_name = armature_name.split(':')[1]
armature.name = original_name
object_action_pairs.append((armature, copy_action)) object_action_pairs.append((armature, copy_action))
original_actions.append(curr_action) original_actions.append(curr_action)
@ -123,7 +123,7 @@ class ExtractAnimationFBX(api.Extractor):
json_path = os.path.join(stagingdir, json_filename) json_path = os.path.join(stagingdir, json_filename)
json_dict = { json_dict = {
"instance_name": asset_group.get(AVALON_PROPERTY).get("namespace") "instance_name": asset_group.get(AVALON_PROPERTY).get("objectName")
} }
# collection = instance.data.get("name") # collection = instance.data.get("name")

View file

@ -2,8 +2,11 @@ import os
import json import json
import bpy import bpy
import bpy_extras
import bpy_extras.anim_utils
from avalon import io from avalon import io
from openpype.hosts.blender.api import plugin
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
import openpype.api import openpype.api
@ -16,6 +19,99 @@ class ExtractLayout(openpype.api.Extractor):
families = ["layout"] families = ["layout"]
optional = True optional = True
def _export_animation(self, asset, instance, stagingdir, fbx_count):
n = fbx_count
for obj in asset.children:
if obj.type != "ARMATURE":
continue
object_action_pairs = []
original_actions = []
starting_frames = []
ending_frames = []
# For each armature, we make a copy of the current action
curr_action = None
copy_action = None
if obj.animation_data and obj.animation_data.action:
curr_action = obj.animation_data.action
copy_action = curr_action.copy()
curr_frame_range = curr_action.frame_range
starting_frames.append(curr_frame_range[0])
ending_frames.append(curr_frame_range[1])
else:
self.log.info("Object have no animation.")
continue
asset_group_name = asset.name
asset.name = asset.get(AVALON_PROPERTY).get("asset_name")
armature_name = obj.name
original_name = armature_name.split(':')[1]
obj.name = original_name
object_action_pairs.append((obj, copy_action))
original_actions.append(curr_action)
# We compute the starting and ending frames
max_frame = min(starting_frames)
min_frame = max(ending_frames)
# We bake the copy of the current action for each object
bpy_extras.anim_utils.bake_action_objects(
object_action_pairs,
frames=range(int(min_frame), int(max_frame)),
do_object=False,
do_clean=False
)
for o in bpy.data.objects:
o.select_set(False)
asset.select_set(True)
obj.select_set(True)
fbx_filename = f"{n:03d}.fbx"
filepath = os.path.join(stagingdir, fbx_filename)
override = plugin.create_blender_context(
active=asset, selected=[asset, obj])
bpy.ops.export_scene.fbx(
override,
filepath=filepath,
use_active_collection=False,
use_selection=True,
bake_anim_use_nla_strips=False,
bake_anim_use_all_actions=False,
add_leaf_bones=False,
armature_nodetype='ROOT',
object_types={'EMPTY', 'ARMATURE'}
)
obj.name = armature_name
asset.name = asset_group_name
asset.select_set(False)
obj.select_set(False)
# We delete the baked action and set the original one back
for i in range(0, len(object_action_pairs)):
pair = object_action_pairs[i]
action = original_actions[i]
if action:
pair[0].animation_data.action = action
if pair[1]:
pair[1].user_clear()
bpy.data.actions.remove(pair[1])
return fbx_filename, n + 1
return None, n
def process(self, instance): def process(self, instance):
# Define extract output file path # Define extract output file path
stagingdir = self.staging_dir(instance) stagingdir = self.staging_dir(instance)
@ -23,10 +119,16 @@ class ExtractLayout(openpype.api.Extractor):
# Perform extraction # Perform extraction
self.log.info("Performing extraction..") self.log.info("Performing extraction..")
if "representations" not in instance.data:
instance.data["representations"] = []
json_data = [] json_data = []
fbx_files = []
asset_group = bpy.data.objects[str(instance)] asset_group = bpy.data.objects[str(instance)]
fbx_count = 0
for asset in asset_group.children: for asset in asset_group.children:
metadata = asset.get(AVALON_PROPERTY) metadata = asset.get(AVALON_PROPERTY)
@ -34,6 +136,7 @@ class ExtractLayout(openpype.api.Extractor):
family = metadata["family"] family = metadata["family"]
self.log.debug("Parent: {}".format(parent)) self.log.debug("Parent: {}".format(parent))
# Get blend reference
blend = io.find_one( blend = io.find_one(
{ {
"type": "representation", "type": "representation",
@ -41,10 +144,39 @@ class ExtractLayout(openpype.api.Extractor):
"name": "blend" "name": "blend"
}, },
projection={"_id": True}) projection={"_id": True})
blend_id = blend["_id"] blend_id = None
if blend:
blend_id = blend["_id"]
# Get fbx reference
fbx = io.find_one(
{
"type": "representation",
"parent": io.ObjectId(parent),
"name": "fbx"
},
projection={"_id": True})
fbx_id = None
if fbx:
fbx_id = fbx["_id"]
# Get abc reference
abc = io.find_one(
{
"type": "representation",
"parent": io.ObjectId(parent),
"name": "abc"
},
projection={"_id": True})
abc_id = None
if abc:
abc_id = abc["_id"]
json_element = {} json_element = {}
json_element["reference"] = str(blend_id) if blend_id:
json_element["reference"] = str(blend_id)
if fbx_id:
json_element["reference_fbx"] = str(fbx_id)
if abc_id:
json_element["reference_abc"] = str(abc_id)
json_element["family"] = family json_element["family"] = family
json_element["instance_name"] = asset.name json_element["instance_name"] = asset.name
json_element["asset_name"] = metadata["asset_name"] json_element["asset_name"] = metadata["asset_name"]
@ -67,6 +199,16 @@ class ExtractLayout(openpype.api.Extractor):
"z": asset.scale.z "z": asset.scale.z
} }
} }
# Extract the animation as well
if family == "rig":
f, n = self._export_animation(
asset, instance, stagingdir, fbx_count)
if f:
fbx_files.append(f)
json_element["animation"] = f
fbx_count = n
json_data.append(json_element) json_data.append(json_element)
json_filename = "{}.json".format(instance.name) json_filename = "{}.json".format(instance.name)
@ -75,16 +217,32 @@ class ExtractLayout(openpype.api.Extractor):
with open(json_path, "w+") as file: with open(json_path, "w+") as file:
json.dump(json_data, fp=file, indent=2) json.dump(json_data, fp=file, indent=2)
if "representations" not in instance.data: json_representation = {
instance.data["representations"] = []
representation = {
'name': 'json', 'name': 'json',
'ext': 'json', 'ext': 'json',
'files': json_filename, 'files': json_filename,
"stagingDir": stagingdir, "stagingDir": stagingdir,
} }
instance.data["representations"].append(representation) instance.data["representations"].append(json_representation)
self.log.debug(fbx_files)
if len(fbx_files) == 1:
fbx_representation = {
'name': 'fbx',
'ext': '000.fbx',
'files': fbx_files[0],
"stagingDir": stagingdir,
}
instance.data["representations"].append(fbx_representation)
elif len(fbx_files) > 1:
fbx_representation = {
'name': 'fbx',
'ext': 'fbx',
'files': fbx_files,
"stagingDir": stagingdir,
}
instance.data["representations"].append(fbx_representation)
self.log.info("Extracted instance '%s' to: %s", self.log.info("Extracted instance '%s' to: %s",
instance.name, representation) instance.name, json_representation)

View file

@ -9,7 +9,7 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
label = "Increment Workfile Version" label = "Increment Workfile Version"
optional = True optional = True
hosts = ["blender"] hosts = ["blender"]
families = ["animation", "model", "rig", "action"] families = ["animation", "model", "rig", "action", "layout"]
def process(self, context): def process(self, context):

View file

@ -5,15 +5,15 @@ import openpype.hosts.blender.api.action
class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin): class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin):
"""Validate that the current object is in Object Mode.""" """Validate that the objects in the instance are in Object Mode."""
order = pyblish.api.ValidatorOrder - 0.01 order = pyblish.api.ValidatorOrder - 0.01
hosts = ["blender"] hosts = ["blender"]
families = ["model", "rig"] families = ["model", "rig", "layout"]
category = "geometry" category = "geometry"
label = "Object is in Object Mode" label = "Validate Object Mode"
actions = [openpype.hosts.blender.api.action.SelectInvalidAction] actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
optional = True optional = False
@classmethod @classmethod
def get_invalid(cls, instance) -> List: def get_invalid(cls, instance) -> List:

View file

@ -71,7 +71,7 @@ class ExtractSubsetResources(openpype.api.Extractor):
staging_dir = self.staging_dir(instance) staging_dir = self.staging_dir(instance)
# add default preset type for thumbnail and reviewable video # add default preset type for thumbnail and reviewable video
# update them with settings and overide in case the same # update them with settings and override in case the same
# are found in there # are found in there
export_presets = deepcopy(self.default_presets) export_presets = deepcopy(self.default_presets)
export_presets.update(self.export_presets_mapping) export_presets.update(self.export_presets_mapping)

View file

@ -218,12 +218,10 @@ def on_task_changed(*args):
) )
def before_workfile_save(workfile_path): def before_workfile_save(event):
if not workfile_path: workdir_path = event.workdir_path
return if workdir_path:
copy_workspace_mel(workdir_path)
workdir = os.path.dirname(workfile_path)
copy_workspace_mel(workdir)
class MayaDirmap(HostDirmap): class MayaDirmap(HostDirmap):

View file

@ -0,0 +1,34 @@
from maya import cmds
import pyblish.api
from avalon import maya
import openpype.api
import openpype.hosts.maya.api.action
class ValidateCycleError(pyblish.api.InstancePlugin):
"""Validate nodes produce no cycle errors."""
order = openpype.api.ValidateContentsOrder + 0.05
label = "Cycle Errors"
hosts = ["maya"]
families = ["rig"]
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
optional = True
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError("Nodes produce a cycle error: %s" % invalid)
@classmethod
def get_invalid(cls, instance):
with maya.maintained_selection():
cmds.select(instance[:], noExpand=True)
plugs = cmds.cycleCheck(all=False, # check selection only
list=True)
invalid = cmds.ls(plugs, objectsOnly=True, long=True)
return invalid

View file

@ -71,8 +71,18 @@ class AnimationFBXLoader(api.Loader):
if instance_name: if instance_name:
automated = True automated = True
actor_name = 'PersistentLevel.' + instance_name # Old method to get the actor
actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name) # actor_name = 'PersistentLevel.' + instance_name
# actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name)
actors = unreal.EditorLevelLibrary.get_all_level_actors()
for a in actors:
if a.get_class().get_name() != "SkeletalMeshActor":
continue
if a.get_actor_label() == instance_name:
actor = a
break
if not actor:
raise Exception(f"Could not find actor {instance_name}")
skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton
task.options.set_editor_property('skeleton', skeleton) task.options.set_editor_property('skeleton', skeleton)
@ -173,20 +183,35 @@ class AnimationFBXLoader(api.Loader):
task.set_editor_property('destination_name', name) task.set_editor_property('destination_name', name)
task.set_editor_property('replace_existing', True) task.set_editor_property('replace_existing', True)
task.set_editor_property('automated', True) task.set_editor_property('automated', True)
task.set_editor_property('save', False) task.set_editor_property('save', True)
# set import options here # set import options here
task.options.set_editor_property( task.options.set_editor_property(
'automated_import_should_detect_type', True) 'automated_import_should_detect_type', False)
task.options.set_editor_property( task.options.set_editor_property(
'original_import_type', unreal.FBXImportType.FBXIT_ANIMATION) 'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH)
task.options.set_editor_property(
'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION)
task.options.set_editor_property('import_mesh', False) task.options.set_editor_property('import_mesh', False)
task.options.set_editor_property('import_animations', True) task.options.set_editor_property('import_animations', True)
task.options.set_editor_property('override_full_name', True)
task.options.skeletal_mesh_import_data.set_editor_property( task.options.anim_sequence_import_data.set_editor_property(
'import_content_type', 'animation_length',
unreal.FBXImportContentType.FBXICT_SKINNING_WEIGHTS unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME
) )
task.options.anim_sequence_import_data.set_editor_property(
'import_meshes_in_bone_hierarchy', False)
task.options.anim_sequence_import_data.set_editor_property(
'use_default_sample_rate', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_custom_attribute', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_bone_tracks', True)
task.options.anim_sequence_import_data.set_editor_property(
'remove_redundant_keys', True)
task.options.anim_sequence_import_data.set_editor_property(
'convert_scene', True)
skeletal_mesh = unreal.EditorAssetLibrary.load_asset( skeletal_mesh = unreal.EditorAssetLibrary.load_asset(
container.get('namespace') + "/" + container.get('asset_name')) container.get('namespace') + "/" + container.get('asset_name'))
@ -219,7 +244,7 @@ class AnimationFBXLoader(api.Loader):
unreal.EditorAssetLibrary.delete_directory(path) unreal.EditorAssetLibrary.delete_directory(path)
asset_content = unreal.EditorAssetLibrary.list_assets( asset_content = unreal.EditorAssetLibrary.list_assets(
parent_path, recursive=False parent_path, recursive=False, include_folder=True
) )
if len(asset_content) == 0: if len(asset_content) == 0:

View file

@ -0,0 +1,544 @@
import os
import json
from pathlib import Path
import unreal
from unreal import EditorAssetLibrary
from unreal import EditorLevelLibrary
from unreal import AssetToolsHelpers
from unreal import FBXImportType
from unreal import MathLibrary as umath
from avalon import api, pipeline
from avalon.unreal import lib
from avalon.unreal import pipeline as unreal_pipeline
class LayoutLoader(api.Loader):
"""Load Layout from a JSON file"""
families = ["layout"]
representations = ["json"]
label = "Load Layout"
icon = "code-fork"
color = "orange"
def _get_asset_containers(self, path):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
asset_content = EditorAssetLibrary.list_assets(
path, recursive=True)
asset_containers = []
# Get all the asset containers
for a in asset_content:
obj = ar.get_asset_by_object_path(a)
if obj.get_asset().get_class().get_name() == 'AssetContainer':
asset_containers.append(obj)
return asset_containers
def _get_fbx_loader(self, loaders, family):
name = ""
if family == 'rig':
name = "SkeletalMeshFBXLoader"
elif family == 'model':
name = "StaticMeshFBXLoader"
elif family == 'camera':
name = "CameraLoader"
if name == "":
return None
for loader in loaders:
if loader.__name__ == name:
return loader
return None
def _get_abc_loader(self, 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 _process_family(self, assets, classname, transform, inst_name=None):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
actors = []
for asset in assets:
obj = ar.get_asset_by_object_path(asset).get_asset()
if obj.get_class().get_name() == classname:
actor = EditorLevelLibrary.spawn_actor_from_object(
obj,
transform.get('translation')
)
if inst_name:
try:
# Rename method leads to crash
# actor.rename(name=inst_name)
# The label works, although it make it slightly more
# complicated to check for the names, as we need to
# loop through all the actors in the level
actor.set_actor_label(inst_name)
except Exception as e:
print(e)
actor.set_actor_rotation(unreal.Rotator(
umath.radians_to_degrees(
transform.get('rotation').get('x')),
-umath.radians_to_degrees(
transform.get('rotation').get('y')),
umath.radians_to_degrees(
transform.get('rotation').get('z')),
), False)
actor.set_actor_scale3d(transform.get('scale'))
actors.append(actor)
return actors
def _import_animation(
self, asset_dir, path, instance_name, skeleton, actors_dict,
animation_file):
anim_file = Path(animation_file)
anim_file_name = anim_file.with_suffix('')
anim_path = f"{asset_dir}/animations/{anim_file_name}"
# Import animation
task = unreal.AssetImportTask()
task.options = unreal.FbxImportUI()
task.set_editor_property(
'filename', str(path.with_suffix(f".{animation_file}")))
task.set_editor_property('destination_path', anim_path)
task.set_editor_property(
'destination_name', f"{instance_name}_animation")
task.set_editor_property('replace_existing', False)
task.set_editor_property('automated', True)
task.set_editor_property('save', False)
# set import options here
task.options.set_editor_property(
'automated_import_should_detect_type', False)
task.options.set_editor_property(
'original_import_type', FBXImportType.FBXIT_SKELETAL_MESH)
task.options.set_editor_property(
'mesh_type_to_import', FBXImportType.FBXIT_ANIMATION)
task.options.set_editor_property('import_mesh', False)
task.options.set_editor_property('import_animations', True)
task.options.set_editor_property('override_full_name', True)
task.options.set_editor_property('skeleton', skeleton)
task.options.anim_sequence_import_data.set_editor_property(
'animation_length',
unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME
)
task.options.anim_sequence_import_data.set_editor_property(
'import_meshes_in_bone_hierarchy', False)
task.options.anim_sequence_import_data.set_editor_property(
'use_default_sample_rate', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_custom_attribute', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_bone_tracks', True)
task.options.anim_sequence_import_data.set_editor_property(
'remove_redundant_keys', True)
task.options.anim_sequence_import_data.set_editor_property(
'convert_scene', True)
AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
asset_content = unreal.EditorAssetLibrary.list_assets(
anim_path, recursive=False, include_folder=False
)
animation = None
for a in asset_content:
unreal.EditorAssetLibrary.save_asset(a)
imported_asset_data = unreal.EditorAssetLibrary.find_asset_data(a)
imported_asset = unreal.AssetRegistryHelpers.get_asset(
imported_asset_data)
if imported_asset.__class__ == unreal.AnimSequence:
animation = imported_asset
break
if animation:
actor = None
if actors_dict.get(instance_name):
for a in actors_dict.get(instance_name):
if a.get_class().get_name() == 'SkeletalMeshActor':
actor = a
break
animation.set_editor_property('enable_root_motion', True)
actor.skeletal_mesh_component.set_editor_property(
'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE)
actor.skeletal_mesh_component.animation_data.set_editor_property(
'anim_to_play', animation)
def _process(self, libpath, asset_dir, loaded=None):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
with open(libpath, "r") as fp:
data = json.load(fp)
all_loaders = api.discover(api.Loader)
if not loaded:
loaded = []
path = Path(libpath)
skeleton_dict = {}
actors_dict = {}
for element in data:
reference = None
if element.get('reference_fbx'):
reference = element.get('reference_fbx')
elif element.get('reference_abc'):
reference = element.get('reference_abc')
# If reference is None, this element is skipped, as it cannot be
# imported in Unreal
if not reference:
continue
instance_name = element.get('instance_name')
skeleton = None
if reference not in loaded:
loaded.append(reference)
family = element.get('family')
loaders = api.loaders_from_representation(
all_loaders, reference)
loader = None
if reference == element.get('reference_fbx'):
loader = self._get_fbx_loader(loaders, family)
elif reference == element.get('reference_abc'):
loader = self._get_abc_loader(loaders, family)
if not loader:
continue
options = {
"asset_dir": asset_dir
}
assets = api.load(
loader,
reference,
namespace=instance_name,
options=options
)
instances = [
item for item in data
if (item.get('reference_fbx') == reference or
item.get('reference_abc') == reference)]
for instance in instances:
transform = instance.get('transform')
inst = instance.get('instance_name')
actors = []
if family == 'model':
actors = self._process_family(
assets, 'StaticMesh', transform, inst)
elif family == 'rig':
actors = self._process_family(
assets, 'SkeletalMesh', transform, inst)
actors_dict[inst] = actors
if family == 'rig':
# Finds skeleton among the imported assets
for asset in assets:
obj = ar.get_asset_by_object_path(asset).get_asset()
if obj.get_class().get_name() == 'Skeleton':
skeleton = obj
if skeleton:
break
if skeleton:
skeleton_dict[reference] = skeleton
else:
skeleton = skeleton_dict.get(reference)
animation_file = element.get('animation')
if animation_file and skeleton:
self._import_animation(
asset_dir, path, instance_name, skeleton,
actors_dict, animation_file)
def _remove_family(self, assets, components, classname, propname):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
objects = []
for a in assets:
obj = ar.get_asset_by_object_path(a)
if obj.get_asset().get_class().get_name() == classname:
objects.append(obj)
for obj in objects:
for comp in components:
if comp.get_editor_property(propname) == obj.get_asset():
comp.get_owner().destroy_actor()
def _remove_actors(self, path):
asset_containers = self._get_asset_containers(path)
# Get all the static and skeletal meshes components in the level
components = EditorLevelLibrary.get_all_level_actors_components()
static_meshes_comp = [
c for c in components
if c.get_class().get_name() == 'StaticMeshComponent']
skel_meshes_comp = [
c for c in components
if c.get_class().get_name() == 'SkeletalMeshComponent']
# For all the asset containers, get the static and skeletal meshes.
# Then, check the components in the level and destroy the matching
# actors.
for asset_container in asset_containers:
package_path = asset_container.get_editor_property('package_path')
family = EditorAssetLibrary.get_metadata_tag(
asset_container.get_asset(), 'family')
assets = EditorAssetLibrary.list_assets(
str(package_path), recursive=False)
if family == 'model':
self._remove_family(
assets, static_meshes_comp, 'StaticMesh', 'static_mesh')
elif family == 'rig':
self._remove_family(
assets, skel_meshes_comp, 'SkeletalMesh', 'skeletal_mesh')
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
then call `containerise()` on it - this moves all content to new
directory and then it will create AssetContainer there and imprint it
with metadata. This will mark this path as container.
Args:
context (dict): application context
name (str): subset name
namespace (str): in Unreal this is basically path to container.
This is not passed here, so namespace is set
by `containerise()` because only then we know
real path.
data (dict): Those would be data to be imprinted. This is not used
now, data are imprinted by `containerise()`.
Returns:
list(str): list of container content
"""
# Create directory for asset and avalon container
root = "/Game/Avalon/Assets"
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
"{}/{}/{}".format(root, asset, name), suffix="")
container_name += suffix
EditorAssetLibrary.make_directory(asset_dir)
self._process(self.fname, asset_dir)
# Create Asset Container
lib.create_avalon_container(
container=container_name, path=asset_dir)
data = {
"schema": "openpype:container-2.0",
"id": pipeline.AVALON_CONTAINER_ID,
"asset": asset,
"namespace": asset_dir,
"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"]
}
unreal_pipeline.imprint(
"{}/{}".format(asset_dir, container_name), data)
asset_content = EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=False)
for a in asset_content:
EditorAssetLibrary.save_asset(a)
return asset_content
def update(self, container, representation):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
source_path = api.get_representation_path(representation)
destination_path = container["namespace"]
libpath = Path(api.get_representation_path(representation))
self._remove_actors(destination_path)
# Delete old animations
anim_path = f"{destination_path}/animations/"
EditorAssetLibrary.delete_directory(anim_path)
with open(source_path, "r") as fp:
data = json.load(fp)
references = [e.get('reference_fbx') for e in data]
asset_containers = self._get_asset_containers(destination_path)
loaded = []
# Delete all the assets imported with the previous version of the
# layout, if they're not in the new layout.
for asset_container in asset_containers:
if asset_container.get_editor_property(
'asset_name') == container["objectName"]:
continue
ref = EditorAssetLibrary.get_metadata_tag(
asset_container.get_asset(), 'representation')
ppath = asset_container.get_editor_property('package_path')
if ref not in references:
# If the asset is not in the new layout, delete it.
# Also check if the parent directory is empty, and delete that
# as well, if it is.
EditorAssetLibrary.delete_directory(ppath)
parent = os.path.dirname(str(ppath))
parent_content = EditorAssetLibrary.list_assets(
parent, recursive=False, include_folder=True
)
if len(parent_content) == 0:
EditorAssetLibrary.delete_directory(parent)
else:
# If the asset is in the new layout, search the instances in
# the JSON file, and create actors for them.
actors_dict = {}
skeleton_dict = {}
for element in data:
reference = element.get('reference_fbx')
instance_name = element.get('instance_name')
skeleton = None
if reference == ref and ref not in loaded:
loaded.append(ref)
family = element.get('family')
assets = EditorAssetLibrary.list_assets(
ppath, recursive=True, include_folder=False)
instances = [
item for item in data
if item.get('reference_fbx') == reference]
for instance in instances:
transform = instance.get('transform')
inst = instance.get('instance_name')
actors = []
if family == 'model':
actors = self._process_family(
assets, 'StaticMesh', transform, inst)
elif family == 'rig':
actors = self._process_family(
assets, 'SkeletalMesh', transform, inst)
actors_dict[inst] = actors
if family == 'rig':
# Finds skeleton among the imported assets
for asset in assets:
obj = ar.get_asset_by_object_path(
asset).get_asset()
if obj.get_class().get_name() == 'Skeleton':
skeleton = obj
if skeleton:
break
if skeleton:
skeleton_dict[reference] = skeleton
else:
skeleton = skeleton_dict.get(reference)
animation_file = element.get('animation')
if animation_file and skeleton:
self._import_animation(
destination_path, libpath,
instance_name, skeleton,
actors_dict, animation_file)
self._process(source_path, destination_path, loaded)
container_path = "{}/{}".format(container["namespace"],
container["objectName"])
# update metadata
unreal_pipeline.imprint(
container_path,
{
"representation": str(representation["_id"]),
"parent": str(representation["parent"])
})
asset_content = EditorAssetLibrary.list_assets(
destination_path, recursive=True, include_folder=False)
for a in asset_content:
EditorAssetLibrary.save_asset(a)
def remove(self, container):
"""
First, destroy all actors of the assets to be removed. Then, deletes
the asset's directory.
"""
path = container["namespace"]
parent_path = os.path.dirname(path)
self._remove_actors(path)
EditorAssetLibrary.delete_directory(path)
asset_content = EditorAssetLibrary.list_assets(
parent_path, recursive=False, include_folder=True
)
if len(asset_content) == 0:
EditorAssetLibrary.delete_directory(parent_path)

View file

@ -15,7 +15,7 @@ class SkeletalMeshFBXLoader(api.Loader):
icon = "cube" icon = "cube"
color = "orange" color = "orange"
def load(self, context, name, namespace, data): def load(self, context, name, namespace, options):
""" """
Load and containerise representation into Content Browser. Load and containerise representation into Content Browser.
@ -40,6 +40,8 @@ class SkeletalMeshFBXLoader(api.Loader):
# Create directory for asset and avalon container # Create directory for asset and avalon container
root = "/Game/Avalon/Assets" root = "/Game/Avalon/Assets"
if options and options.get("asset_dir"):
root = options["asset_dir"]
asset = context.get('asset').get('name') asset = context.get('asset').get('name')
suffix = "_CON" suffix = "_CON"
if asset: if asset:

View file

@ -40,7 +40,7 @@ class StaticMeshFBXLoader(api.Loader):
return task return task
def load(self, context, name, namespace, data): def load(self, context, name, namespace, options):
""" """
Load and containerise representation into Content Browser. Load and containerise representation into Content Browser.
@ -65,6 +65,8 @@ class StaticMeshFBXLoader(api.Loader):
# Create directory for asset and avalon container # Create directory for asset and avalon container
root = "/Game/Avalon/Assets" root = "/Game/Avalon/Assets"
if options and options.get("asset_dir"):
root = options["asset_dir"]
asset = context.get('asset').get('name') asset = context.get('asset').get('name')
suffix = "_CON" suffix = "_CON"
if asset: if asset:

View file

@ -0,0 +1,139 @@
import os
import shutil
import pyblish.api
from openpype.lib import (
get_ffmpeg_tool_path,
run_subprocess,
get_transcode_temp_directory,
convert_for_ffmpeg,
should_convert_for_ffmpeg
)
class ExtractThumbnail(pyblish.api.InstancePlugin):
"""Create jpg thumbnail from input using ffmpeg."""
label = "Extract Thumbnail"
order = pyblish.api.ExtractorOrder
families = [
"render",
"image"
]
hosts = ["webpublisher"]
targets = ["filespublish"]
def process(self, instance):
self.log.info("subset {}".format(instance.data['subset']))
filtered_repres = self._get_filtered_repres(instance)
for repre in filtered_repres:
repre_files = repre["files"]
if not isinstance(repre_files, (list, tuple)):
input_file = repre_files
else:
file_index = int(float(len(repre_files)) * 0.5)
input_file = repre_files[file_index]
stagingdir = os.path.normpath(repre["stagingDir"])
full_input_path = os.path.join(stagingdir, input_file)
self.log.info("Input filepath: {}".format(full_input_path))
do_convert = should_convert_for_ffmpeg(full_input_path)
# If result is None the requirement of conversion can't be
# determined
if do_convert is None:
self.log.info((
"Can't determine if representation requires conversion."
" Skipped."
))
continue
# Do conversion if needed
# - change staging dir of source representation
# - must be set back after output definitions processing
convert_dir = None
if do_convert:
convert_dir = get_transcode_temp_directory()
filename = os.path.basename(full_input_path)
convert_for_ffmpeg(
full_input_path,
convert_dir,
None,
None,
self.log
)
full_input_path = os.path.join(convert_dir, filename)
filename = os.path.splitext(input_file)[0]
while filename.endswith("."):
filename = filename[:-1]
thumbnail_filename = filename + "_thumbnail.jpg"
full_output_path = os.path.join(stagingdir, thumbnail_filename)
self.log.info("output {}".format(full_output_path))
ffmpeg_args = [
get_ffmpeg_tool_path("ffmpeg"),
"-y",
"-i", full_input_path,
"-vframes", "1",
full_output_path
]
# run subprocess
self.log.debug("{}".format(" ".join(ffmpeg_args)))
try: # temporary until oiiotool is supported cross platform
run_subprocess(
ffmpeg_args, logger=self.log
)
except RuntimeError as exp:
if "Compression" in str(exp):
self.log.debug(
"Unsupported compression on input files. Skipping!!!"
)
return
self.log.warning("Conversion crashed", exc_info=True)
raise
new_repre = {
"name": "thumbnail",
"ext": "jpg",
"files": thumbnail_filename,
"stagingDir": stagingdir,
"thumbnail": True,
"tags": ["thumbnail"]
}
# adding representation
self.log.debug("Adding: {}".format(new_repre))
instance.data["representations"].append(new_repre)
# Cleanup temp folder
if convert_dir is not None and os.path.exists(convert_dir):
shutil.rmtree(convert_dir)
def _get_filtered_repres(self, instance):
filtered_repres = []
repres = instance.data.get("representations") or []
for repre in repres:
self.log.debug(repre)
tags = repre.get("tags") or []
# Skip instance if already has thumbnail representation
if "thumbnail" in tags:
return []
if "review" not in tags:
continue
if not repre.get("files"):
self.log.info((
"Representation \"{}\" don't have files. Skipping"
).format(repre["name"]))
continue
filtered_repres.append(repre)
return filtered_repres

View file

@ -1490,6 +1490,7 @@ def _prepare_last_workfile(data, workdir):
import avalon.api import avalon.api
log = data["log"] log = data["log"]
_workdir_data = data.get("workdir_data") _workdir_data = data.get("workdir_data")
if not _workdir_data: if not _workdir_data:
log.info( log.info(
@ -1503,9 +1504,15 @@ def _prepare_last_workfile(data, workdir):
project_name = data["project_name"] project_name = data["project_name"]
task_name = data["task_name"] task_name = data["task_name"]
task_type = data["task_type"] task_type = data["task_type"]
start_last_workfile = should_start_last_workfile(
project_name, app.host_name, task_name, task_type start_last_workfile = data.get("start_last_workfile")
) if start_last_workfile is None:
start_last_workfile = should_start_last_workfile(
project_name, app.host_name, task_name, task_type
)
else:
log.info("Opening of last workfile was disabled by user")
data["start_last_workfile"] = start_last_workfile data["start_last_workfile"] = start_last_workfile
workfile_startup = should_workfile_tool_start( workfile_startup = should_workfile_tool_start(

View file

@ -164,7 +164,7 @@ class ProcessEventHub(SocketBaseEventHub):
sys.exit(0) sys.exit(0)
def wait(self, duration=None): def wait(self, duration=None):
"""Overriden wait """Overridden wait
Event are loaded from Mongo DB when queue is empty. Handled event is Event are loaded from Mongo DB when queue is empty. Handled event is
set as processed in Mongo DB. set as processed in Mongo DB.
""" """

View file

@ -95,7 +95,7 @@ class DropboxHandler(AbstractProvider):
"key": "acting_as_member", "key": "acting_as_member",
"label": "Acting As Member" "label": "Acting As Member"
}, },
# roots could be overriden only on Project level, User cannot # roots could be overridden only on Project level, User cannot
{ {
"key": "root", "key": "root",
"label": "Roots", "label": "Roots",

View file

@ -119,7 +119,7 @@ class GDriveHandler(AbstractProvider):
# {platform} tells that value is multiplatform and only specific OS # {platform} tells that value is multiplatform and only specific OS
# should be returned # should be returned
editable = [ editable = [
# credentials could be overriden on Project or User level # credentials could be overridden on Project or User level
{ {
"type": "path", "type": "path",
"key": "credentials_url", "key": "credentials_url",
@ -127,7 +127,7 @@ class GDriveHandler(AbstractProvider):
"multiplatform": True, "multiplatform": True,
"placeholder": "Credentials url" "placeholder": "Credentials url"
}, },
# roots could be overriden only on Project leve, User cannot # roots could be overridden only on Project level, User cannot
{ {
"key": "root", "key": "root",
"label": "Roots", "label": "Roots",
@ -414,7 +414,7 @@ class GDriveHandler(AbstractProvider):
def delete_folder(self, path, force=False): def delete_folder(self, path, force=False):
""" """
Deletes folder on GDrive. Checks if folder contains any files or Deletes folder on GDrive. Checks if folder contains any files or
subfolders. In that case raises error, could be overriden by subfolders. In that case raises error, could be overridden by
'force' argument. 'force' argument.
In that case deletes folder on 'path' and all its children. In that case deletes folder on 'path' and all its children.

View file

@ -97,7 +97,7 @@ class SFTPHandler(AbstractProvider):
# {platform} tells that value is multiplatform and only specific OS # {platform} tells that value is multiplatform and only specific OS
# should be returned # should be returned
editable = [ editable = [
# credentials could be overriden on Project or User level # credentials could be overridden on Project or User level
{ {
'key': "sftp_host", 'key': "sftp_host",
'label': "SFTP host name", 'label': "SFTP host name",
@ -129,7 +129,7 @@ class SFTPHandler(AbstractProvider):
'label': "SFTP user ssh key password", 'label': "SFTP user ssh key password",
'type': 'text' 'type': 'text'
}, },
# roots could be overriden only on Project leve, User cannot # roots could be overridden only on Project level, User cannot
{ {
"key": "root", "key": "root",
"label": "Roots", "label": "Roots",

View file

@ -1073,7 +1073,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule):
""" """
Returns settings for 'studio' and user's local site Returns settings for 'studio' and user's local site
Returns base values from setting, not overriden by Local Settings, Returns base values from setting, not overridden by Local Settings,
eg. value used to push TO LS not to get actual value for syncing. eg. value used to push TO LS not to get actual value for syncing.
""" """
if not project_name: if not project_name:

View file

@ -115,7 +115,7 @@ class ITrayAction(ITrayModule):
Add action to tray menu which will trigger `on_action_trigger`. Add action to tray menu which will trigger `on_action_trigger`.
It is expected to be used for showing tools. It is expected to be used for showing tools.
Methods `tray_start`, `tray_exit` and `connect_with_modules` are overriden Methods `tray_start`, `tray_exit` and `connect_with_modules` are overridden
as it's not expected that action will use them. But it is possible if as it's not expected that action will use them. But it is possible if
necessary. necessary.
""" """

View file

@ -72,7 +72,7 @@ class WorkerRpc(JsonRpc):
self._job_queue.remove_worker(worker) self._job_queue.remove_worker(worker)
async def handle_websocket_request(self, http_request): async def handle_websocket_request(self, http_request):
"""Overide this method to catch CLOSING messages.""" """Override this method to catch CLOSING messages."""
http_request.msg_id = 0 http_request.msg_id = 0
http_request.pending = {} http_request.pending = {}

View file

@ -1,3 +1,8 @@
from .events import (
BaseEvent,
BeforeWorkfileSave
)
from .attribute_definitions import ( from .attribute_definitions import (
AbtractAttrDef, AbtractAttrDef,
UnknownDef, UnknownDef,
@ -9,6 +14,9 @@ from .attribute_definitions import (
__all__ = ( __all__ = (
"BaseEvent",
"BeforeWorkfileSave",
"AbtractAttrDef", "AbtractAttrDef",
"UnknownDef", "UnknownDef",
"NumberDef", "NumberDef",

View file

@ -0,0 +1,51 @@
"""Events holding data about specific event."""
# Inherit from 'object' for Python 2 hosts
class BaseEvent(object):
"""Base event object.
Can be used to anything because data are not much specific. Only required
argument is topic which defines why event is happening and may be used for
filtering.
Arg:
topic (str): Identifier of event.
data (Any): Data specific for event. Dictionary is recommended.
"""
_data = {}
def __init__(self, topic, data=None):
self._topic = topic
if data is None:
data = {}
self._data = data
@property
def data(self):
return self._data
@property
def topic(self):
return self._topic
@classmethod
def emit(cls, *args, **kwargs):
"""Create object of event and emit.
Args:
Same args as '__init__' expects which may be class specific.
"""
from avalon import pipeline
obj = cls(*args, **kwargs)
pipeline.emit(obj.topic, [obj])
return obj
class BeforeWorkfileSave(BaseEvent):
"""Before workfile changes event data."""
def __init__(self, filename, workdir):
super(BeforeWorkfileSave, self).__init__("before.workfile.save")
self.filename = filename
self.workdir_path = workdir

View file

@ -31,7 +31,7 @@ class DiscoverResult:
def publish_plugins_discover(paths=None): def publish_plugins_discover(paths=None):
"""Find and return available pyblish plug-ins """Find and return available pyblish plug-ins
Overriden function from `pyblish` module to be able collect crashed files Overridden function from `pyblish` module to be able collect crashed files
and reason of their crash. and reason of their crash.
Arguments: Arguments:

View file

@ -11,6 +11,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin):
order = pyblish.api.CollectorOrder order = pyblish.api.CollectorOrder
label = 'Collect Scene Version' label = 'Collect Scene Version'
# configurable in Settings
hosts = [ hosts = [
"aftereffects", "aftereffects",
"blender", "blender",
@ -26,7 +27,19 @@ class CollectSceneVersion(pyblish.api.ContextPlugin):
"tvpaint" "tvpaint"
] ]
# in some cases of headless publishing (for example webpublisher using PS)
# you want to ignore version from name and let integrate use next version
skip_hosts_headless_publish = []
def process(self, context): def process(self, context):
# tests should be close to regular publish as possible
if (
os.environ.get("HEADLESS_PUBLISH")
and not os.environ.get("IS_TEST")
and context.data["hostName"] in self.skip_hosts_headless_publish):
self.log.debug("Skipping for headless publishing")
return
assert context.data.get('currentFile'), "Cannot get current file" assert context.data.get('currentFile'), "Cannot get current file"
filename = os.path.basename(context.data.get('currentFile')) filename = os.path.basename(context.data.get('currentFile'))

View file

@ -24,7 +24,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin):
"imagesequence", "render", "render2d", "imagesequence", "render", "render2d",
"source", "plate", "take" "source", "plate", "take"
] ]
hosts = ["shell", "fusion", "resolve", "webpublisher"] hosts = ["shell", "fusion", "resolve"]
enabled = False enabled = False
# presetable attribute # presetable attribute

View file

@ -252,7 +252,7 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
- required IF start frame is not set when using frames or timecode burnins - required IF start frame is not set when using frames or timecode burnins
On initializing class can be set General options through "options_init" arg. On initializing class can be set General options through "options_init" arg.
General can be overriden when adding burnin General can be overridden when adding burnin
''' '''
TOP_CENTERED = ffmpeg_burnins.TOP_CENTERED TOP_CENTERED = ffmpeg_burnins.TOP_CENTERED
@ -549,7 +549,7 @@ def burnins_from_data(
codec_data (list): All codec related arguments in list. codec_data (list): All codec related arguments in list.
options (dict): Options for burnins. options (dict): Options for burnins.
burnin_values (dict): Contain positioned values. burnin_values (dict): Contain positioned values.
overwrite (bool): Output will be overriden if already exists, overwrite (bool): Output will be overwritten if already exists,
True by default. True by default.
Presets must be set separately. Should be dict with 2 keys: Presets must be set separately. Should be dict with 2 keys:

View file

@ -2,14 +2,14 @@ import re
# Metadata keys for work with studio and project overrides # Metadata keys for work with studio and project overrides
M_OVERRIDEN_KEY = "__overriden_keys__" M_OVERRIDDEN_KEY = "__overriden_keys__"
# Metadata key for storing information about environments # Metadata key for storing information about environments
M_ENVIRONMENT_KEY = "__environment_keys__" M_ENVIRONMENT_KEY = "__environment_keys__"
# Metadata key for storing dynamic created labels # Metadata key for storing dynamic created labels
M_DYNAMIC_KEY_LABEL = "__dynamic_keys_labels__" M_DYNAMIC_KEY_LABEL = "__dynamic_keys_labels__"
METADATA_KEYS = ( METADATA_KEYS = (
M_OVERRIDEN_KEY, M_OVERRIDDEN_KEY,
M_ENVIRONMENT_KEY, M_ENVIRONMENT_KEY,
M_DYNAMIC_KEY_LABEL M_DYNAMIC_KEY_LABEL
) )
@ -34,7 +34,7 @@ KEY_REGEX = re.compile(r"^[{}]+$".format(KEY_ALLOWED_SYMBOLS))
__all__ = ( __all__ = (
"M_OVERRIDEN_KEY", "M_OVERRIDDEN_KEY",
"M_ENVIRONMENT_KEY", "M_ENVIRONMENT_KEY",
"M_DYNAMIC_KEY_LABEL", "M_DYNAMIC_KEY_LABEL",

View file

@ -3,6 +3,24 @@
"CollectAnatomyInstanceData": { "CollectAnatomyInstanceData": {
"follow_workfile_version": false "follow_workfile_version": false
}, },
"CollectSceneVersion": {
"hosts": [
"aftereffects",
"blender",
"celaction",
"fusion",
"harmony",
"hiero",
"houdini",
"maya",
"nuke",
"photoshop",
"resolve",
"tvpaint"
],
"skip_hosts_headless_publish": [
]
},
"ValidateEditorialAssetName": { "ValidateEditorialAssetName": {
"enabled": true, "enabled": true,
"optional": false "optional": false

View file

@ -188,6 +188,13 @@
"whitelist_native_plugins": false, "whitelist_native_plugins": false,
"authorized_plugins": [] "authorized_plugins": []
}, },
"ValidateCycleError": {
"enabled": true,
"optional": false,
"families": [
"rig"
]
},
"ValidateUnrealStaticMeshName": { "ValidateUnrealStaticMeshName": {
"enabled": true, "enabled": true,
"validate_mesh": false, "validate_mesh": false,

View file

@ -12,7 +12,7 @@
{ {
"color_code": [], "color_code": [],
"layer_name_regex": [], "layer_name_regex": [],
"family": "", "family": "image",
"subset_template_name": "" "subset_template_name": ""
} }
] ]

View file

@ -752,7 +752,7 @@ class BaseItemEntity(BaseEntity):
@abstractmethod @abstractmethod
def _add_to_project_override(self, on_change_trigger): def _add_to_project_override(self, on_change_trigger):
"""Item's implementation to set values as overriden for project. """Item's implementation to set values as overridden for project.
Mark item and all it's children to be stored as project overrides. Mark item and all it's children to be stored as project overrides.
""" """
@ -794,7 +794,7 @@ class BaseItemEntity(BaseEntity):
"""Item's implementation to remove project overrides. """Item's implementation to remove project overrides.
Mark item as does not have project overrides. Must not change Mark item as does not have project overrides. Must not change
`was_overriden` attribute value. `was_overridden` attribute value.
Args: Args:
on_change_trigger (list): Callbacks of `on_change` should be stored on_change_trigger (list): Callbacks of `on_change` should be stored

View file

@ -6,7 +6,7 @@ from .lib import (
) )
from openpype.settings.constants import ( from openpype.settings.constants import (
METADATA_KEYS, METADATA_KEYS,
M_OVERRIDEN_KEY, M_OVERRIDDEN_KEY,
KEY_REGEX KEY_REGEX
) )
from . import ( from . import (
@ -119,7 +119,7 @@ class DictConditionalEntity(ItemEntity):
# `current_metadata` are still when schema is loaded # `current_metadata` are still when schema is loaded
# - only metadata stored with dict item are gorup overrides in # - only metadata stored with dict item are gorup overrides in
# M_OVERRIDEN_KEY # M_OVERRIDDEN_KEY
self._current_metadata = {} self._current_metadata = {}
self._metadata_are_modified = False self._metadata_are_modified = False
@ -377,9 +377,9 @@ class DictConditionalEntity(ItemEntity):
): ):
continue continue
if M_OVERRIDEN_KEY not in current_metadata: if M_OVERRIDDEN_KEY not in current_metadata:
current_metadata[M_OVERRIDEN_KEY] = [] current_metadata[M_OVERRIDDEN_KEY] = []
current_metadata[M_OVERRIDEN_KEY].append(key) current_metadata[M_OVERRIDDEN_KEY].append(key)
# Define if current metadata are avaialble for current override state # Define if current metadata are avaialble for current override state
metadata = NOT_SET metadata = NOT_SET
@ -535,7 +535,7 @@ class DictConditionalEntity(ItemEntity):
enum_value = value.get(self.enum_key) enum_value = value.get(self.enum_key)
old_metadata = metadata.get(M_OVERRIDEN_KEY) old_metadata = metadata.get(M_OVERRIDDEN_KEY)
if old_metadata: if old_metadata:
old_metadata_set = set(old_metadata) old_metadata_set = set(old_metadata)
new_metadata = [] new_metadata = []
@ -547,7 +547,7 @@ class DictConditionalEntity(ItemEntity):
for key in old_metadata_set: for key in old_metadata_set:
new_metadata.append(key) new_metadata.append(key)
metadata[M_OVERRIDEN_KEY] = new_metadata metadata[M_OVERRIDDEN_KEY] = new_metadata
return value, metadata return value, metadata

View file

@ -9,7 +9,7 @@ from .lib import (
) )
from openpype.settings.constants import ( from openpype.settings.constants import (
METADATA_KEYS, METADATA_KEYS,
M_OVERRIDEN_KEY, M_OVERRIDDEN_KEY,
KEY_REGEX KEY_REGEX
) )
from . import ( from . import (
@ -183,7 +183,7 @@ class DictImmutableKeysEntity(ItemEntity):
# `current_metadata` are still when schema is loaded # `current_metadata` are still when schema is loaded
# - only metadata stored with dict item are gorup overrides in # - only metadata stored with dict item are gorup overrides in
# M_OVERRIDEN_KEY # M_OVERRIDDEN_KEY
self._current_metadata = {} self._current_metadata = {}
self._metadata_are_modified = False self._metadata_are_modified = False
@ -257,9 +257,9 @@ class DictImmutableKeysEntity(ItemEntity):
): ):
continue continue
if M_OVERRIDEN_KEY not in current_metadata: if M_OVERRIDDEN_KEY not in current_metadata:
current_metadata[M_OVERRIDEN_KEY] = [] current_metadata[M_OVERRIDDEN_KEY] = []
current_metadata[M_OVERRIDEN_KEY].append(key) current_metadata[M_OVERRIDDEN_KEY].append(key)
# Define if current metadata are avaialble for current override state # Define if current metadata are avaialble for current override state
metadata = NOT_SET metadata = NOT_SET
@ -399,7 +399,7 @@ class DictImmutableKeysEntity(ItemEntity):
if key in value: if key in value:
metadata[key] = value.pop(key) metadata[key] = value.pop(key)
old_metadata = metadata.get(M_OVERRIDEN_KEY) old_metadata = metadata.get(M_OVERRIDDEN_KEY)
if old_metadata: if old_metadata:
old_metadata_set = set(old_metadata) old_metadata_set = set(old_metadata)
new_metadata = [] new_metadata = []
@ -410,7 +410,7 @@ class DictImmutableKeysEntity(ItemEntity):
for key in old_metadata_set: for key in old_metadata_set:
new_metadata.append(key) new_metadata.append(key)
metadata[M_OVERRIDEN_KEY] = new_metadata metadata[M_OVERRIDDEN_KEY] = new_metadata
return value, metadata return value, metadata

View file

@ -222,7 +222,7 @@ class DictMutableKeysEntity(EndpointEntity):
self.required_keys = self.schema_data.get("required_keys") or [] self.required_keys = self.schema_data.get("required_keys") or []
self.collapsible_key = self.schema_data.get("collapsible_key") or False self.collapsible_key = self.schema_data.get("collapsible_key") or False
# GUI attributes # GUI attributes
self.hightlight_content = ( self.highlight_content = (
self.schema_data.get("highlight_content") or False self.schema_data.get("highlight_content") or False
) )

View file

@ -121,6 +121,20 @@ class EnumEntity(BaseEnumEntity):
) )
super(EnumEntity, self).schema_validations() super(EnumEntity, self).schema_validations()
def set_override_state(self, *args, **kwargs):
super(EnumEntity, self).set_override_state(*args, **kwargs)
# Make sure current value is valid
if self.multiselection:
new_value = []
for key in self._current_value:
if key in self.valid_keys:
new_value.append(key)
self._current_value = new_value
elif self._current_value not in self.valid_keys:
self._current_value = self.value_on_not_set
class HostsEnumEntity(BaseEnumEntity): class HostsEnumEntity(BaseEnumEntity):
"""Enumeration of host names. """Enumeration of host names.

View file

@ -101,7 +101,7 @@ class OverrideState:
- DEFAULTS - Entity cares only about default values. It is not - DEFAULTS - Entity cares only about default values. It is not
possible to set higher state if any entity does not have filled possible to set higher state if any entity does not have filled
default value. default value.
- STUDIO - First layer of overrides. Hold only studio overriden values - STUDIO - First layer of overrides. Hold only studio overridden values
that are applied on top of defaults. that are applied on top of defaults.
- PROJECT - Second layer of overrides. Hold only project overrides that are - PROJECT - Second layer of overrides. Hold only project overrides that are
applied on top of defaults and studio overrides. applied on top of defaults and studio overrides.

View file

@ -10,7 +10,7 @@
- `"is_file"` - this key is for storing openpype defaults in `openpype` repo - `"is_file"` - this key is for storing openpype defaults in `openpype` repo
- reasons of existence: developing new schemas does not require to create defaults manually - reasons of existence: developing new schemas does not require to create defaults manually
- key is validated, must be once in hierarchy else it won't be possible to store openpype defaults - key is validated, must be once in hierarchy else it won't be possible to store openpype defaults
- `"is_group"` - define that all values under key in hierarchy will be overriden if any value is modified, this information is also stored to overrides - `"is_group"` - define that all values under key in hierarchy will be overridden if any value is modified, this information is also stored to overrides
- this keys is not allowed for all inputs as they may have not reason for that - this keys is not allowed for all inputs as they may have not reason for that
- key is validated, can be only once in hierarchy but is not required - key is validated, can be only once in hierarchy but is not required
- currently there are `system settings` and `project settings` - currently there are `system settings` and `project settings`
@ -767,7 +767,7 @@ Anatomy represents data stored on project document.
### anatomy ### anatomy
- entity works similarly to `dict` - entity works similarly to `dict`
- anatomy has always all keys overriden with overrides - anatomy has always all keys overridden with overrides
- overrides are not applied as all anatomy data must be available from project document - overrides are not applied as all anatomy data must be available from project document
- all children must be groups - all children must be groups

View file

@ -18,6 +18,27 @@
} }
] ]
}, },
{
"type": "dict",
"collapsible": true,
"key": "CollectSceneVersion",
"label": "Collect Version from Workfile",
"is_group": true,
"children": [
{
"key": "hosts",
"label": "Host names",
"type": "hosts-enum",
"multiselection": true
},
{
"key": "skip_hosts_headless_publish",
"label": "Skip for host if headless publish",
"type": "hosts-enum",
"multiselection": true
}
]
},
{ {
"type": "dict", "type": "dict",
"collapsible": true, "collapsible": true,

View file

@ -154,6 +154,33 @@
] ]
}, },
{
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"key": "ValidateCycleError",
"label": "Validate Cycle Error",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "optional",
"label": "Optional"
},
{
"key": "families",
"label": "Families",
"type": "list",
"object_type": "text"
}
]
},
{ {
"type": "dict", "type": "dict",
"collapsible": true, "collapsible": true,

View file

@ -11,13 +11,13 @@
}, },
{ {
"type": "dict-conditional", "type": "dict-conditional",
"key": "overriden_value", "key": "overridden_value",
"label": "Overriden value", "label": "Overridden value",
"enum_key": "overriden", "enum_key": "overridden",
"enum_is_horizontal": true, "enum_is_horizontal": true,
"enum_children": [ "enum_children": [
{ {
"key": "overriden", "key": "overridden",
"label": "Override value", "label": "Override value",
"children": [ "children": [
{ {

View file

@ -14,7 +14,7 @@ from .constants import (
PROJECT_SETTINGS_KEY, PROJECT_SETTINGS_KEY,
PROJECT_ANATOMY_KEY, PROJECT_ANATOMY_KEY,
LOCAL_SETTING_KEY, LOCAL_SETTING_KEY,
M_OVERRIDEN_KEY, M_OVERRIDDEN_KEY,
LEGACY_SETTINGS_VERSION LEGACY_SETTINGS_VERSION
) )
@ -417,12 +417,12 @@ class MongoSettingsHandler(SettingsHandler):
continue continue
# Pop key from values # Pop key from values
output[key] = general_data.pop(key) output[key] = general_data.pop(key)
# Pop key from overriden metadata # Pop key from overridden metadata
if ( if (
M_OVERRIDEN_KEY in general_data M_OVERRIDDEN_KEY in general_data
and key in general_data[M_OVERRIDEN_KEY] and key in general_data[M_OVERRIDDEN_KEY]
): ):
general_data[M_OVERRIDEN_KEY].remove(key) general_data[M_OVERRIDDEN_KEY].remove(key)
return output return output
def _apply_global_settings( def _apply_global_settings(
@ -482,17 +482,17 @@ class MongoSettingsHandler(SettingsHandler):
system_general = {} system_general = {}
system_settings_data["general"] = system_general system_settings_data["general"] = system_general
overriden_keys = system_general.get(M_OVERRIDEN_KEY) or [] overridden_keys = system_general.get(M_OVERRIDDEN_KEY) or []
for key in self.global_general_keys: for key in self.global_general_keys:
if key not in globals_data: if key not in globals_data:
continue continue
system_general[key] = globals_data[key] system_general[key] = globals_data[key]
if key not in overriden_keys: if key not in overridden_keys:
overriden_keys.append(key) overridden_keys.append(key)
if overriden_keys: if overridden_keys:
system_general[M_OVERRIDEN_KEY] = overriden_keys system_general[M_OVERRIDDEN_KEY] = overridden_keys
return system_settings_document return system_settings_document

View file

@ -8,7 +8,7 @@ from .exceptions import (
SaveWarningExc SaveWarningExc
) )
from .constants import ( from .constants import (
M_OVERRIDEN_KEY, M_OVERRIDDEN_KEY,
M_ENVIRONMENT_KEY, M_ENVIRONMENT_KEY,
METADATA_KEYS, METADATA_KEYS,
@ -671,13 +671,13 @@ def subkey_merge(_dict, value, keys):
def merge_overrides(source_dict, override_dict): def merge_overrides(source_dict, override_dict):
"""Merge data from override_dict to source_dict.""" """Merge data from override_dict to source_dict."""
if M_OVERRIDEN_KEY in override_dict: if M_OVERRIDDEN_KEY in override_dict:
overriden_keys = set(override_dict.pop(M_OVERRIDEN_KEY)) overridden_keys = set(override_dict.pop(M_OVERRIDDEN_KEY))
else: else:
overriden_keys = set() overridden_keys = set()
for key, value in override_dict.items(): for key, value in override_dict.items():
if (key in overriden_keys or key not in source_dict): if (key in overridden_keys or key not in source_dict):
source_dict[key] = value source_dict[key] = value
elif isinstance(value, dict) and isinstance(source_dict[key], dict): elif isinstance(value, dict) and isinstance(source_dict[key], dict):
@ -699,7 +699,7 @@ def apply_local_settings_on_system_settings(system_settings, local_settings):
"""Apply local settings on studio system settings. """Apply local settings on studio system settings.
ATM local settings can modify only application executables. Executable ATM local settings can modify only application executables. Executable
values are not overriden but prepended. values are not overridden but prepended.
""" """
if not local_settings or "applications" not in local_settings: if not local_settings or "applications" not in local_settings:
return return
@ -1039,7 +1039,7 @@ def get_environments():
"""Calculated environment based on defaults and system settings. """Calculated environment based on defaults and system settings.
Any default environment also found in the system settings will be fully Any default environment also found in the system settings will be fully
overriden by the one from the system settings. overridden by the one from the system settings.
Returns: Returns:
dict: Output should be ready for `acre` module. dict: Output should be ready for `acre` module.

View file

@ -112,7 +112,7 @@
"breadcrumbs-btn-bg": "rgba(127, 127, 127, 60)", "breadcrumbs-btn-bg": "rgba(127, 127, 127, 60)",
"breadcrumbs-btn-bg-hover": "rgba(127, 127, 127, 90)", "breadcrumbs-btn-bg-hover": "rgba(127, 127, 127, 90)",
"content-hightlighted": "rgba(19, 26, 32, 15)", "content-highlighted": "rgba(19, 26, 32, 15)",
"focus-border": "#839caf", "focus-border": "#839caf",
"image-btn": "#bfccd6", "image-btn": "#bfccd6",
"image-btn-hover": "#189aea", "image-btn-hover": "#189aea",

View file

@ -1093,16 +1093,16 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
#ExpandLabel[state="modified"]:hover, #SettingsLabel[state="modified"]:hover { #ExpandLabel[state="modified"]:hover, #SettingsLabel[state="modified"]:hover {
color: {color:settings:modified-light}; color: {color:settings:modified-light};
} }
#ExpandLabel[state="overriden-modified"], #SettingsLabel[state="overriden-modified"] { #ExpandLabel[state="overridden-modified"], #SettingsLabel[state="overridden-modified"] {
color: {color:settings:modified-mid}; color: {color:settings:modified-mid};
} }
#ExpandLabel[state="overriden-modified"]:hover, #SettingsLabel[state="overriden-modified"]:hover { #ExpandLabel[state="overridden-modified"]:hover, #SettingsLabel[state="overridden-modified"]:hover {
color: {color:settings:modified-light}; color: {color:settings:modified-light};
} }
#ExpandLabel[state="overriden"], #SettingsLabel[state="overriden"] { #ExpandLabel[state="overridden"], #SettingsLabel[state="overridden"] {
color: {color:settings:project-mid}; color: {color:settings:project-mid};
} }
#ExpandLabel[state="overriden"]:hover, #SettingsLabel[state="overriden"]:hover { #ExpandLabel[state="overridden"]:hover, #SettingsLabel[state="overridden"]:hover {
color: {color:settings:project-light}; color: {color:settings:project-light};
} }
#ExpandLabel[state="invalid"], #SettingsLabel[state="invalid"] { #ExpandLabel[state="invalid"], #SettingsLabel[state="invalid"] {
@ -1130,10 +1130,10 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
#SettingsMainWidget QWidget[input-state="modified"] { #SettingsMainWidget QWidget[input-state="modified"] {
border-color: {color:settings:modified-mid}; border-color: {color:settings:modified-mid};
} }
#SettingsMainWidget QWidget[input-state="overriden-modified"] { #SettingsMainWidget QWidget[input-state="overridden-modified"] {
border-color: {color:settings:modified-mid}; border-color: {color:settings:modified-mid};
} }
#SettingsMainWidget QWidget[input-state="overriden"] { #SettingsMainWidget QWidget[input-state="overridden"] {
border-color: {color:settings:project-mid}; border-color: {color:settings:project-mid};
} }
#SettingsMainWidget QWidget[input-state="invalid"] { #SettingsMainWidget QWidget[input-state="invalid"] {
@ -1159,8 +1159,8 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
#ContentWidget { #ContentWidget {
background-color: transparent; background-color: transparent;
} }
#ContentWidget[content_state="hightlighted"] { #ContentWidget[content_state="highlighted"] {
background-color: {color:settings:content-hightlighted}; background-color: {color:settings:content-highlighted};
} }
#SideLineWidget { #SideLineWidget {
@ -1186,11 +1186,11 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
#SideLineWidget[state="child-invalid"] {border-color: {color:settings:invalid-dark};} #SideLineWidget[state="child-invalid"] {border-color: {color:settings:invalid-dark};}
#SideLineWidget[state="child-invalid"]:hover {border-color: {color:settings:invalid-light};} #SideLineWidget[state="child-invalid"]:hover {border-color: {color:settings:invalid-light};}
#SideLineWidget[state="child-overriden"] {border-color: {color:settings:project-dark};} #SideLineWidget[state="child-overridden"] {border-color: {color:settings:project-dark};}
#SideLineWidget[state="child-overriden"]:hover {border-color: {color:settings:project-mid};} #SideLineWidget[state="child-overridden"]:hover {border-color: {color:settings:project-mid};}
#SideLineWidget[state="child-overriden-modified"] {border-color: {color:settings:modified-dark};} #SideLineWidget[state="child-overridden-modified"] {border-color: {color:settings:modified-dark};}
#SideLineWidget[state="child-overriden-modified"]:hover {border-color: {color:settings:modified-mid};} #SideLineWidget[state="child-overridden-modified"]:hover {border-color: {color:settings:modified-mid};}
#DictAsWidgetBody { #DictAsWidgetBody {
background: transparent; background: transparent;

View file

@ -32,9 +32,9 @@ def test_avalon_plugin_presets(monkeypatch, printer):
assert MyTestCreator in plugins assert MyTestCreator in plugins
for p in plugins: for p in plugins:
if p.__name__ == "MyTestCreator": if p.__name__ == "MyTestCreator":
printer("Test if we have overriden existing property") printer("Test if we have overridden existing property")
assert p.my_test_property == "B" assert p.my_test_property == "B"
printer("Test if we have overriden superclass property") printer("Test if we have overridden superclass property")
assert p.active is False assert p.active is False
printer("Test if we have added new property") printer("Test if we have added new property")
assert p.new_property == "new" assert p.new_property == "new"

View file

@ -87,7 +87,7 @@ class Window(QtWidgets.QDialog):
btn_layout = QtWidgets.QHBoxLayout(btns_widget) btn_layout = QtWidgets.QHBoxLayout(btns_widget)
btn_create_asset = QtWidgets.QPushButton("Create asset") btn_create_asset = QtWidgets.QPushButton("Create asset")
btn_create_asset.setToolTip( btn_create_asset.setToolTip(
"Creates all neccessary components for asset" "Creates all necessary components for asset"
) )
checkbox_app = None checkbox_app = None
if self.context is not None: if self.context is not None:
@ -231,7 +231,7 @@ class Window(QtWidgets.QDialog):
test_name = name.replace(' ', '') test_name = name.replace(' ', '')
error_message = None error_message = None
message = QtWidgets.QMessageBox(self) message = QtWidgets.QMessageBox(self)
message.setWindowTitle("Some errors has occured") message.setWindowTitle("Some errors have occurred")
message.setIcon(QtWidgets.QMessageBox.Critical) message.setIcon(QtWidgets.QMessageBox.Critical)
# TODO: show error messages on any error # TODO: show error messages on any error
if self.valid_parent is not True and test_name == '': if self.valid_parent is not True and test_name == '':

View file

@ -44,7 +44,7 @@ def preserve_expanded_rows(tree_view,
This function is created to maintain the expand vs collapse status of This function is created to maintain the expand vs collapse status of
the model items. When refresh is triggered the items which are expanded the model items. When refresh is triggered the items which are expanded
will stay expanded and vise versa. will stay expanded and vice versa.
Arguments: Arguments:
tree_view (QWidgets.QTreeView): the tree view which is tree_view (QWidgets.QTreeView): the tree view which is
@ -94,7 +94,7 @@ def preserve_selection(tree_view,
This function is created to maintain the selection status of This function is created to maintain the selection status of
the model items. When refresh is triggered the items which are expanded the model items. When refresh is triggered the items which are expanded
will stay expanded and vise versa. will stay expanded and vice versa.
tree_view (QWidgets.QTreeView): the tree view nested in the application tree_view (QWidgets.QTreeView): the tree view nested in the application
column (int): the column to retrieve the data from column (int): the column to retrieve the data from
@ -179,7 +179,7 @@ class AssetModel(TreeModel):
""" """
if silos: if silos:
# WARNING: Silo item "_id" is set to silo value # WARNING: Silo item "_id" is set to silo value
# mainly because GUI issue with perserve selection and expanded row # mainly because GUI issue with preserve selection and expanded row
# and because of easier hierarchy parenting (in "assets") # and because of easier hierarchy parenting (in "assets")
for silo in silos: for silo in silos:
item = Item({ item = Item({

View file

@ -46,7 +46,7 @@ class ContextDialog(QtWidgets.QDialog):
# UI initialization # UI initialization
main_splitter = QtWidgets.QSplitter(self) main_splitter = QtWidgets.QSplitter(self)
# Left side widget containt project combobox and asset widget # Left side widget contains project combobox and asset widget
left_side_widget = QtWidgets.QWidget(main_splitter) left_side_widget = QtWidgets.QWidget(main_splitter)
project_combobox = QtWidgets.QComboBox(left_side_widget) project_combobox = QtWidgets.QComboBox(left_side_widget)

View file

@ -354,7 +354,7 @@ class CreatorWindow(QtWidgets.QDialog):
Override keyPressEvent to do nothing so that Maya's panels won't Override keyPressEvent to do nothing so that Maya's panels won't
take focus when pressing "SHIFT" whilst mouse is over viewport or take focus when pressing "SHIFT" whilst mouse is over viewport or
outliner. This way users don't accidently perform Maya commands outliner. This way users don't accidentally perform Maya commands
whilst trying to name an instance. whilst trying to name an instance.
""" """

View file

@ -107,7 +107,7 @@ class ExperimentalToolsDialog(QtWidgets.QDialog):
# Is dialog first shown # Is dialog first shown
self._first_show = True self._first_show = True
# Trigger refresh when window get's activity # Trigger refresh when window gets activity
self._refresh_on_active = True self._refresh_on_active = True
# Is window active # Is window active
self._window_is_active = False self._window_is_active = False

View file

@ -43,7 +43,7 @@ class ExperimentalTool:
self._enabled = enabled self._enabled = enabled
def execute(self): def execute(self):
"""Trigger registerd callback.""" """Trigger registered callback."""
self.callback() self.callback()

View file

@ -62,6 +62,7 @@ class ApplicationAction(api.Action):
icon = None icon = None
color = None color = None
order = 0 order = 0
data = {}
_log = None _log = None
required_session_keys = ( required_session_keys = (
@ -103,7 +104,8 @@ class ApplicationAction(api.Action):
self.application.launch( self.application.launch(
project_name=project_name, project_name=project_name,
asset_name=asset_name, asset_name=asset_name,
task_name=task_name task_name=task_name,
**self.data
) )
except ApplictionExecutableNotFound as exc: except ApplictionExecutableNotFound as exc:

View file

@ -7,6 +7,8 @@ VARIANT_GROUP_ROLE = QtCore.Qt.UserRole + 2
ACTION_ID_ROLE = QtCore.Qt.UserRole + 3 ACTION_ID_ROLE = QtCore.Qt.UserRole + 3
ANIMATION_START_ROLE = QtCore.Qt.UserRole + 4 ANIMATION_START_ROLE = QtCore.Qt.UserRole + 4
ANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 5 ANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 5
FORCE_NOT_OPEN_WORKFILE_ROLE = QtCore.Qt.UserRole + 6
ACTION_TOOLTIP_ROLE = QtCore.Qt.UserRole + 7
# Animation length in seconds # Animation length in seconds
ANIMATION_LEN = 7 ANIMATION_LEN = 7

View file

@ -2,7 +2,8 @@ import time
from Qt import QtCore, QtWidgets, QtGui from Qt import QtCore, QtWidgets, QtGui
from .constants import ( from .constants import (
ANIMATION_START_ROLE, ANIMATION_START_ROLE,
ANIMATION_STATE_ROLE ANIMATION_STATE_ROLE,
FORCE_NOT_OPEN_WORKFILE_ROLE
) )
@ -69,6 +70,16 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate):
self._draw_animation(painter, option, index) self._draw_animation(painter, option, index)
super(ActionDelegate, self).paint(painter, option, index) super(ActionDelegate, self).paint(painter, option, index)
if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE):
rect = QtCore.QRectF(option.rect.x(), option.rect.height(),
5, 5)
painter.setPen(QtCore.Qt.transparent)
painter.setBrush(QtGui.QColor(200, 0, 0))
painter.drawEllipse(rect)
painter.setBrush(self.extender_bg_brush)
is_group = False is_group = False
for group_role in self.group_roles: for group_role in self.group_roles:
is_group = index.data(group_role) is_group = index.data(group_role)

View file

@ -29,7 +29,7 @@ class ProjectHandler(QtCore.QObject):
Helps to organize two separate widgets handling current project selection. Helps to organize two separate widgets handling current project selection.
It is easier to trigger project change callbacks from one place than from It is easier to trigger project change callbacks from one place than from
multiple differect places without proper handling or sequence changes. multiple different places without proper handling or sequence changes.
Args: Args:
dbcon(AvalonMongoDB): Mongo connection with Session. dbcon(AvalonMongoDB): Mongo connection with Session.
@ -42,7 +42,7 @@ class ProjectHandler(QtCore.QObject):
# that may require reshing of projects # that may require reshing of projects
refresh_interval = 10000 refresh_interval = 10000
# Signal emmited when project has changed # Signal emitted when project has changed
project_changed = QtCore.Signal(str) project_changed = QtCore.Signal(str)
projects_refreshed = QtCore.Signal() projects_refreshed = QtCore.Signal()
timer_timeout = QtCore.Signal() timer_timeout = QtCore.Signal()

View file

@ -2,19 +2,21 @@ import uuid
import copy import copy
import logging import logging
import collections import collections
import appdirs
from . import lib from . import lib
from .constants import ( from .constants import (
ACTION_ROLE, ACTION_ROLE,
GROUP_ROLE, GROUP_ROLE,
VARIANT_GROUP_ROLE, VARIANT_GROUP_ROLE,
ACTION_ID_ROLE ACTION_ID_ROLE,
FORCE_NOT_OPEN_WORKFILE_ROLE
) )
from .actions import ApplicationAction from .actions import ApplicationAction
from Qt import QtCore, QtGui from Qt import QtCore, QtGui
from avalon.vendor import qtawesome from avalon.vendor import qtawesome
from avalon import style, api from avalon import style, api
from openpype.lib import ApplicationManager from openpype.lib import ApplicationManager, JSONSettingRegistry
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -30,6 +32,13 @@ class ActionModel(QtGui.QStandardItemModel):
# Cache of available actions # Cache of available actions
self._registered_actions = list() self._registered_actions = list()
self.items_by_id = {} self.items_by_id = {}
path = appdirs.user_data_dir("openpype", "pypeclub")
self.launcher_registry = JSONSettingRegistry("launcher", path)
try:
_ = self.launcher_registry.get_item("force_not_open_workfile")
except ValueError:
self.launcher_registry.set_item("force_not_open_workfile", [])
def discover(self): def discover(self):
"""Set up Actions cache. Run this for each new project.""" """Set up Actions cache. Run this for each new project."""
@ -75,7 +84,8 @@ class ActionModel(QtGui.QStandardItemModel):
"group": None, "group": None,
"icon": app.icon, "icon": app.icon,
"color": getattr(app, "color", None), "color": getattr(app, "color", None),
"order": getattr(app, "order", None) or 0 "order": getattr(app, "order", None) or 0,
"data": {}
} }
) )
@ -102,7 +112,7 @@ class ActionModel(QtGui.QStandardItemModel):
# Groups # Groups
group_name = getattr(action, "group", None) group_name = getattr(action, "group", None)
# Lable variants # Label variants
label = getattr(action, "label", None) label = getattr(action, "label", None)
label_variant = getattr(action, "label_variant", None) label_variant = getattr(action, "label_variant", None)
if label_variant and not label: if label_variant and not label:
@ -179,11 +189,17 @@ class ActionModel(QtGui.QStandardItemModel):
self.beginResetModel() self.beginResetModel()
stored = self.launcher_registry.get_item("force_not_open_workfile")
items = [] items = []
for order in sorted(items_by_order.keys()): for order in sorted(items_by_order.keys()):
for item in items_by_order[order]: for item in items_by_order[order]:
item_id = str(uuid.uuid4()) item_id = str(uuid.uuid4())
item.setData(item_id, ACTION_ID_ROLE) item.setData(item_id, ACTION_ID_ROLE)
if self.is_force_not_open_workfile(item,
stored):
self.change_action_item(item, True)
self.items_by_id[item_id] = item self.items_by_id[item_id] = item
items.append(item) items.append(item)
@ -222,6 +238,90 @@ class ActionModel(QtGui.QStandardItemModel):
key=lambda action: (action.order, action.name) key=lambda action: (action.order, action.name)
) )
def update_force_not_open_workfile_settings(self, is_checked, action_id):
"""Store/remove config for forcing to skip opening last workfile.
Args:
is_checked (bool): True to add, False to remove
action_id (str)
"""
action_item = self.items_by_id.get(action_id)
if not action_item:
return
action = action_item.data(ACTION_ROLE)
actual_data = self._prepare_compare_data(action)
stored = self.launcher_registry.get_item("force_not_open_workfile")
if is_checked:
stored.append(actual_data)
else:
final_values = []
for config in stored:
if config != actual_data:
final_values.append(config)
stored = final_values
self.launcher_registry.set_item("force_not_open_workfile", stored)
self.launcher_registry._get_item.cache_clear()
self.change_action_item(action_item, is_checked)
def change_action_item(self, item, checked):
"""Modifies tooltip and sets if opening of last workfile forbidden"""
tooltip = item.data(QtCore.Qt.ToolTipRole)
if checked:
tooltip += " (Not opening last workfile)"
item.setData(tooltip, QtCore.Qt.ToolTipRole)
item.setData(checked, FORCE_NOT_OPEN_WORKFILE_ROLE)
def is_application_action(self, action):
"""Checks if item is of a ApplicationAction type
Args:
action (action)
"""
if isinstance(action, list) and action:
action = action[0]
return ApplicationAction in action.__bases__
def is_force_not_open_workfile(self, item, stored):
"""Checks if application for task is marked to not open workfile
There might be specific tasks where is unwanted to open workfile right
always (broken file, low performance). This allows artist to mark to
skip opening for combination (project, asset, task_name, app)
Args:
item (QStandardItem)
stored (list) of dict
"""
action = item.data(ACTION_ROLE)
if not self.is_application_action(action):
return False
actual_data = self._prepare_compare_data(action)
for config in stored:
if config == actual_data:
return True
return False
def _prepare_compare_data(self, action):
if isinstance(action, list) and action:
action = action[0]
compare_data = {}
if action:
compare_data = {
"app_label": action.label.lower(),
"project_name": self.dbcon.Session["AVALON_PROJECT"],
"asset": self.dbcon.Session["AVALON_ASSET"],
"task_name": self.dbcon.Session["AVALON_TASK"]
}
return compare_data
class ProjectModel(QtGui.QStandardItemModel): class ProjectModel(QtGui.QStandardItemModel):
"""List of projects""" """List of projects"""

View file

@ -15,7 +15,8 @@ from .constants import (
ACTION_ID_ROLE, ACTION_ID_ROLE,
ANIMATION_START_ROLE, ANIMATION_START_ROLE,
ANIMATION_STATE_ROLE, ANIMATION_STATE_ROLE,
ANIMATION_LEN ANIMATION_LEN,
FORCE_NOT_OPEN_WORKFILE_ROLE
) )
@ -96,6 +97,7 @@ class ActionBar(QtWidgets.QWidget):
view.setViewMode(QtWidgets.QListView.IconMode) view.setViewMode(QtWidgets.QListView.IconMode)
view.setResizeMode(QtWidgets.QListView.Adjust) view.setResizeMode(QtWidgets.QListView.Adjust)
view.setSelectionMode(QtWidgets.QListView.NoSelection) view.setSelectionMode(QtWidgets.QListView.NoSelection)
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
view.setEditTriggers(QtWidgets.QListView.NoEditTriggers) view.setEditTriggers(QtWidgets.QListView.NoEditTriggers)
view.setWrapping(True) view.setWrapping(True)
view.setGridSize(QtCore.QSize(70, 75)) view.setGridSize(QtCore.QSize(70, 75))
@ -135,8 +137,16 @@ class ActionBar(QtWidgets.QWidget):
project_handler.projects_refreshed.connect(self._on_projects_refresh) project_handler.projects_refreshed.connect(self._on_projects_refresh)
view.clicked.connect(self.on_clicked) view.clicked.connect(self.on_clicked)
view.customContextMenuRequested.connect(self.on_context_menu)
self._context_menu = None
self._discover_on_menu = False
def discover_actions(self): def discover_actions(self):
if self._context_menu is not None:
self._discover_on_menu = True
return
if self._animation_timer.isActive(): if self._animation_timer.isActive():
self._animation_timer.stop() self._animation_timer.stop()
self.model.discover() self.model.discover()
@ -171,7 +181,7 @@ class ActionBar(QtWidgets.QWidget):
self.update() self.update()
def _start_animation(self, index): def _start_animation(self, index):
# Offset refresh timout # Offset refresh timeout
self.project_handler.start_timer() self.project_handler.start_timer()
action_id = index.data(ACTION_ID_ROLE) action_id = index.data(ACTION_ID_ROLE)
item = self.model.items_by_id.get(action_id) item = self.model.items_by_id.get(action_id)
@ -181,6 +191,46 @@ class ActionBar(QtWidgets.QWidget):
self._animated_items.add(action_id) self._animated_items.add(action_id)
self._animation_timer.start() self._animation_timer.start()
def on_context_menu(self, point):
"""Creates menu to force skip opening last workfile."""
index = self.view.indexAt(point)
if not index.isValid():
return
action_item = index.data(ACTION_ROLE)
if not self.model.is_application_action(action_item):
return
menu = QtWidgets.QMenu(self.view)
checkbox = QtWidgets.QCheckBox("Skip opening last workfile.",
menu)
if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE):
checkbox.setChecked(True)
action_id = index.data(ACTION_ID_ROLE)
checkbox.stateChanged.connect(
lambda: self.on_checkbox_changed(checkbox.isChecked(),
action_id))
action = QtWidgets.QWidgetAction(menu)
action.setDefaultWidget(checkbox)
menu.addAction(action)
self._context_menu = menu
global_point = self.mapToGlobal(point)
menu.exec_(global_point)
self._context_menu = None
if self._discover_on_menu:
self._discover_on_menu = False
self.discover_actions()
def on_checkbox_changed(self, is_checked, action_id):
self.model.update_force_not_open_workfile_settings(is_checked,
action_id)
self.view.update()
if self._context_menu is not None:
self._context_menu.close()
def on_clicked(self, index): def on_clicked(self, index):
if not index or not index.isValid(): if not index or not index.isValid():
return return
@ -189,11 +239,15 @@ class ActionBar(QtWidgets.QWidget):
is_variant_group = index.data(VARIANT_GROUP_ROLE) is_variant_group = index.data(VARIANT_GROUP_ROLE)
if not is_group and not is_variant_group: if not is_group and not is_variant_group:
action = index.data(ACTION_ROLE) action = index.data(ACTION_ROLE)
if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE):
action.data["start_last_workfile"] = False
else:
action.data.pop("start_last_workfile", None)
self._start_animation(index) self._start_animation(index)
self.action_clicked.emit(action) self.action_clicked.emit(action)
return return
# Offset refresh timout # Offset refresh timeout
self.project_handler.start_timer() self.project_handler.start_timer()
actions = index.data(ACTION_ROLE) actions = index.data(ACTION_ROLE)
@ -212,7 +266,7 @@ class ActionBar(QtWidgets.QWidget):
by_variant_label = collections.defaultdict(list) by_variant_label = collections.defaultdict(list)
orders = [] orders = []
for action in actions: for action in actions:
# Lable variants # Label variants
label = getattr(action, "label", None) label = getattr(action, "label", None)
label_variant = getattr(action, "label_variant", None) label_variant = getattr(action, "label_variant", None)
if label_variant and not label: if label_variant and not label:

View file

@ -27,7 +27,7 @@ module.window = None
# Register callback on task change # Register callback on task change
# - callback can't be defined in Window as it is weak reference callback # - callback can't be defined in Window as it is weak reference callback
# so `WeakSet` will remove it immidiatelly # so `WeakSet` will remove it immediately
def on_context_task_change(*args, **kwargs): def on_context_task_change(*args, **kwargs):
if module.window: if module.window:
module.window.on_context_task_change(*args, **kwargs) module.window.on_context_task_change(*args, **kwargs)
@ -455,7 +455,7 @@ class LoaderWindow(QtWidgets.QDialog):
shift_pressed = QtCore.Qt.ShiftModifier & modifiers shift_pressed = QtCore.Qt.ShiftModifier & modifiers
if shift_pressed: if shift_pressed:
print("Force quitted..") print("Force quit..")
self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
print("Good bye") print("Good bye")

View file

@ -46,7 +46,7 @@ def get_options(action, loader, parent, repre_contexts):
Args: Args:
action (OptionalAction) - action in menu action (OptionalAction) - action in menu
loader (cls of api.Loader) - not initilized yet loader (cls of api.Loader) - not initialized yet
parent (Qt element to parent dialog to) parent (Qt element to parent dialog to)
repre_contexts (list) of dict with full info about selected repres repre_contexts (list) of dict with full info about selected repres
Returns: Returns:

View file

@ -233,8 +233,8 @@ class LookOutliner(QtWidgets.QWidget):
list: list of dictionaries list: list of dictionaries
""" """
datas = [i.data(TreeModel.ItemRole) for i in self.view.get_indices()] items = [i.data(TreeModel.ItemRole) for i in self.view.get_indices()]
return [d for d in datas if d is not None] return [item for item in items if item is not None]
def right_mouse_menu(self, pos): def right_mouse_menu(self, pos):
"""Build RMB menu for look view""" """Build RMB menu for look view"""

View file

@ -124,12 +124,12 @@ class HierarchyModel(QtCore.QAbstractItemModel):
Main part of ProjectManager. Main part of ProjectManager.
Model should be able to load existing entities, create new, handle their Model should be able to load existing entities, create new, handle their
validations like name duplication and validate if is possible to save it's validations like name duplication and validate if is possible to save its
data. data.
Args: Args:
dbcon (AvalonMongoDB): Connection to MongoDB with set AVALON_PROJECT in dbcon (AvalonMongoDB): Connection to MongoDB with set AVALON_PROJECT in
it's Session to current project. its Session to current project.
""" """
# Definition of all possible columns with their labels in default order # Definition of all possible columns with their labels in default order
@ -799,7 +799,7 @@ class HierarchyModel(QtCore.QAbstractItemModel):
for row in range(parent_item.rowCount()): for row in range(parent_item.rowCount()):
child_item = parent_item.child(row) child_item = parent_item.child(row)
child_id = child_item.id child_id = child_item.id
# Not sure if this can happend # Not sure if this can happen
# TODO validate this line it seems dangerous as start/end # TODO validate this line it seems dangerous as start/end
# row is not changed # row is not changed
if child_id not in children: if child_id not in children:
@ -1902,7 +1902,7 @@ class AssetItem(BaseItem):
return self._data["name"] return self._data["name"]
def child_parents(self): def child_parents(self):
"""Chilren AssetItem can use this method to get it's parent names. """Children AssetItem can use this method to get it's parent names.
This is used for `data.parents` key on document. This is used for `data.parents` key on document.
""" """
@ -2006,7 +2006,7 @@ class AssetItem(BaseItem):
@classmethod @classmethod
def data_from_doc(cls, asset_doc): def data_from_doc(cls, asset_doc):
"""Convert asset document from Mongo to item data.""" """Convert asset document from Mongo to item data."""
# Minimum required data for cases that it is new AssetItem withoud doc # Minimum required data for cases that it is new AssetItem without doc
data = { data = {
"name": None, "name": None,
"type": "asset" "type": "asset"
@ -2253,7 +2253,7 @@ class TaskItem(BaseItem):
"""Item representing Task item on Asset document. """Item representing Task item on Asset document.
Always should be AssetItem children and never should have any other Always should be AssetItem children and never should have any other
childrens. children.
It's name value should be validated with it's parent which only knows if It's name value should be validated with it's parent which only knows if
has same name as other sibling under same parent. has same name as other sibling under same parent.

View file

@ -110,7 +110,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
elif event.type() == QtCore.QEvent.KeyPress: elif event.type() == QtCore.QEvent.KeyPress:
# TODO: handle QtCore.Qt.Key_Enter, Key_Return? # TODO: handle QtCore.Qt.Key_Enter, Key_Return?
if event.key() == QtCore.Qt.Key_Space: if event.key() == QtCore.Qt.Key_Space:
# toogle the current items check state # toggle the current items check state
if ( if (
index_flags & QtCore.Qt.ItemIsUserCheckable index_flags & QtCore.Qt.ItemIsUserCheckable
and index_flags & QtCore.Qt.ItemIsTristate and index_flags & QtCore.Qt.ItemIsTristate

View file

@ -555,7 +555,7 @@ class PublisherController:
self.create_context.reset_avalon_context() self.create_context.reset_avalon_context()
self._reset_plugins() self._reset_plugins()
# Publish part must be resetted after plugins # Publish part must be reset after plugins
self._reset_publish() self._reset_publish()
self._reset_instances() self._reset_instances()
@ -690,7 +690,7 @@ class PublisherController:
def remove_instances(self, instances): def remove_instances(self, instances):
"""""" """"""
# QUESTION Expect that instaces are really removed? In that case save # QUESTION Expect that instances are really removed? In that case save
# reset is not required and save changes too. # reset is not required and save changes too.
self.save_changes() self.save_changes()

View file

@ -51,7 +51,7 @@ class _HBottomLineWidget(QtWidgets.QWidget):
Corners may have curve set by radius (`set_radius`). Radius should expect Corners may have curve set by radius (`set_radius`). Radius should expect
height of widget. height of widget.
Bottom line is drawed at the bottom of widget. If radius is 0 then height Bottom line is drawn at the bottom of widget. If radius is 0 then height
of widget should be 1px. of widget should be 1px.
It is expected that parent widget will set height and radius. It is expected that parent widget will set height and radius.
@ -94,7 +94,7 @@ class _HTopCornerLineWidget(QtWidgets.QWidget):
or or
`````` ``````
Horizontal line is drawed in the middle of widget. Horizontal line is drawn in the middle of widget.
Widget represents left or right corner. Corner may have curve set by Widget represents left or right corner. Corner may have curve set by
radius (`set_radius`). Radius should expect height of widget (maximum half radius (`set_radius`). Radius should expect height of widget (maximum half
@ -225,7 +225,7 @@ class BorderedLabelWidget(QtWidgets.QFrame):
self._radius = radius self._radius = radius
side_width = 1 + radius side_width = 1 + radius
# Dont't use fixed width/height as that would set also set # Don't use fixed width/height as that would set also set
# the other size (When fixed width is set then is also set # the other size (When fixed width is set then is also set
# fixed height). # fixed height).
self._left_w.setMinimumWidth(side_width) self._left_w.setMinimumWidth(side_width)

View file

@ -388,7 +388,7 @@ class InstanceCardView(AbstractInstanceView):
def sizeHint(self): def sizeHint(self):
"""Modify sizeHint based on visibility of scroll bars.""" """Modify sizeHint based on visibility of scroll bars."""
# Calculate width hint by content widget and verticall scroll bar # Calculate width hint by content widget and vertical scroll bar
scroll_bar = self._scroll_area.verticalScrollBar() scroll_bar = self._scroll_area.verticalScrollBar()
width = ( width = (
self._content_widget.sizeHint().width() self._content_widget.sizeHint().width()

View file

@ -6,7 +6,7 @@ attribute on instance (Group defined by creator).
Each item can be enabled/disabled with their checkbox, whole group Each item can be enabled/disabled with their checkbox, whole group
can be enabled/disabled with checkbox on group or can be enabled/disabled with checkbox on group or
selection can be enabled disabled using checkbox or keyboard key presses: selection can be enabled disabled using checkbox or keyboard key presses:
- Space - change state of selection to oposite - Space - change state of selection to opposite
- Enter - enable selection - Enter - enable selection
- Backspace - disable selection - Backspace - disable selection
@ -589,7 +589,7 @@ class InstanceListView(AbstractInstanceView):
# - create new instance, update existing and remove not existing # - create new instance, update existing and remove not existing
for group_name, group_item in self._group_items.items(): for group_name, group_item in self._group_items.items():
# Instance items to remove # Instance items to remove
# - will contain all exising instance ids at the start # - will contain all existing instance ids at the start
# - instance ids may be removed when existing instances are checked # - instance ids may be removed when existing instances are checked
to_remove = set() to_remove = set()
# Mapping of existing instances under group item # Mapping of existing instances under group item
@ -659,7 +659,7 @@ class InstanceListView(AbstractInstanceView):
for instance_id in to_remove: for instance_id in to_remove:
idx_to_remove.append(existing_mapping[instance_id]) idx_to_remove.append(existing_mapping[instance_id])
# Remove them in reverse order to prevend row index changes # Remove them in reverse order to prevent row index changes
for idx in reversed(sorted(idx_to_remove)): for idx in reversed(sorted(idx_to_remove)):
group_item.removeRows(idx, 1) group_item.removeRows(idx, 1)

View file

@ -276,7 +276,7 @@ class VerticallScrollArea(QtWidgets.QScrollArea):
The biggest difference is that the scroll area has scroll bar on left side The biggest difference is that the scroll area has scroll bar on left side
and resize of content will also resize scrollarea itself. and resize of content will also resize scrollarea itself.
Resize if deffered by 100ms because at the moment of resize are not yet Resize if deferred by 100ms because at the moment of resize are not yet
propagated sizes and visibility of scroll bars. propagated sizes and visibility of scroll bars.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View file

@ -749,7 +749,7 @@ class TasksCombobox(QtWidgets.QComboBox):
self.value_changed.emit() self.value_changed.emit()
def set_text(self, text): def set_text(self, text):
"""Set context shown in combobox without chaning selected items.""" """Set context shown in combobox without changing selected items."""
if text == self._text: if text == self._text:
return return
@ -1000,7 +1000,7 @@ class VariantInputWidget(PlaceholderLineEdit):
self.value_changed.emit() self.value_changed.emit()
def reset_to_origin(self): def reset_to_origin(self):
"""Set origin value of selected instnaces.""" """Set origin value of selected instances."""
self.set_value(self._origin_value) self.set_value(self._origin_value)
def get_value(self): def get_value(self):
@ -1105,7 +1105,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
Subset name is or may be affected on context. Gives abiity to modify Subset name is or may be affected on context. Gives abiity to modify
context and subset name of instance. This change is not autopromoted but context and subset name of instance. This change is not autopromoted but
must be submited. must be submitted.
Warning: Until artist hit `Submit` changes must not be propagated to Warning: Until artist hit `Submit` changes must not be propagated to
instance data. instance data.
@ -1179,7 +1179,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
self.cancel_btn = cancel_btn self.cancel_btn = cancel_btn
def _on_submit(self): def _on_submit(self):
"""Commit changes for selected instnaces.""" """Commit changes for selected instances."""
variant_value = None variant_value = None
asset_name = None asset_name = None
task_name = None task_name = None
@ -1363,7 +1363,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
self._attr_def_id_to_instances = {} self._attr_def_id_to_instances = {}
self._attr_def_id_to_attr_def = {} self._attr_def_id_to_attr_def = {}
# To store content of scroll area to prevend garbage collection # To store content of scroll area to prevent garbage collection
self._content_widget = None self._content_widget = None
def set_instances_valid(self, valid): def set_instances_valid(self, valid):
@ -1375,7 +1375,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
self._content_widget.setEnabled(valid) self._content_widget.setEnabled(valid)
def set_current_instances(self, instances): def set_current_instances(self, instances):
"""Set current instances for which are attribute definitons shown.""" """Set current instances for which are attribute definitions shown."""
prev_content_widget = self._scroll_area.widget() prev_content_widget = self._scroll_area.widget()
if prev_content_widget: if prev_content_widget:
self._scroll_area.takeWidget() self._scroll_area.takeWidget()
@ -1461,7 +1461,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
self._attr_def_id_to_attr_def = {} self._attr_def_id_to_attr_def = {}
self._attr_def_id_to_plugin_name = {} self._attr_def_id_to_plugin_name = {}
# Store content of scroll area to prevend garbage collection # Store content of scroll area to prevent garbage collection
self._content_widget = None self._content_widget = None
def set_instances_valid(self, valid): def set_instances_valid(self, valid):
@ -1473,7 +1473,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
self._content_widget.setEnabled(valid) self._content_widget.setEnabled(valid)
def set_current_instances(self, instances, context_selected): def set_current_instances(self, instances, context_selected):
"""Set current instances for which are attribute definitons shown.""" """Set current instances for which are attribute definitions shown."""
prev_content_widget = self._scroll_area.widget() prev_content_widget = self._scroll_area.widget()
if prev_content_widget: if prev_content_widget:
self._scroll_area.takeWidget() self._scroll_area.takeWidget()

View file

@ -106,14 +106,14 @@ class Controller(QtCore.QObject):
# ??? Emitted for each process # ??? Emitted for each process
was_processed = QtCore.Signal(dict) was_processed = QtCore.Signal(dict)
# Emmited when reset # Emitted when reset
# - all data are reset (plugins, processing, pari yielder, etc.) # - all data are reset (plugins, processing, pari yielder, etc.)
was_reset = QtCore.Signal() was_reset = QtCore.Signal()
# Emmited when previous group changed # Emitted when previous group changed
passed_group = QtCore.Signal(object) passed_group = QtCore.Signal(object)
# Emmited when want to change state of instances # Emitted when want to change state of instances
switch_toggleability = QtCore.Signal(bool) switch_toggleability = QtCore.Signal(bool)
# On action finished # On action finished
@ -322,7 +322,7 @@ class Controller(QtCore.QObject):
try: try:
result = pyblish.plugin.process(plugin, self.context, instance) result = pyblish.plugin.process(plugin, self.context, instance)
# Make note of the order at which the # Make note of the order at which the
# potential error error occured. # potential error error occurred.
if result["error"] is not None: if result["error"] is not None:
self.processing["ordersWithError"].add(plugin.order) self.processing["ordersWithError"].add(plugin.order)
@ -564,7 +564,7 @@ class Controller(QtCore.QObject):
case must be taken to ensure there are no memory leaks. case must be taken to ensure there are no memory leaks.
Explicitly deleting objects shines a light on where objects Explicitly deleting objects shines a light on where objects
may still be referenced in the form of an error. No errors may still be referenced in the form of an error. No errors
means this was uneccesary, but that's ok. means this was unnecessary, but that's ok.
""" """
for instance in self.context: for instance in self.context:

View file

@ -218,7 +218,7 @@ class OrderGroups:
def sort_groups(_groups_dict): def sort_groups(_groups_dict):
sorted_dict = collections.OrderedDict() sorted_dict = collections.OrderedDict()
# make sure wont affect any dictionary as pointer # make sure won't affect any dictionary as pointer
groups_dict = copy.deepcopy(_groups_dict) groups_dict = copy.deepcopy(_groups_dict)
last_order = None last_order = None
if None in groups_dict: if None in groups_dict:

View file

@ -558,7 +558,7 @@ class SwitchAssetDialog(QtWidgets.QDialog):
repre_docs = io.find( repre_docs = io.find(
{ {
"type": "rerpesentation", "type": "representation",
"parent": subset_doc["_id"], "parent": subset_doc["_id"],
"name": {"$in": list(repre_names)} "name": {"$in": list(repre_names)}
}, },

View file

@ -125,7 +125,7 @@ class SceneInventoryWindow(QtWidgets.QDialog):
Override keyPressEvent to do nothing so that Maya's panels won't Override keyPressEvent to do nothing so that Maya's panels won't
take focus when pressing "SHIFT" whilst mouse is over viewport or take focus when pressing "SHIFT" whilst mouse is over viewport or
outliner. This way users don't accidently perform Maya commands outliner. This way users don't accidentally perform Maya commands
whilst trying to name an instance. whilst trying to name an instance.
""" """

View file

@ -5,7 +5,7 @@ LABEL_REMOVE_PROJECT = "Remove from project"
LABEL_ADD_PROJECT = "Add to project" LABEL_ADD_PROJECT = "Add to project"
LABEL_DISCARD_CHANGES = "Discard changes" LABEL_DISCARD_CHANGES = "Discard changes"
# Local setting contants # Local setting constants
# TODO move to settings constants # TODO move to settings constants
LOCAL_GENERAL_KEY = "general" LOCAL_GENERAL_KEY = "general"
LOCAL_PROJECTS_KEY = "projects" LOCAL_PROJECTS_KEY = "projects"

View file

@ -126,7 +126,7 @@ class DynamicInputItem(QtCore.QObject):
return "studio" return "studio"
else: else:
if current_value: if current_value:
return "overriden" return "overridden"
if self.value_item.default_value: if self.value_item.default_value:
return "studio" return "studio"
@ -512,7 +512,7 @@ class _SiteCombobox(QtWidgets.QWidget):
return "studio" return "studio"
else: else:
if current_value: if current_value:
return "overriden" return "overridden"
studio_value = self._get_local_settings_item(DEFAULT_PROJECT_KEY) studio_value = self._get_local_settings_item(DEFAULT_PROJECT_KEY)
if studio_value: if studio_value:

View file

@ -222,7 +222,7 @@ class LocalSettingsWindow(QtWidgets.QWidget):
# Do not create local settings widget in init phase as it's using # Do not create local settings widget in init phase as it's using
# settings objects that must be OK to be able create this widget # settings objects that must be OK to be able create this widget
# - we want to show dialog if anything goes wrong # - we want to show dialog if anything goes wrong
# - without reseting nothing is shown # - without resetting nothing is shown
self._settings_widget = None self._settings_widget = None
self._scroll_widget = scroll_widget self._scroll_widget = scroll_widget
self.reset_btn = reset_btn self.reset_btn = reset_btn

View file

@ -10,7 +10,7 @@
- `"is_file"` - this key is for storing openpype defaults in `openpype` repo - `"is_file"` - this key is for storing openpype defaults in `openpype` repo
- reasons of existence: developing new schemas does not require to create defaults manually - reasons of existence: developing new schemas does not require to create defaults manually
- key is validated, must be once in hierarchy else it won't be possible to store openpype defaults - key is validated, must be once in hierarchy else it won't be possible to store openpype defaults
- `"is_group"` - define that all values under key in hierarchy will be overriden if any value is modified, this information is also stored to overrides - `"is_group"` - define that all values under key in hierarchy will be overridden if any value is modified, this information is also stored to overrides
- this keys is not allowed for all inputs as they may have not reason for that - this keys is not allowed for all inputs as they may have not reason for that
- key is validated, can be only once in hierarchy but is not required - key is validated, can be only once in hierarchy but is not required
- currently there are `system configurations` and `project configurations` - currently there are `system configurations` and `project configurations`
@ -199,7 +199,7 @@
- number input, can be used for both integer and float - number input, can be used for both integer and float
- key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`) - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`)
- key `"minimum"` as minimum allowed number to enter (Default: `-99999`) - key `"minimum"` as minimum allowed number to enter (Default: `-99999`)
- key `"maxium"` as maximum allowed number to enter (Default: `99999`) - key `"maximum"` as maximum allowed number to enter (Default: `99999`)
``` ```
{ {
"type": "number", "type": "number",

View file

@ -93,7 +93,7 @@ class BaseWidget(QtWidgets.QWidget):
if is_modified: if is_modified:
return "modified" return "modified"
if has_project_override: if has_project_override:
return "overriden" return "overridden"
if has_studio_override: if has_studio_override:
return "studio" return "studio"
return "" return ""
@ -168,7 +168,7 @@ class BaseWidget(QtWidgets.QWidget):
with self.category_widget.working_state_context(): with self.category_widget.working_state_context():
self.entity.add_to_project_override self.entity.add_to_project_override
action = QtWidgets.QAction("Add to project project override") action = QtWidgets.QAction("Add to project override")
actions_mapping[action] = add_to_project_override actions_mapping[action] = add_to_project_override
menu.addAction(action) menu.addAction(action)
@ -289,7 +289,7 @@ class BaseWidget(QtWidgets.QWidget):
action = QtWidgets.QAction("Paste", menu) action = QtWidgets.QAction("Paste", menu)
output.append((action, paste_value)) output.append((action, paste_value))
# Paste value to matchin entity # Paste value to matching entity
def paste_value_to_path(): def paste_value_to_path():
with self.category_widget.working_state_context(): with self.category_widget.working_state_context():
_set_entity_value(matching_entity, value) _set_entity_value(matching_entity, value)

View file

@ -183,7 +183,7 @@ class DictConditionalWidget(BaseWidget):
content_widget.setObjectName("ContentWidget") content_widget.setObjectName("ContentWidget")
if self.entity.highlight_content: if self.entity.highlight_content:
content_state = "hightlighted" content_state = "highlighted"
bottom_margin = 5 bottom_margin = 5
else: else:
content_state = "" content_state = ""

View file

@ -354,7 +354,7 @@ class ModifiableDictItem(QtWidgets.QWidget):
if self.entity.has_unsaved_changes: if self.entity.has_unsaved_changes:
return "modified" return "modified"
if self.entity.has_project_override: if self.entity.has_project_override:
return "overriden" return "overridden"
if self.entity.has_studio_override: if self.entity.has_studio_override:
return "studio" return "studio"
return "" return ""
@ -600,8 +600,8 @@ class DictMutableKeysWidget(BaseWidget):
self.input_fields = [] self.input_fields = []
self.required_inputs_by_key = {} self.required_inputs_by_key = {}
if self.entity.hightlight_content: if self.entity.highlight_content:
content_state = "hightlighted" content_state = "highlighted"
bottom_margin = 5 bottom_margin = 5
else: else:
content_state = "" content_state = ""

View file

@ -150,7 +150,7 @@ class DictImmutableKeysWidget(BaseWidget):
content_widget.setObjectName("ContentWidget") content_widget.setObjectName("ContentWidget")
if self.entity.highlight_content: if self.entity.highlight_content:
content_state = "hightlighted" content_state = "highlighted"
bottom_margin = 5 bottom_margin = 5
else: else:
content_state = "" content_state = ""
@ -477,7 +477,7 @@ class OpenPypeVersionText(TextWidget):
self.entity.set(value) self.entity.set(value)
self.update_style() self.update_style()
else: else:
# Manually trigger hierachical style update # Manually trigger hierarchical style update
self.ignore_input_changes.set_ignore(True) self.ignore_input_changes.set_ignore(True)
self.ignore_input_changes.set_ignore(False) self.ignore_input_changes.set_ignore(False)
@ -675,7 +675,7 @@ class RawJsonWidget(InputWidget):
self.entity.set(self.input_field.json_value()) self.entity.set(self.input_field.json_value())
self.update_style() self.update_style()
else: else:
# Manually trigger hierachical style update # Manually trigger hierarchical style update
self.ignore_input_changes.set_ignore(True) self.ignore_input_changes.set_ignore(True)
self.ignore_input_changes.set_ignore(False) self.ignore_input_changes.set_ignore(False)
@ -792,7 +792,7 @@ class PathWidget(BaseWidget):
self.input_field.hierarchical_style_update() self.input_field.hierarchical_style_update()
def _on_entity_change(self): def _on_entity_change(self):
# No need to do anything. Styles will be updated from top hierachy. # No need to do anything. Styles will be updated from top hierarchy.
pass pass
def update_style(self): def update_style(self):

View file

@ -7,7 +7,7 @@ VALUE_CHANGE_OFFSET_MS = 300
def create_deffered_value_change_timer(callback): def create_deffered_value_change_timer(callback):
"""Deffer value change callback. """Defer value change callback.
UI won't trigger all callbacks on each value change but after predefined UI won't trigger all callbacks on each value change but after predefined
time. Timer is reset on each start so callback is triggered after user time. Timer is reset on each start so callback is triggered after user

View file

@ -28,7 +28,7 @@ class ListStrictWidget(BaseWidget):
break break
self._any_children_has_label = any_children_has_label self._any_children_has_label = any_children_has_label
# Change column stretch factor for verticall alignment # Change column stretch factor for vertical alignment
if not self.entity.is_horizontal: if not self.entity.is_horizontal:
col_index = 2 if any_children_has_label else 1 col_index = 2 if any_children_has_label else 1
content_layout.setColumnStretch(col_index, 1) content_layout.setColumnStretch(col_index, 1)

View file

@ -131,7 +131,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
elif event.type() == QtCore.QEvent.KeyPress: elif event.type() == QtCore.QEvent.KeyPress:
# TODO: handle QtCore.Qt.Key_Enter, Key_Return? # TODO: handle QtCore.Qt.Key_Enter, Key_Return?
if event.key() == QtCore.Qt.Key_Space: if event.key() == QtCore.Qt.Key_Space:
# toogle the current items check state # toggle the current items check state
if ( if (
index_flags & QtCore.Qt.ItemIsUserCheckable index_flags & QtCore.Qt.ItemIsUserCheckable
and index_flags & QtCore.Qt.ItemIsTristate and index_flags & QtCore.Qt.ItemIsTristate

View file

@ -173,7 +173,7 @@ class MainWidget(QtWidgets.QWidget):
def _on_restart_required(self): def _on_restart_required(self):
# Don't show dialog if there are not registered slots for # Don't show dialog if there are not registered slots for
# `trigger_restart` signal. # `trigger_restart` signal.
# - For example when settings are runnin as standalone tool # - For example when settings are running as standalone tool
# - PySide2 and PyQt5 compatible way how to find out # - PySide2 and PyQt5 compatible way how to find out
method_index = self.metaObject().indexOfMethod("trigger_restart()") method_index = self.metaObject().indexOfMethod("trigger_restart()")
method = self.metaObject().method(method_index) method = self.metaObject().method(method_index)

View file

@ -99,7 +99,7 @@ class Window(QtWidgets.QDialog):
return self._db return self._db
def on_start(self): def on_start(self):
''' Things must be done when initilized. ''' Things must be done when initialized.
''' '''
# Refresh asset input in Family widget # Refresh asset input in Family widget
self.on_asset_changed() self.on_asset_changed()

View file

@ -68,7 +68,7 @@ class AssetModel(TreeModel):
""" """
if silos: if silos:
# WARNING: Silo item "_id" is set to silo value # WARNING: Silo item "_id" is set to silo value
# mainly because GUI issue with perserve selection and expanded row # mainly because GUI issue with preserve selection and expanded row
# and because of easier hierarchy parenting (in "assets") # and because of easier hierarchy parenting (in "assets")
for silo in silos: for silo in silos:
node = Node({ node = Node({

View file

@ -18,7 +18,7 @@ def preserve_expanded_rows(tree_view,
This function is created to maintain the expand vs collapse status of This function is created to maintain the expand vs collapse status of
the model items. When refresh is triggered the items which are expanded the model items. When refresh is triggered the items which are expanded
will stay expanded and vise versa. will stay expanded and vice versa.
Arguments: Arguments:
tree_view (QWidgets.QTreeView): the tree view which is tree_view (QWidgets.QTreeView): the tree view which is
@ -68,7 +68,7 @@ def preserve_selection(tree_view,
This function is created to maintain the selection status of This function is created to maintain the selection status of
the model items. When refresh is triggered the items which are expanded the model items. When refresh is triggered the items which are expanded
will stay expanded and vise versa. will stay expanded and vice versa.
tree_view (QWidgets.QTreeView): the tree view nested in the application tree_view (QWidgets.QTreeView): the tree view nested in the application
column (int): the column to retrieve the data from column (int): the column to retrieve the data from
@ -390,7 +390,7 @@ class AssetWidget(QtWidgets.QWidget):
assets, (tuple, list) assets, (tuple, list)
), "Assets must be list or tuple" ), "Assets must be list or tuple"
# convert to list - tuple cant be modified # convert to list - tuple can't be modified
assets = list(assets) assets = list(assets)
# Clear selection # Clear selection

View file

@ -101,7 +101,7 @@ class DropDataFrame(QtWidgets.QFrame):
return paths return paths
def _add_item(self, data, actions=[]): def _add_item(self, data, actions=[]):
# Assign to self so garbage collector wont remove the component # Assign to self so garbage collector won't remove the component
# during initialization # during initialization
new_component = ComponentItem(self.components_list, self) new_component = ComponentItem(self.components_list, self)
new_component.set_context(data) new_component.set_context(data)

View file

@ -373,7 +373,7 @@ class FamilyWidget(QtWidgets.QWidget):
Override keyPressEvent to do nothing so that Maya's panels won't Override keyPressEvent to do nothing so that Maya's panels won't
take focus when pressing "SHIFT" whilst mouse is over viewport or take focus when pressing "SHIFT" whilst mouse is over viewport or
outliner. This way users don't accidently perform Maya commands outliner. This way users don't accidentally perform Maya commands
whilst trying to name an instance. whilst trying to name an instance.
""" """

Some files were not shown because too many files have changed in this diff Show more