mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge branch 'develop' into feature/OP-2361_Store-settings-by-OpenPype-version
This commit is contained in:
commit
773d3aa176
113 changed files with 1697 additions and 358 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
34
openpype/hosts/maya/plugins/publish/validate_cycle_error.py
Normal file
34
openpype/hosts/maya/plugins/publish/validate_cycle_error.py
Normal 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
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
544
openpype/hosts/unreal/plugins/load/load_layout.py
Normal file
544
openpype/hosts/unreal/plugins/load/load_layout.py
Normal 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)
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
139
openpype/hosts/webpublisher/plugins/publish/extract_thumbnail.py
Normal file
139
openpype/hosts/webpublisher/plugins/publish/extract_thumbnail.py
Normal 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
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
51
openpype/pipeline/lib/events.py
Normal file
51
openpype/pipeline/lib/events.py
Normal 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
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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'))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
{
|
{
|
||||||
"color_code": [],
|
"color_code": [],
|
||||||
"layer_name_regex": [],
|
"layer_name_regex": [],
|
||||||
"family": "",
|
"family": "image",
|
||||||
"subset_template_name": ""
|
"subset_template_name": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 == '':
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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"""
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"""
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 = ""
|
||||||
|
|
|
||||||
|
|
@ -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 = ""
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue