mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge branch 'develop' into feature/1235-hiero-unify-otio-workflow-from-resolve
This commit is contained in:
commit
d28b13dcbb
65 changed files with 2038 additions and 864 deletions
|
|
@ -4,12 +4,12 @@ from openpype.lib import PreLaunchHook
|
|||
|
||||
class PrePython2Vendor(PreLaunchHook):
|
||||
"""Prepend python 2 dependencies for py2 hosts."""
|
||||
# WARNING This hook will probably be deprecated in OpenPype 3 - kept for
|
||||
# test
|
||||
order = 10
|
||||
app_groups = ["hiero", "nuke", "nukex", "unreal", "maya", "houdini"]
|
||||
|
||||
def execute(self):
|
||||
if not self.application.use_python_2:
|
||||
return
|
||||
|
||||
# Prepare vendor dir path
|
||||
self.log.info("adding global python 2 vendor")
|
||||
pype_root = os.getenv("OPENPYPE_REPOS_ROOT")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import logging
|
|||
from avalon import io
|
||||
from avalon import api as avalon
|
||||
from avalon.vendor import Qt
|
||||
from openpype import lib
|
||||
from openpype import lib, api
|
||||
import pyblish.api as pyblish
|
||||
import openpype.hosts.aftereffects
|
||||
|
||||
|
|
@ -81,3 +81,69 @@ def uninstall():
|
|||
def on_pyblish_instance_toggled(instance, old_value, new_value):
|
||||
"""Toggle layer visibility on instance toggles."""
|
||||
instance[0].Visible = new_value
|
||||
|
||||
|
||||
def get_asset_settings():
|
||||
"""Get settings on current asset from database.
|
||||
|
||||
Returns:
|
||||
dict: Scene data.
|
||||
|
||||
"""
|
||||
asset_data = lib.get_asset()["data"]
|
||||
fps = asset_data.get("fps")
|
||||
frame_start = asset_data.get("frameStart")
|
||||
frame_end = asset_data.get("frameEnd")
|
||||
handle_start = asset_data.get("handleStart")
|
||||
handle_end = asset_data.get("handleEnd")
|
||||
resolution_width = asset_data.get("resolutionWidth")
|
||||
resolution_height = asset_data.get("resolutionHeight")
|
||||
duration = frame_end + handle_end - max(frame_start - handle_start, 0)
|
||||
entity_type = asset_data.get("entityType")
|
||||
|
||||
scene_data = {
|
||||
"fps": fps,
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"resolutionWidth": resolution_width,
|
||||
"resolutionHeight": resolution_height,
|
||||
"duration": duration
|
||||
}
|
||||
|
||||
try:
|
||||
# temporary, in pype3 replace with api.get_current_project_settings
|
||||
skip_resolution_check = (
|
||||
api.get_current_project_settings()
|
||||
["plugins"]
|
||||
["aftereffects"]
|
||||
["publish"]
|
||||
["ValidateSceneSettings"]
|
||||
["skip_resolution_check"]
|
||||
)
|
||||
skip_timelines_check = (
|
||||
api.get_current_project_settings()
|
||||
["plugins"]
|
||||
["aftereffects"]
|
||||
["publish"]
|
||||
["ValidateSceneSettings"]
|
||||
["skip_timelines_check"]
|
||||
)
|
||||
except KeyError:
|
||||
skip_resolution_check = ['*']
|
||||
skip_timelines_check = ['*']
|
||||
|
||||
if os.getenv('AVALON_TASK') in skip_resolution_check or \
|
||||
'*' in skip_timelines_check:
|
||||
scene_data.pop("resolutionWidth")
|
||||
scene_data.pop("resolutionHeight")
|
||||
|
||||
if entity_type in skip_timelines_check or '*' in skip_timelines_check:
|
||||
scene_data.pop('fps', None)
|
||||
scene_data.pop('frameStart', None)
|
||||
scene_data.pop('frameEnd', None)
|
||||
scene_data.pop('handleStart', None)
|
||||
scene_data.pop('handleEnd', None)
|
||||
|
||||
return scene_data
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class AERenderInstance(RenderInstance):
|
|||
# extend generic, composition name is needed
|
||||
comp_name = attr.ib(default=None)
|
||||
comp_id = attr.ib(default=None)
|
||||
fps = attr.ib(default=None)
|
||||
|
||||
|
||||
class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
||||
|
|
@ -45,6 +46,7 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
|||
raise ValueError("Couldn't find id, unable to publish. " +
|
||||
"Please recreate instance.")
|
||||
item_id = inst["members"][0]
|
||||
|
||||
work_area_info = self.stub.get_work_area(int(item_id))
|
||||
|
||||
if not work_area_info:
|
||||
|
|
@ -57,6 +59,8 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
|||
frameEnd = round(work_area_info.workAreaStart +
|
||||
float(work_area_info.workAreaDuration) *
|
||||
float(work_area_info.frameRate)) - 1
|
||||
fps = work_area_info.frameRate
|
||||
# TODO add resolution when supported by extension
|
||||
|
||||
if inst["family"] == "render" and inst["active"]:
|
||||
instance = AERenderInstance(
|
||||
|
|
@ -86,7 +90,8 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
|||
frameStart=frameStart,
|
||||
frameEnd=frameEnd,
|
||||
frameStep=1,
|
||||
toBeRenderedOn='deadline'
|
||||
toBeRenderedOn='deadline',
|
||||
fps=fps
|
||||
)
|
||||
|
||||
comp = compositions_by_id.get(int(item_id))
|
||||
|
|
@ -102,7 +107,6 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
|||
|
||||
instances.append(instance)
|
||||
|
||||
self.log.debug("instances::{}".format(instances))
|
||||
return instances
|
||||
|
||||
def get_expected_files(self, render_instance):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validate scene settings."""
|
||||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from avalon import aftereffects
|
||||
|
||||
import openpype.hosts.aftereffects.api as api
|
||||
|
||||
stub = aftereffects.stub()
|
||||
|
||||
|
||||
class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Ensures that Composition Settings (right mouse on comp) are same as
|
||||
in FTrack on task.
|
||||
|
||||
By default checks only duration - how many frames should be rendered.
|
||||
Compares:
|
||||
Frame start - Frame end + 1 from FTrack
|
||||
against
|
||||
Duration in Composition Settings.
|
||||
|
||||
If this complains:
|
||||
Check error message where is discrepancy.
|
||||
Check FTrack task 'pype' section of task attributes for expected
|
||||
values.
|
||||
Check/modify rendered Composition Settings.
|
||||
|
||||
If you know what you are doing run publishing again, uncheck this
|
||||
validation before Validation phase.
|
||||
"""
|
||||
|
||||
"""
|
||||
Dev docu:
|
||||
Could be configured by 'presets/plugins/aftereffects/publish'
|
||||
|
||||
skip_timelines_check - fill task name for which skip validation of
|
||||
frameStart
|
||||
frameEnd
|
||||
fps
|
||||
handleStart
|
||||
handleEnd
|
||||
skip_resolution_check - fill entity type ('asset') to skip validation
|
||||
resolutionWidth
|
||||
resolutionHeight
|
||||
TODO support in extension is missing for now
|
||||
|
||||
By defaults validates duration (how many frames should be published)
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Scene Settings"
|
||||
families = ["render.farm"]
|
||||
hosts = ["aftereffects"]
|
||||
optional = True
|
||||
|
||||
skip_timelines_check = ["*"] # * >> skip for all
|
||||
skip_resolution_check = ["*"]
|
||||
|
||||
def process(self, instance):
|
||||
"""Plugin entry point."""
|
||||
expected_settings = api.get_asset_settings()
|
||||
self.log.info("expected_settings::{}".format(expected_settings))
|
||||
|
||||
# handle case where ftrack uses only two decimal places
|
||||
# 23.976023976023978 vs. 23.98
|
||||
fps = instance.data.get("fps")
|
||||
if fps:
|
||||
if isinstance(fps, float):
|
||||
fps = float(
|
||||
"{:.2f}".format(fps))
|
||||
expected_settings["fps"] = fps
|
||||
|
||||
duration = instance.data.get("frameEndHandle") - \
|
||||
instance.data.get("frameStartHandle") + 1
|
||||
|
||||
current_settings = {
|
||||
"fps": fps,
|
||||
"frameStartHandle": instance.data.get("frameStartHandle"),
|
||||
"frameEndHandle": instance.data.get("frameEndHandle"),
|
||||
"resolutionWidth": instance.data.get("resolutionWidth"),
|
||||
"resolutionHeight": instance.data.get("resolutionHeight"),
|
||||
"duration": duration
|
||||
}
|
||||
self.log.info("current_settings:: {}".format(current_settings))
|
||||
|
||||
invalid_settings = []
|
||||
for key, value in expected_settings.items():
|
||||
if value != current_settings[key]:
|
||||
invalid_settings.append(
|
||||
"{} expected: {} found: {}".format(key, value,
|
||||
current_settings[key])
|
||||
)
|
||||
|
||||
if ((expected_settings.get("handleStart")
|
||||
or expected_settings.get("handleEnd"))
|
||||
and invalid_settings):
|
||||
msg = "Handles included in calculation. Remove handles in DB " +\
|
||||
"or extend frame range in Composition Setting."
|
||||
invalid_settings[-1]["reason"] = msg
|
||||
|
||||
msg = "Found invalid settings:\n{}".format(
|
||||
"\n".join(invalid_settings)
|
||||
)
|
||||
assert not invalid_settings, msg
|
||||
assert os.path.exists(instance.data.get("source")), (
|
||||
"Scene file not found (saved under wrong name)"
|
||||
)
|
||||
|
|
@ -9,7 +9,7 @@ from avalon import api
|
|||
import avalon.blender
|
||||
from openpype.api import PypeCreatorMixin
|
||||
|
||||
VALID_EXTENSIONS = [".blend", ".json"]
|
||||
VALID_EXTENSIONS = [".blend", ".json", ".abc"]
|
||||
|
||||
|
||||
def asset_name(
|
||||
|
|
|
|||
35
openpype/hosts/blender/plugins/create/create_pointcache.py
Normal file
35
openpype/hosts/blender/plugins/create/create_pointcache.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"""Create a pointcache asset."""
|
||||
|
||||
import bpy
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender import lib
|
||||
import openpype.hosts.blender.api.plugin
|
||||
|
||||
|
||||
class CreatePointcache(openpype.hosts.blender.api.plugin.Creator):
|
||||
"""Polygonal static geometry"""
|
||||
|
||||
name = "pointcacheMain"
|
||||
label = "Point Cache"
|
||||
family = "pointcache"
|
||||
icon = "gears"
|
||||
|
||||
def process(self):
|
||||
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
collection = bpy.data.collections.new(name=name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
self.data['task'] = api.Session.get('AVALON_TASK')
|
||||
lib.imprint(collection, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
objects = lib.get_selection()
|
||||
for obj in objects:
|
||||
collection.objects.link(obj)
|
||||
if obj.type == 'EMPTY':
|
||||
objects.extend(obj.children)
|
||||
|
||||
return collection
|
||||
246
openpype/hosts/blender/plugins/load/load_abc.py
Normal file
246
openpype/hosts/blender/plugins/load/load_abc.py
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
"""Load an asset in Blender from an Alembic file."""
|
||||
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from avalon import api, blender
|
||||
import bpy
|
||||
import openpype.hosts.blender.api.plugin as plugin
|
||||
|
||||
|
||||
class CacheModelLoader(plugin.AssetLoader):
|
||||
"""Load cache models.
|
||||
|
||||
Stores the imported asset in a collection named after the asset.
|
||||
|
||||
Note:
|
||||
At least for now it only supports Alembic files.
|
||||
"""
|
||||
|
||||
families = ["model", "pointcache"]
|
||||
representations = ["abc"]
|
||||
|
||||
label = "Link Alembic"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, objects, container):
|
||||
for obj in list(objects):
|
||||
if obj.type == 'MESH':
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
elif obj.type == 'EMPTY':
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
bpy.data.collections.remove(container)
|
||||
|
||||
def _process(self, libpath, container_name, parent_collection):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
view_layer = bpy.context.view_layer
|
||||
view_layer_collection = view_layer.active_layer_collection.collection
|
||||
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
bpy.ops.wm.alembic_import(
|
||||
filepath=libpath,
|
||||
relative_path=relative
|
||||
)
|
||||
|
||||
parent = parent_collection
|
||||
|
||||
if parent is None:
|
||||
parent = bpy.context.scene.collection
|
||||
|
||||
model_container = bpy.data.collections.new(container_name)
|
||||
parent.children.link(model_container)
|
||||
for obj in bpy.context.selected_objects:
|
||||
model_container.objects.link(obj)
|
||||
view_layer_collection.objects.unlink(obj)
|
||||
|
||||
name = obj.name
|
||||
obj.name = f"{name}:{container_name}"
|
||||
|
||||
# Groups are imported as Empty objects in Blender
|
||||
if obj.type == 'MESH':
|
||||
data_name = obj.data.name
|
||||
obj.data.name = f"{data_name}:{container_name}"
|
||||
|
||||
if not obj.get(blender.pipeline.AVALON_PROPERTY):
|
||||
obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": container_name})
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return model_container
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[List]:
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
lib_container = plugin.asset_name(
|
||||
asset, subset
|
||||
)
|
||||
unique_number = plugin.get_unique_number(
|
||||
asset, subset
|
||||
)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
container_name = plugin.asset_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
container = bpy.data.collections.new(lib_container)
|
||||
container.name = container_name
|
||||
blender.pipeline.containerise_existing(
|
||||
container,
|
||||
name,
|
||||
namespace,
|
||||
context,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
|
||||
container_metadata = container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
|
||||
obj_container = self._process(
|
||||
libpath, container_name, None)
|
||||
|
||||
container_metadata["obj_container"] = obj_container
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
container_metadata["objects"] = obj_container.all_objects
|
||||
|
||||
nodes = list(container.objects)
|
||||
nodes.append(container)
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
|
||||
def update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
|
||||
This will remove all objects of the current collection, load the new
|
||||
ones and add them to the collection.
|
||||
If the objects of the collection are used in another collection they
|
||||
will not be removed, only unlinked. Normally this should not be the
|
||||
case though.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
self.log.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert collection, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
collection_libpath = collection_metadata["libpath"]
|
||||
|
||||
obj_container = plugin.get_local_collection_with_name(
|
||||
collection_metadata["obj_container"].name
|
||||
)
|
||||
objects = obj_container.all_objects
|
||||
|
||||
container_name = obj_container.name
|
||||
|
||||
normalized_collection_libpath = (
|
||||
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
self.log.debug(
|
||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_collection_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_collection_libpath == normalized_libpath:
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
parent = plugin.get_parent_collection(obj_container)
|
||||
|
||||
self._remove(objects, obj_container)
|
||||
|
||||
obj_container = self._process(
|
||||
str(libpath), container_name, parent)
|
||||
|
||||
collection_metadata["obj_container"] = obj_container
|
||||
collection_metadata["objects"] = obj_container.all_objects
|
||||
collection_metadata["libpath"] = str(libpath)
|
||||
collection_metadata["representation"] = str(representation["_id"])
|
||||
|
||||
def remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
|
||||
Arguments:
|
||||
container (openpype:container-1.0): Container to remove,
|
||||
from `host.ls()`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the container was deleted.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
if not collection:
|
||||
return False
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
obj_container = plugin.get_local_collection_with_name(
|
||||
collection_metadata["obj_container"].name
|
||||
)
|
||||
objects = obj_container.all_objects
|
||||
|
||||
self._remove(objects, obj_container)
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
return True
|
||||
|
|
@ -242,65 +242,3 @@ class BlendModelLoader(plugin.AssetLoader):
|
|||
bpy.data.collections.remove(collection)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class CacheModelLoader(plugin.AssetLoader):
|
||||
"""Load cache models.
|
||||
|
||||
Stores the imported asset in a collection named after the asset.
|
||||
|
||||
Note:
|
||||
At least for now it only supports Alembic files.
|
||||
"""
|
||||
|
||||
families = ["model"]
|
||||
representations = ["abc"]
|
||||
|
||||
label = "Link Model"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[List]:
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Loading of Alembic files is not yet implemented.")
|
||||
# TODO (jasper): implement Alembic import.
|
||||
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
# TODO (jasper): evaluate use of namespace which is 'alien' to Blender.
|
||||
lib_container = container_name = (
|
||||
plugin.asset_name(asset, subset, namespace)
|
||||
)
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=relative
|
||||
) as (data_from, data_to):
|
||||
data_to.collections = [lib_container]
|
||||
|
||||
scene = bpy.context.scene
|
||||
instance_empty = bpy.data.objects.new(
|
||||
container_name, None
|
||||
)
|
||||
scene.collection.objects.link(instance_empty)
|
||||
instance_empty.instance_type = 'COLLECTION'
|
||||
collection = bpy.data.collections[lib_container]
|
||||
collection.name = container_name
|
||||
instance_empty.instance_collection = collection
|
||||
|
||||
nodes = list(collection.objects)
|
||||
nodes.append(collection)
|
||||
nodes.append(instance_empty)
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@ class ExtractABC(openpype.api.Extractor):
|
|||
|
||||
label = "Extract ABC"
|
||||
hosts = ["blender"]
|
||||
families = ["model"]
|
||||
families = ["model", "pointcache"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
# Define extract output file path
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.fbx"
|
||||
filename = f"{instance.name}.abc"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
context = bpy.context
|
||||
|
|
@ -52,6 +52,8 @@ class ExtractABC(openpype.api.Extractor):
|
|||
|
||||
old_scale = scene.unit_settings.scale_length
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
selected = list()
|
||||
|
||||
for obj in instance:
|
||||
|
|
@ -67,14 +69,11 @@ class ExtractABC(openpype.api.Extractor):
|
|||
# We set the scale of the scene for the export
|
||||
scene.unit_settings.scale_length = 0.01
|
||||
|
||||
self.log.info(new_context)
|
||||
|
||||
# We export the abc
|
||||
bpy.ops.wm.alembic_export(
|
||||
new_context,
|
||||
filepath=filepath,
|
||||
start=1,
|
||||
end=1
|
||||
selected=True
|
||||
)
|
||||
|
||||
view_layer.active_layer_collection = old_active_layer_collection
|
||||
|
|
|
|||
23
openpype/hosts/maya/plugins/create/create_redshift_proxy.py
Normal file
23
openpype/hosts/maya/plugins/create/create_redshift_proxy.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator of Redshift proxy subset types."""
|
||||
|
||||
from openpype.hosts.maya.api import plugin, lib
|
||||
|
||||
|
||||
class CreateRedshiftProxy(plugin.Creator):
|
||||
"""Create instance of Redshift Proxy subset."""
|
||||
|
||||
name = "redshiftproxy"
|
||||
label = "Redshift Proxy"
|
||||
family = "redshiftproxy"
|
||||
icon = "gears"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateRedshiftProxy, self).__init__(*args, **kwargs)
|
||||
|
||||
animation_data = lib.collect_animation_data()
|
||||
|
||||
self.data["animation"] = False
|
||||
self.data["proxyFrameStart"] = animation_data["frameStart"]
|
||||
self.data["proxyFrameEnd"] = animation_data["frameEnd"]
|
||||
self.data["proxyFrameStep"] = animation_data["step"]
|
||||
146
openpype/hosts/maya/plugins/load/load_redshift_proxy.py
Normal file
146
openpype/hosts/maya/plugins/load/load_redshift_proxy.py
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Loader for Redshift proxy."""
|
||||
from avalon.maya import lib
|
||||
from avalon import api
|
||||
from openpype.api import get_project_settings
|
||||
import os
|
||||
import maya.cmds as cmds
|
||||
import clique
|
||||
|
||||
|
||||
class RedshiftProxyLoader(api.Loader):
|
||||
"""Load Redshift proxy"""
|
||||
|
||||
families = ["redshiftproxy"]
|
||||
representations = ["rs"]
|
||||
|
||||
label = "Import Redshift Proxy"
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def load(self, context, name=None, namespace=None, options=None):
|
||||
"""Plugin entry point."""
|
||||
|
||||
from avalon.maya.pipeline import containerise
|
||||
from openpype.hosts.maya.api.lib import namespaced
|
||||
|
||||
try:
|
||||
family = context["representation"]["context"]["family"]
|
||||
except ValueError:
|
||||
family = "redshiftproxy"
|
||||
|
||||
asset_name = context['asset']["name"]
|
||||
namespace = namespace or lib.unique_namespace(
|
||||
asset_name + "_",
|
||||
prefix="_" if asset_name[0].isdigit() else "",
|
||||
suffix="_",
|
||||
)
|
||||
|
||||
# Ensure Redshift for Maya is loaded.
|
||||
cmds.loadPlugin("redshift4maya", quiet=True)
|
||||
|
||||
with lib.maintained_selection():
|
||||
cmds.namespace(addNamespace=namespace)
|
||||
with namespaced(namespace, new=False):
|
||||
nodes, group_node = self.create_rs_proxy(
|
||||
name, self.fname)
|
||||
|
||||
self[:] = nodes
|
||||
if not nodes:
|
||||
return
|
||||
|
||||
# colour the group node
|
||||
settings = get_project_settings(os.environ['AVALON_PROJECT'])
|
||||
colors = settings['maya']['load']['colors']
|
||||
c = colors.get(family)
|
||||
if c is not None:
|
||||
cmds.setAttr("{0}.useOutlinerColor".format(group_node), 1)
|
||||
cmds.setAttr("{0}.outlinerColor".format(group_node),
|
||||
c[0], c[1], c[2])
|
||||
|
||||
return containerise(
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
nodes=nodes,
|
||||
context=context,
|
||||
loader=self.__class__.__name__)
|
||||
|
||||
def update(self, container, representation):
|
||||
|
||||
node = container['objectName']
|
||||
assert cmds.objExists(node), "Missing container"
|
||||
|
||||
members = cmds.sets(node, query=True) or []
|
||||
rs_meshes = cmds.ls(members, type="RedshiftProxyMesh")
|
||||
assert rs_meshes, "Cannot find RedshiftProxyMesh in container"
|
||||
|
||||
filename = api.get_representation_path(representation)
|
||||
|
||||
for rs_mesh in rs_meshes:
|
||||
cmds.setAttr("{}.fileName".format(rs_mesh),
|
||||
filename,
|
||||
type="string")
|
||||
|
||||
# Update metadata
|
||||
cmds.setAttr("{}.representation".format(node),
|
||||
str(representation["_id"]),
|
||||
type="string")
|
||||
|
||||
def remove(self, container):
|
||||
|
||||
# Delete container and its contents
|
||||
if cmds.objExists(container['objectName']):
|
||||
members = cmds.sets(container['objectName'], query=True) or []
|
||||
cmds.delete([container['objectName']] + members)
|
||||
|
||||
# Remove the namespace, if empty
|
||||
namespace = container['namespace']
|
||||
if cmds.namespace(exists=namespace):
|
||||
members = cmds.namespaceInfo(namespace, listNamespace=True)
|
||||
if not members:
|
||||
cmds.namespace(removeNamespace=namespace)
|
||||
else:
|
||||
self.log.warning("Namespace not deleted because it "
|
||||
"still has members: %s", namespace)
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
||||
def create_rs_proxy(self, name, path):
|
||||
"""Creates Redshift Proxies showing a proxy object.
|
||||
|
||||
Args:
|
||||
name (str): Proxy name.
|
||||
path (str): Path to proxy file.
|
||||
|
||||
Returns:
|
||||
(str, str): Name of mesh with Redshift proxy and its parent
|
||||
transform.
|
||||
|
||||
"""
|
||||
rs_mesh = cmds.createNode(
|
||||
'RedshiftProxyMesh', name="{}_RS".format(name))
|
||||
mesh_shape = cmds.createNode("mesh", name="{}_GEOShape".format(name))
|
||||
|
||||
cmds.setAttr("{}.fileName".format(rs_mesh),
|
||||
path,
|
||||
type="string")
|
||||
|
||||
cmds.connectAttr("{}.outMesh".format(rs_mesh),
|
||||
"{}.inMesh".format(mesh_shape))
|
||||
|
||||
group_node = cmds.group(empty=True, name="{}_GRP".format(name))
|
||||
mesh_transform = cmds.listRelatives(mesh_shape,
|
||||
parent=True, fullPath=True)
|
||||
cmds.parent(mesh_transform, group_node)
|
||||
nodes = [rs_mesh, mesh_shape, group_node]
|
||||
|
||||
# determine if we need to enable animation support
|
||||
files_in_folder = os.listdir(os.path.dirname(path))
|
||||
collections, remainder = clique.assemble(files_in_folder)
|
||||
|
||||
if collections:
|
||||
cmds.setAttr("{}.useFrameExtension".format(rs_mesh), 1)
|
||||
|
||||
return nodes, group_node
|
||||
|
|
@ -348,6 +348,13 @@ class CollectLook(pyblish.api.InstancePlugin):
|
|||
history = []
|
||||
for material in materials:
|
||||
history.extend(cmds.listHistory(material))
|
||||
|
||||
# handle VrayPluginNodeMtl node - see #1397
|
||||
vray_plugin_nodes = cmds.ls(
|
||||
history, type="VRayPluginNodeMtl", long=True)
|
||||
for vray_node in vray_plugin_nodes:
|
||||
history.extend(cmds.listHistory(vray_node))
|
||||
|
||||
files = cmds.ls(history, type="file", long=True)
|
||||
files.extend(cmds.ls(history, type="aiImage", long=True))
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Redshift Proxy extractor."""
|
||||
import os
|
||||
|
||||
import avalon.maya
|
||||
import openpype.api
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
||||
class ExtractRedshiftProxy(openpype.api.Extractor):
|
||||
"""Extract the content of the instance to a redshift proxy file."""
|
||||
|
||||
label = "Redshift Proxy (.rs)"
|
||||
hosts = ["maya"]
|
||||
families = ["redshiftproxy"]
|
||||
|
||||
def process(self, instance):
|
||||
"""Extractor entry point."""
|
||||
|
||||
staging_dir = self.staging_dir(instance)
|
||||
file_name = "{}.rs".format(instance.name)
|
||||
file_path = os.path.join(staging_dir, file_name)
|
||||
|
||||
anim_on = instance.data["animation"]
|
||||
rs_options = "exportConnectivity=0;enableCompression=1;keepUnused=0;"
|
||||
repr_files = file_name
|
||||
|
||||
if not anim_on:
|
||||
# Remove animation information because it is not required for
|
||||
# non-animated subsets
|
||||
instance.data.pop("proxyFrameStart", None)
|
||||
instance.data.pop("proxyFrameEnd", None)
|
||||
|
||||
else:
|
||||
start_frame = instance.data["proxyFrameStart"]
|
||||
end_frame = instance.data["proxyFrameEnd"]
|
||||
rs_options = "{}startFrame={};endFrame={};frameStep={};".format(
|
||||
rs_options, start_frame,
|
||||
end_frame, instance.data["proxyFrameStep"]
|
||||
)
|
||||
|
||||
root, ext = os.path.splitext(file_path)
|
||||
# Padding is taken from number of digits of the end_frame.
|
||||
# Not sure where Redshift is taking it.
|
||||
repr_files = [
|
||||
"{}.{}{}".format(root, str(frame).rjust(4, "0"), ext) # noqa: E501
|
||||
for frame in range(
|
||||
int(start_frame),
|
||||
int(end_frame) + 1,
|
||||
int(instance.data["proxyFrameStep"]),
|
||||
)]
|
||||
# vertex_colors = instance.data.get("vertexColors", False)
|
||||
|
||||
# Write out rs file
|
||||
self.log.info("Writing: '%s'" % file_path)
|
||||
with avalon.maya.maintained_selection():
|
||||
cmds.select(instance.data["setMembers"], noExpand=True)
|
||||
cmds.file(file_path,
|
||||
pr=False,
|
||||
force=True,
|
||||
type="Redshift Proxy",
|
||||
exportSelected=True,
|
||||
options=rs_options)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
self.log.debug("Files: {}".format(repr_files))
|
||||
|
||||
representation = {
|
||||
'name': 'rs',
|
||||
'ext': 'rs',
|
||||
'files': repr_files,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
if anim_on:
|
||||
representation["frameStart"] = instance.data["proxyFrameStart"]
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s"
|
||||
% (instance.name, staging_dir))
|
||||
|
|
@ -5,7 +5,7 @@ import re
|
|||
|
||||
import avalon.maya
|
||||
import openpype.api
|
||||
from openpype.hosts.maya.render_setup_tools import export_in_rs_layer
|
||||
from openpype.hosts.maya.api.render_setup_tools import export_in_rs_layer
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import openpype.api
|
|||
class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin):
|
||||
"""Validate if mesh is made of triangles for Unreal Engine"""
|
||||
|
||||
order = openpype.api.ValidateMeshOder
|
||||
order = openpype.api.ValidateMeshOrder
|
||||
hosts = ["maya"]
|
||||
families = ["unrealStaticMesh"]
|
||||
category = "geometry"
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ def on_pyblish_instance_toggled(instance, old_value, new_value):
|
|||
log.info("instance toggle: {}, old_value: {}, new_value:{} ".format(
|
||||
instance, old_value, new_value))
|
||||
|
||||
from avalon.api.nuke import (
|
||||
from avalon.nuke import (
|
||||
viewer_update_and_undo_stop,
|
||||
add_publish_knob
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ def install():
|
|||
menu.addCommand(
|
||||
name,
|
||||
workfiles.show,
|
||||
index=(rm_item[0])
|
||||
index=2
|
||||
)
|
||||
|
||||
menu.addSeparator(index=3)
|
||||
# replace reset resolution from avalon core to pype's
|
||||
name = "Reset Resolution"
|
||||
new_name = "Set Resolution"
|
||||
|
|
@ -63,16 +63,7 @@ def install():
|
|||
# add colorspace menu item
|
||||
name = "Set Colorspace"
|
||||
menu.addCommand(
|
||||
name, lambda: WorkfileSettings().set_colorspace(),
|
||||
index=(rm_item[0] + 2)
|
||||
)
|
||||
log.debug("Adding menu item: {}".format(name))
|
||||
|
||||
# add workfile builder menu item
|
||||
name = "Build Workfile"
|
||||
menu.addCommand(
|
||||
name, lambda: BuildWorkfile().process(),
|
||||
index=(rm_item[0] + 7)
|
||||
name, lambda: WorkfileSettings().set_colorspace()
|
||||
)
|
||||
log.debug("Adding menu item: {}".format(name))
|
||||
|
||||
|
|
@ -80,11 +71,20 @@ def install():
|
|||
name = "Apply All Settings"
|
||||
menu.addCommand(
|
||||
name,
|
||||
lambda: WorkfileSettings().set_context_settings(),
|
||||
index=(rm_item[0] + 3)
|
||||
lambda: WorkfileSettings().set_context_settings()
|
||||
)
|
||||
log.debug("Adding menu item: {}".format(name))
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
# add workfile builder menu item
|
||||
name = "Build Workfile"
|
||||
menu.addCommand(
|
||||
name, lambda: BuildWorkfile().process()
|
||||
)
|
||||
log.debug("Adding menu item: {}".format(name))
|
||||
|
||||
|
||||
# adding shortcuts
|
||||
add_shortcuts_from_presets()
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class UnrealPrelaunchHook(PreLaunchHook):
|
|||
asset_name = self.data["asset_name"]
|
||||
task_name = self.data["task_name"]
|
||||
workdir = self.launch_context.env["AVALON_WORKDIR"]
|
||||
engine_version = self.app_name.split("_")[-1].replace("-", ".")
|
||||
engine_version = self.app_name.split("/")[-1].replace("-", ".")
|
||||
unreal_project_name = f"{asset_name}_{task_name}"
|
||||
|
||||
# Unreal is sensitive about project names longer then 20 chars
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from avalon import api, pipeline
|
||||
|
||||
PACKAGE_DIR = os.path.dirname(__file__)
|
||||
PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins", "launcher")
|
||||
ACTIONS_DIR = os.path.join(PLUGINS_DIR, "actions")
|
||||
|
||||
|
||||
def register_launcher_actions():
|
||||
"""Register specific actions which should be accessible in the launcher"""
|
||||
|
||||
actions = []
|
||||
ext = ".py"
|
||||
sys.path.append(ACTIONS_DIR)
|
||||
|
||||
for f in os.listdir(ACTIONS_DIR):
|
||||
file, extention = os.path.splitext(f)
|
||||
if ext in extention:
|
||||
module = __import__(file)
|
||||
klass = getattr(module, file)
|
||||
actions.append(klass)
|
||||
|
||||
if actions is []:
|
||||
return
|
||||
|
||||
for action in actions:
|
||||
print("Using launcher action from config @ '{}'".format(action.name))
|
||||
pipeline.register_plugin(api.Action, action)
|
||||
|
|
@ -79,6 +79,16 @@ from .avalon_context import (
|
|||
change_timer_to_current_context
|
||||
)
|
||||
|
||||
from .local_settings import (
|
||||
IniSettingRegistry,
|
||||
JSONSettingRegistry,
|
||||
OpenPypeSecureRegistry,
|
||||
OpenPypeSettingsRegistry,
|
||||
get_local_site_id,
|
||||
change_openpype_mongo_url,
|
||||
get_openpype_username
|
||||
)
|
||||
|
||||
from .applications import (
|
||||
ApplicationLaunchFailed,
|
||||
ApplictionExecutableNotFound,
|
||||
|
|
@ -112,15 +122,6 @@ from .plugin_tools import (
|
|||
should_decompress
|
||||
)
|
||||
|
||||
from .local_settings import (
|
||||
IniSettingRegistry,
|
||||
JSONSettingRegistry,
|
||||
OpenPypeSecureRegistry,
|
||||
OpenPypeSettingsRegistry,
|
||||
get_local_site_id,
|
||||
change_openpype_mongo_url
|
||||
)
|
||||
|
||||
from .path_tools import (
|
||||
version_up,
|
||||
get_version_from_path,
|
||||
|
|
@ -179,6 +180,14 @@ __all__ = [
|
|||
|
||||
"change_timer_to_current_context",
|
||||
|
||||
"IniSettingRegistry",
|
||||
"JSONSettingRegistry",
|
||||
"OpenPypeSecureRegistry",
|
||||
"OpenPypeSettingsRegistry",
|
||||
"get_local_site_id",
|
||||
"change_openpype_mongo_url",
|
||||
"get_openpype_username",
|
||||
|
||||
"ApplicationLaunchFailed",
|
||||
"ApplictionExecutableNotFound",
|
||||
"ApplicationNotFound",
|
||||
|
|
@ -224,13 +233,6 @@ __all__ = [
|
|||
"validate_mongo_connection",
|
||||
"OpenPypeMongoConnection",
|
||||
|
||||
"IniSettingRegistry",
|
||||
"JSONSettingRegistry",
|
||||
"OpenPypeSecureRegistry",
|
||||
"OpenPypeSettingsRegistry",
|
||||
"get_local_site_id",
|
||||
"change_openpype_mongo_url",
|
||||
|
||||
"timeit",
|
||||
|
||||
"is_overlapping_otio_ranges",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from . import (
|
|||
PypeLogger,
|
||||
Anatomy
|
||||
)
|
||||
from .local_settings import get_openpype_username
|
||||
from .avalon_context import (
|
||||
get_workdir_data,
|
||||
get_workdir_with_workdir_data
|
||||
|
|
@ -179,6 +180,7 @@ class Application:
|
|||
if group.enabled:
|
||||
enabled = data.get("enabled", True)
|
||||
self.enabled = enabled
|
||||
self.use_python_2 = data["use_python_2"]
|
||||
|
||||
self.label = data.get("variant_label") or name
|
||||
self.full_name = "/".join((group.name, name))
|
||||
|
|
@ -261,14 +263,32 @@ class Application:
|
|||
|
||||
|
||||
class ApplicationManager:
|
||||
def __init__(self):
|
||||
self.log = PypeLogger().get_logger(self.__class__.__name__)
|
||||
"""Load applications and tools and store them by their full name.
|
||||
|
||||
Args:
|
||||
system_settings (dict): Preloaded system settings. When passed manager
|
||||
will always use these values. Gives ability to create manager
|
||||
using different settings.
|
||||
"""
|
||||
def __init__(self, system_settings=None):
|
||||
self.log = PypeLogger.get_logger(self.__class__.__name__)
|
||||
|
||||
self.app_groups = {}
|
||||
self.applications = {}
|
||||
self.tool_groups = {}
|
||||
self.tools = {}
|
||||
|
||||
self._system_settings = system_settings
|
||||
|
||||
self.refresh()
|
||||
|
||||
def set_system_settings(self, system_settings):
|
||||
"""Ability to change init system settings.
|
||||
|
||||
This will trigger refresh of manager.
|
||||
"""
|
||||
self._system_settings = system_settings
|
||||
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
|
|
@ -278,9 +298,12 @@ class ApplicationManager:
|
|||
self.tool_groups.clear()
|
||||
self.tools.clear()
|
||||
|
||||
settings = get_system_settings(
|
||||
clear_metadata=False, exclude_locals=False
|
||||
)
|
||||
if self._system_settings is not None:
|
||||
settings = copy.deepcopy(self._system_settings)
|
||||
else:
|
||||
settings = get_system_settings(
|
||||
clear_metadata=False, exclude_locals=False
|
||||
)
|
||||
|
||||
app_defs = settings["applications"]
|
||||
for group_name, variant_defs in app_defs.items():
|
||||
|
|
@ -1224,7 +1247,7 @@ def _prepare_last_workfile(data, workdir):
|
|||
file_template = anatomy.templates["work"]["file"]
|
||||
workdir_data.update({
|
||||
"version": 1,
|
||||
"user": os.environ.get("OPENPYPE_USERNAME") or getpass.getuser(),
|
||||
"user": get_openpype_username(),
|
||||
"ext": extensions[0]
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package to deal with saving and retrieving user specific settings."""
|
||||
import os
|
||||
import json
|
||||
import getpass
|
||||
import platform
|
||||
from datetime import datetime
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import json
|
||||
|
||||
# TODO Use pype igniter logic instead of using duplicated code
|
||||
# disable lru cache in Python 2
|
||||
|
|
@ -24,11 +26,11 @@ try:
|
|||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
|
||||
import platform
|
||||
|
||||
import six
|
||||
import appdirs
|
||||
|
||||
from openpype.settings import get_local_settings
|
||||
|
||||
from .import validate_mongo_connection
|
||||
|
||||
_PLACEHOLDER = object()
|
||||
|
|
@ -538,3 +540,25 @@ def change_openpype_mongo_url(new_mongo_url):
|
|||
if existing_value is not None:
|
||||
registry.delete_item(key)
|
||||
registry.set_item(key, new_mongo_url)
|
||||
|
||||
|
||||
def get_openpype_username():
|
||||
"""OpenPype username used for templates and publishing.
|
||||
|
||||
May be different than machine's username.
|
||||
|
||||
Always returns "OPENPYPE_USERNAME" environment if is set then tries local
|
||||
settings and last option is to use `getpass.getuser()` which returns
|
||||
machine username.
|
||||
"""
|
||||
username = os.environ.get("OPENPYPE_USERNAME")
|
||||
if not username:
|
||||
local_settings = get_local_settings()
|
||||
username = (
|
||||
local_settings
|
||||
.get("general", {})
|
||||
.get("username")
|
||||
)
|
||||
if not username:
|
||||
username = getpass.getuser()
|
||||
return username
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@ class PypeFormatter(logging.Formatter):
|
|||
|
||||
if record.exc_info is not None:
|
||||
line_len = len(str(record.exc_info[1]))
|
||||
if line_len > 30:
|
||||
line_len = 30
|
||||
out = "{}\n{}\n{}\n{}\n{}".format(
|
||||
out,
|
||||
line_len * "=",
|
||||
|
|
|
|||
|
|
@ -18,10 +18,6 @@ from .webserver import (
|
|||
WebServerModule,
|
||||
IWebServerRoutes
|
||||
)
|
||||
from .user import (
|
||||
UserModule,
|
||||
IUserModule
|
||||
)
|
||||
from .idle_manager import (
|
||||
IdleManager,
|
||||
IIdleManager
|
||||
|
|
@ -60,9 +56,6 @@ __all__ = (
|
|||
"WebServerModule",
|
||||
"IWebServerRoutes",
|
||||
|
||||
"UserModule",
|
||||
"IUserModule",
|
||||
|
||||
"IdleManager",
|
||||
"IIdleManager",
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ class AfterEffectsSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline
|
|||
"AVALON_ASSET",
|
||||
"AVALON_TASK",
|
||||
"AVALON_APP_NAME",
|
||||
"OPENPYPE_USERNAME",
|
||||
"OPENPYPE_DEV",
|
||||
"OPENPYPE_LOG_NO_COLORS"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -273,7 +273,6 @@ class HarmonySubmitDeadline(
|
|||
"AVALON_ASSET",
|
||||
"AVALON_TASK",
|
||||
"AVALON_APP_NAME",
|
||||
"OPENPYPE_USERNAME",
|
||||
"OPENPYPE_DEV",
|
||||
"OPENPYPE_LOG_NO_COLORS"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -441,7 +441,6 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
"AVALON_ASSET",
|
||||
"AVALON_TASK",
|
||||
"AVALON_APP_NAME",
|
||||
"OPENPYPE_USERNAME",
|
||||
"OPENPYPE_DEV",
|
||||
"OPENPYPE_LOG_NO_COLORS"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import json
|
|||
|
||||
from openpype.api import ProjectSettings
|
||||
|
||||
from openpype.modules.ftrack.lib import ServerAction
|
||||
from openpype.modules.ftrack.lib.avalon_sync import (
|
||||
get_pype_attr,
|
||||
from openpype.modules.ftrack.lib import (
|
||||
ServerAction,
|
||||
get_openpype_attr,
|
||||
CUST_ATTR_AUTO_SYNC
|
||||
)
|
||||
|
||||
|
|
@ -159,7 +159,7 @@ class PrepareProjectServer(ServerAction):
|
|||
for key, entity in project_anatom_settings["attributes"].items():
|
||||
attribute_values_by_key[key] = entity.value
|
||||
|
||||
cust_attrs, hier_cust_attrs = get_pype_attr(self.session, True)
|
||||
cust_attrs, hier_cust_attrs = get_openpype_attr(self.session, True)
|
||||
|
||||
for attr in hier_cust_attrs:
|
||||
key = attr["key"]
|
||||
|
|
|
|||
|
|
@ -18,12 +18,15 @@ from avalon import schema
|
|||
from avalon.api import AvalonMongoDB
|
||||
|
||||
from openpype.modules.ftrack.lib import (
|
||||
get_openpype_attr,
|
||||
CUST_ATTR_ID_KEY,
|
||||
CUST_ATTR_AUTO_SYNC,
|
||||
|
||||
avalon_sync,
|
||||
|
||||
BaseEvent
|
||||
)
|
||||
from openpype.modules.ftrack.lib.avalon_sync import (
|
||||
CUST_ATTR_ID_KEY,
|
||||
CUST_ATTR_AUTO_SYNC,
|
||||
EntitySchemas
|
||||
)
|
||||
|
||||
|
|
@ -125,7 +128,7 @@ class SyncToAvalonEvent(BaseEvent):
|
|||
@property
|
||||
def avalon_cust_attrs(self):
|
||||
if self._avalon_cust_attrs is None:
|
||||
self._avalon_cust_attrs = avalon_sync.get_pype_attr(
|
||||
self._avalon_cust_attrs = get_openpype_attr(
|
||||
self.process_session, query_keys=self.cust_attr_query_keys
|
||||
)
|
||||
return self._avalon_cust_attrs
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import collections
|
||||
import ftrack_api
|
||||
from openpype.modules.ftrack.lib import BaseAction, statics_icon
|
||||
from openpype.modules.ftrack.lib.avalon_sync import get_pype_attr
|
||||
from openpype.modules.ftrack.lib import (
|
||||
BaseAction,
|
||||
statics_icon,
|
||||
get_openpype_attr
|
||||
)
|
||||
|
||||
|
||||
class CleanHierarchicalAttrsAction(BaseAction):
|
||||
|
|
@ -52,7 +55,7 @@ class CleanHierarchicalAttrsAction(BaseAction):
|
|||
)
|
||||
entity_ids_joined = ", ".join(all_entities_ids)
|
||||
|
||||
attrs, hier_attrs = get_pype_attr(session)
|
||||
attrs, hier_attrs = get_openpype_attr(session)
|
||||
|
||||
for attr in hier_attrs:
|
||||
configuration_key = attr["key"]
|
||||
|
|
|
|||
|
|
@ -2,10 +2,20 @@ import collections
|
|||
import json
|
||||
import arrow
|
||||
import ftrack_api
|
||||
from openpype.modules.ftrack.lib import BaseAction, statics_icon
|
||||
from openpype.modules.ftrack.lib.avalon_sync import (
|
||||
CUST_ATTR_ID_KEY, CUST_ATTR_GROUP, default_custom_attributes_definition
|
||||
from openpype.modules.ftrack.lib import (
|
||||
BaseAction,
|
||||
statics_icon,
|
||||
|
||||
CUST_ATTR_ID_KEY,
|
||||
CUST_ATTR_GROUP,
|
||||
CUST_ATTR_TOOLS,
|
||||
CUST_ATTR_APPLICATIONS,
|
||||
|
||||
default_custom_attributes_definition,
|
||||
app_definitions_from_app_manager,
|
||||
tool_definitions_from_app_manager
|
||||
)
|
||||
|
||||
from openpype.api import get_system_settings
|
||||
from openpype.lib import ApplicationManager
|
||||
|
||||
|
|
@ -370,24 +380,12 @@ class CustomAttributes(BaseAction):
|
|||
exc_info=True
|
||||
)
|
||||
|
||||
def app_defs_from_app_manager(self):
|
||||
app_definitions = []
|
||||
for app_name, app in self.app_manager.applications.items():
|
||||
if app.enabled and app.is_host:
|
||||
app_definitions.append({
|
||||
app_name: app.full_label
|
||||
})
|
||||
|
||||
if not app_definitions:
|
||||
app_definitions.append({"empty": "< Empty >"})
|
||||
return app_definitions
|
||||
|
||||
def applications_attribute(self, event):
|
||||
apps_data = self.app_defs_from_app_manager()
|
||||
apps_data = app_definitions_from_app_manager(self.app_manager)
|
||||
|
||||
applications_custom_attr_data = {
|
||||
"label": "Applications",
|
||||
"key": "applications",
|
||||
"key": CUST_ATTR_APPLICATIONS,
|
||||
"type": "enumerator",
|
||||
"entity_type": "show",
|
||||
"group": CUST_ATTR_GROUP,
|
||||
|
|
@ -399,19 +397,11 @@ class CustomAttributes(BaseAction):
|
|||
self.process_attr_data(applications_custom_attr_data, event)
|
||||
|
||||
def tools_attribute(self, event):
|
||||
tools_data = []
|
||||
for tool_name, tool in self.app_manager.tools.items():
|
||||
tools_data.append({
|
||||
tool_name: tool.label
|
||||
})
|
||||
|
||||
# Make sure there is at least one item
|
||||
if not tools_data:
|
||||
tools_data.append({"empty": "< Empty >"})
|
||||
tools_data = tool_definitions_from_app_manager(self.app_manager)
|
||||
|
||||
tools_custom_attr_data = {
|
||||
"label": "Tools",
|
||||
"key": "tools_env",
|
||||
"key": CUST_ATTR_TOOLS,
|
||||
"type": "enumerator",
|
||||
"is_hierarchical": True,
|
||||
"group": CUST_ATTR_GROUP,
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ from openpype.api import ProjectSettings
|
|||
|
||||
from openpype.modules.ftrack.lib import (
|
||||
BaseAction,
|
||||
statics_icon
|
||||
)
|
||||
from openpype.modules.ftrack.lib.avalon_sync import (
|
||||
get_pype_attr,
|
||||
statics_icon,
|
||||
get_openpype_attr,
|
||||
CUST_ATTR_AUTO_SYNC
|
||||
)
|
||||
|
||||
|
|
@ -162,7 +160,7 @@ class PrepareProjectLocal(BaseAction):
|
|||
for key, entity in project_anatom_settings["attributes"].items():
|
||||
attribute_values_by_key[key] = entity.value
|
||||
|
||||
cust_attrs, hier_cust_attrs = get_pype_attr(self.session, True)
|
||||
cust_attrs, hier_cust_attrs = get_openpype_attr(self.session, True)
|
||||
|
||||
for attr in hier_cust_attrs:
|
||||
key = attr["key"]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import json
|
||||
import collections
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import six
|
||||
|
|
@ -8,10 +9,10 @@ from openpype.modules import (
|
|||
ITrayModule,
|
||||
IPluginPaths,
|
||||
ITimersManager,
|
||||
IUserModule,
|
||||
ILaunchHookPaths,
|
||||
ISettingsChangeListener
|
||||
)
|
||||
from openpype.settings import SaveWarningExc
|
||||
|
||||
FTRACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
@ -32,7 +33,6 @@ class FtrackModule(
|
|||
ITrayModule,
|
||||
IPluginPaths,
|
||||
ITimersManager,
|
||||
IUserModule,
|
||||
ILaunchHookPaths,
|
||||
ISettingsChangeListener
|
||||
):
|
||||
|
|
@ -123,15 +123,86 @@ class FtrackModule(
|
|||
if self.tray_module:
|
||||
self.tray_module.stop_timer_manager()
|
||||
|
||||
def on_pype_user_change(self, username):
|
||||
"""Implementation of IUserModule interface."""
|
||||
if self.tray_module:
|
||||
self.tray_module.changed_user()
|
||||
|
||||
def on_system_settings_save(self, *_args, **_kwargs):
|
||||
def on_system_settings_save(
|
||||
self, old_value, new_value, changes, new_value_metadata
|
||||
):
|
||||
"""Implementation of ISettingsChangeListener interface."""
|
||||
# Ignore
|
||||
return
|
||||
try:
|
||||
session = self.create_ftrack_session()
|
||||
except Exception:
|
||||
self.log.warning("Couldn't create ftrack session.", exc_info=True)
|
||||
raise SaveWarningExc((
|
||||
"Saving of attributes to ftrack wasn't successful,"
|
||||
" try running Create/Update Avalon Attributes in ftrack."
|
||||
))
|
||||
|
||||
from .lib import (
|
||||
get_openpype_attr,
|
||||
CUST_ATTR_APPLICATIONS,
|
||||
CUST_ATTR_TOOLS,
|
||||
app_definitions_from_app_manager,
|
||||
tool_definitions_from_app_manager
|
||||
)
|
||||
from openpype.api import ApplicationManager
|
||||
query_keys = [
|
||||
"id",
|
||||
"key",
|
||||
"config"
|
||||
]
|
||||
custom_attributes = get_openpype_attr(
|
||||
session,
|
||||
split_hierarchical=False,
|
||||
query_keys=query_keys
|
||||
)
|
||||
app_attribute = None
|
||||
tool_attribute = None
|
||||
for custom_attribute in custom_attributes:
|
||||
key = custom_attribute["key"]
|
||||
if key == CUST_ATTR_APPLICATIONS:
|
||||
app_attribute = custom_attribute
|
||||
elif key == CUST_ATTR_TOOLS:
|
||||
tool_attribute = custom_attribute
|
||||
|
||||
app_manager = ApplicationManager(new_value_metadata)
|
||||
missing_attributes = []
|
||||
if not app_attribute:
|
||||
missing_attributes.append(CUST_ATTR_APPLICATIONS)
|
||||
else:
|
||||
config = json.loads(app_attribute["config"])
|
||||
new_data = app_definitions_from_app_manager(app_manager)
|
||||
prepared_data = []
|
||||
for item in new_data:
|
||||
for key, label in item.items():
|
||||
prepared_data.append({
|
||||
"menu": label,
|
||||
"value": key
|
||||
})
|
||||
|
||||
config["data"] = json.dumps(prepared_data)
|
||||
app_attribute["config"] = json.dumps(config)
|
||||
|
||||
if not tool_attribute:
|
||||
missing_attributes.append(CUST_ATTR_TOOLS)
|
||||
else:
|
||||
config = json.loads(tool_attribute["config"])
|
||||
new_data = tool_definitions_from_app_manager(app_manager)
|
||||
prepared_data = []
|
||||
for item in new_data:
|
||||
for key, label in item.items():
|
||||
prepared_data.append({
|
||||
"menu": label,
|
||||
"value": key
|
||||
})
|
||||
config["data"] = json.dumps(prepared_data)
|
||||
tool_attribute["config"] = json.dumps(config)
|
||||
|
||||
session.commit()
|
||||
|
||||
if missing_attributes:
|
||||
raise SaveWarningExc((
|
||||
"Couldn't find custom attribute/s ({}) to update."
|
||||
" Try running Create/Update Avalon Attributes in ftrack."
|
||||
).format(", ".join(missing_attributes)))
|
||||
|
||||
def on_project_settings_save(self, *_args, **_kwargs):
|
||||
"""Implementation of ISettingsChangeListener interface."""
|
||||
|
|
@ -139,7 +210,7 @@ class FtrackModule(
|
|||
return
|
||||
|
||||
def on_project_anatomy_save(
|
||||
self, old_value, new_value, changes, project_name
|
||||
self, old_value, new_value, changes, project_name, new_value_metadata
|
||||
):
|
||||
"""Implementation of ISettingsChangeListener interface."""
|
||||
if not project_name:
|
||||
|
|
@ -150,32 +221,49 @@ class FtrackModule(
|
|||
return
|
||||
|
||||
import ftrack_api
|
||||
from openpype.modules.ftrack.lib import avalon_sync
|
||||
from openpype.modules.ftrack.lib import get_openpype_attr
|
||||
|
||||
try:
|
||||
session = self.create_ftrack_session()
|
||||
except Exception:
|
||||
self.log.warning("Couldn't create ftrack session.", exc_info=True)
|
||||
raise SaveWarningExc((
|
||||
"Saving of attributes to ftrack wasn't successful,"
|
||||
" try running Create/Update Avalon Attributes in ftrack."
|
||||
))
|
||||
|
||||
session = self.create_ftrack_session()
|
||||
project_entity = session.query(
|
||||
"Project where full_name is \"{}\"".format(project_name)
|
||||
).first()
|
||||
|
||||
if not project_entity:
|
||||
self.log.warning((
|
||||
"Ftrack project with names \"{}\" was not found."
|
||||
" Skipping settings attributes change callback."
|
||||
))
|
||||
return
|
||||
msg = (
|
||||
"Ftrack project with name \"{}\" was not found in Ftrack."
|
||||
" Can't push attribute changes."
|
||||
).format(project_name)
|
||||
self.log.warning(msg)
|
||||
raise SaveWarningExc(msg)
|
||||
|
||||
project_id = project_entity["id"]
|
||||
|
||||
cust_attr, hier_attr = avalon_sync.get_pype_attr(session)
|
||||
cust_attr, hier_attr = get_openpype_attr(session)
|
||||
cust_attr_by_key = {attr["key"]: attr for attr in cust_attr}
|
||||
hier_attrs_by_key = {attr["key"]: attr for attr in hier_attr}
|
||||
|
||||
failed = {}
|
||||
missing = {}
|
||||
for key, value in attributes_changes.items():
|
||||
configuration = hier_attrs_by_key.get(key)
|
||||
if not configuration:
|
||||
configuration = cust_attr_by_key.get(key)
|
||||
if not configuration:
|
||||
self.log.warning(
|
||||
"Custom attribute \"{}\" was not found.".format(key)
|
||||
)
|
||||
missing[key] = value
|
||||
continue
|
||||
|
||||
# TODO add add permissions check
|
||||
# TODO add value validations
|
||||
# - value type and list items
|
||||
entity_key = collections.OrderedDict()
|
||||
|
|
@ -189,10 +277,45 @@ class FtrackModule(
|
|||
"value",
|
||||
ftrack_api.symbol.NOT_SET,
|
||||
value
|
||||
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
try:
|
||||
session.commit()
|
||||
self.log.debug(
|
||||
"Changed project custom attribute \"{}\" to \"{}\"".format(
|
||||
key, value
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to set \"{}\" to \"{}\"".format(key, value),
|
||||
exc_info=True
|
||||
)
|
||||
session.rollback()
|
||||
failed[key] = value
|
||||
|
||||
if not failed and not missing:
|
||||
return
|
||||
|
||||
error_msg = (
|
||||
"Values were not updated on Ftrack which may cause issues."
|
||||
" try running Create/Update Avalon Attributes in ftrack "
|
||||
" and resave project settings."
|
||||
)
|
||||
if missing:
|
||||
error_msg += "\nMissing Custom attributes on Ftrack: {}.".format(
|
||||
", ".join([
|
||||
'"{}"'.format(key)
|
||||
for key in missing.keys()
|
||||
])
|
||||
)
|
||||
if failed:
|
||||
joined_failed = ", ".join([
|
||||
'"{}": "{}"'.format(key, value)
|
||||
for key, value in failed.items()
|
||||
])
|
||||
error_msg += "\nFailed to set: {}".format(joined_failed)
|
||||
raise SaveWarningExc(error_msg)
|
||||
|
||||
def create_ftrack_session(self, **session_kwargs):
|
||||
import ftrack_api
|
||||
|
|
|
|||
|
|
@ -8,10 +8,13 @@ class PrePython2Support(PreLaunchHook):
|
|||
|
||||
Path to vendor modules is added to the beggining of PYTHONPATH.
|
||||
"""
|
||||
# There will be needed more granular filtering in future
|
||||
app_groups = ["maya", "nuke", "nukex", "hiero", "nukestudio", "unreal"]
|
||||
|
||||
def execute(self):
|
||||
if not self.application.use_python_2:
|
||||
return
|
||||
|
||||
self.log.info("Adding Ftrack Python 2 packages to PYTHONPATH.")
|
||||
|
||||
# Prepare vendor dir path
|
||||
python_2_vendor = os.path.join(FTRACK_MODULE_DIR, "python2_vendor")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,21 @@
|
|||
from .constants import (
|
||||
CUST_ATTR_ID_KEY,
|
||||
CUST_ATTR_AUTO_SYNC,
|
||||
CUST_ATTR_GROUP,
|
||||
CUST_ATTR_TOOLS,
|
||||
CUST_ATTR_APPLICATIONS
|
||||
)
|
||||
from . settings import (
|
||||
get_ftrack_url_from_settings,
|
||||
get_ftrack_event_mongo_info
|
||||
)
|
||||
from .custom_attributes import (
|
||||
default_custom_attributes_definition,
|
||||
app_definitions_from_app_manager,
|
||||
tool_definitions_from_app_manager,
|
||||
get_openpype_attr
|
||||
)
|
||||
|
||||
from . import avalon_sync
|
||||
from . import credentials
|
||||
from .ftrack_base_handler import BaseHandler
|
||||
|
|
@ -10,9 +24,20 @@ from .ftrack_action_handler import BaseAction, ServerAction, statics_icon
|
|||
|
||||
|
||||
__all__ = (
|
||||
"CUST_ATTR_ID_KEY",
|
||||
"CUST_ATTR_AUTO_SYNC",
|
||||
"CUST_ATTR_GROUP",
|
||||
"CUST_ATTR_TOOLS",
|
||||
"CUST_ATTR_APPLICATIONS",
|
||||
|
||||
"get_ftrack_url_from_settings",
|
||||
"get_ftrack_event_mongo_info",
|
||||
|
||||
"default_custom_attributes_definition",
|
||||
"app_definitions_from_app_manager",
|
||||
"tool_definitions_from_app_manager",
|
||||
"get_openpype_attr",
|
||||
|
||||
"avalon_sync",
|
||||
|
||||
"credentials",
|
||||
|
|
|
|||
|
|
@ -14,17 +14,21 @@ else:
|
|||
from avalon.api import AvalonMongoDB
|
||||
|
||||
import avalon
|
||||
|
||||
from openpype.api import (
|
||||
Logger,
|
||||
Anatomy,
|
||||
get_anatomy_settings
|
||||
)
|
||||
from openpype.lib import ApplicationManager
|
||||
|
||||
from .constants import CUST_ATTR_ID_KEY
|
||||
from .custom_attributes import get_openpype_attr
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
from bson.errors import InvalidId
|
||||
from pymongo import UpdateOne
|
||||
import ftrack_api
|
||||
from openpype.lib import ApplicationManager
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
|
@ -36,23 +40,6 @@ EntitySchemas = {
|
|||
"config": "openpype:config-2.0"
|
||||
}
|
||||
|
||||
# Group name of custom attributes
|
||||
CUST_ATTR_GROUP = "openpype"
|
||||
|
||||
# name of Custom attribute that stores mongo_id from avalon db
|
||||
CUST_ATTR_ID_KEY = "avalon_mongo_id"
|
||||
CUST_ATTR_AUTO_SYNC = "avalon_auto_sync"
|
||||
|
||||
|
||||
def default_custom_attributes_definition():
|
||||
json_file_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"custom_attributes.json"
|
||||
)
|
||||
with open(json_file_path, "r") as json_stream:
|
||||
data = json.load(json_stream)
|
||||
return data
|
||||
|
||||
|
||||
def check_regex(name, entity_type, in_schema=None, schema_patterns=None):
|
||||
schema_name = "asset-3.0"
|
||||
|
|
@ -91,39 +78,6 @@ def join_query_keys(keys):
|
|||
return ",".join(["\"{}\"".format(key) for key in keys])
|
||||
|
||||
|
||||
def get_pype_attr(session, split_hierarchical=True, query_keys=None):
|
||||
custom_attributes = []
|
||||
hier_custom_attributes = []
|
||||
if not query_keys:
|
||||
query_keys = [
|
||||
"id",
|
||||
"entity_type",
|
||||
"object_type_id",
|
||||
"is_hierarchical",
|
||||
"default"
|
||||
]
|
||||
# TODO remove deprecated "pype" group from query
|
||||
cust_attrs_query = (
|
||||
"select {}"
|
||||
" from CustomAttributeConfiguration"
|
||||
# Kept `pype` for Backwards Compatiblity
|
||||
" where group.name in (\"pype\", \"{}\")"
|
||||
).format(", ".join(query_keys), CUST_ATTR_GROUP)
|
||||
all_avalon_attr = session.query(cust_attrs_query).all()
|
||||
for cust_attr in all_avalon_attr:
|
||||
if split_hierarchical and cust_attr["is_hierarchical"]:
|
||||
hier_custom_attributes.append(cust_attr)
|
||||
continue
|
||||
|
||||
custom_attributes.append(cust_attr)
|
||||
|
||||
if split_hierarchical:
|
||||
# return tuple
|
||||
return custom_attributes, hier_custom_attributes
|
||||
|
||||
return custom_attributes
|
||||
|
||||
|
||||
def get_python_type_for_custom_attribute(cust_attr, cust_attr_type_name=None):
|
||||
"""Python type that should value of custom attribute have.
|
||||
|
||||
|
|
@ -921,7 +875,7 @@ class SyncEntitiesFactory:
|
|||
def set_cutom_attributes(self):
|
||||
self.log.debug("* Preparing custom attributes")
|
||||
# Get custom attributes and values
|
||||
custom_attrs, hier_attrs = get_pype_attr(
|
||||
custom_attrs, hier_attrs = get_openpype_attr(
|
||||
self.session, query_keys=self.cust_attr_query_keys
|
||||
)
|
||||
ent_types = self.session.query("select id, name from ObjectType").all()
|
||||
|
|
@ -2508,7 +2462,7 @@ class SyncEntitiesFactory:
|
|||
if new_entity_id not in p_chilren:
|
||||
self.entities_dict[parent_id]["children"].append(new_entity_id)
|
||||
|
||||
cust_attr, _ = get_pype_attr(self.session)
|
||||
cust_attr, _ = get_openpype_attr(self.session)
|
||||
for _attr in cust_attr:
|
||||
key = _attr["key"]
|
||||
if key not in av_entity["data"]:
|
||||
|
|
|
|||
12
openpype/modules/ftrack/lib/constants.py
Normal file
12
openpype/modules/ftrack/lib/constants.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Group name of custom attributes
|
||||
CUST_ATTR_GROUP = "openpype"
|
||||
|
||||
# name of Custom attribute that stores mongo_id from avalon db
|
||||
CUST_ATTR_ID_KEY = "avalon_mongo_id"
|
||||
# Auto sync of project
|
||||
CUST_ATTR_AUTO_SYNC = "avalon_auto_sync"
|
||||
|
||||
# Applications custom attribute name
|
||||
CUST_ATTR_APPLICATIONS = "applications"
|
||||
# Environment tools custom attribute
|
||||
CUST_ATTR_TOOLS = "tools_env"
|
||||
73
openpype/modules/ftrack/lib/custom_attributes.py
Normal file
73
openpype/modules/ftrack/lib/custom_attributes.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
from .constants import CUST_ATTR_GROUP
|
||||
|
||||
|
||||
def default_custom_attributes_definition():
|
||||
json_file_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"custom_attributes.json"
|
||||
)
|
||||
with open(json_file_path, "r") as json_stream:
|
||||
data = json.load(json_stream)
|
||||
return data
|
||||
|
||||
|
||||
def app_definitions_from_app_manager(app_manager):
|
||||
app_definitions = []
|
||||
for app_name, app in app_manager.applications.items():
|
||||
if app.enabled and app.is_host:
|
||||
app_definitions.append({
|
||||
app_name: app.full_label
|
||||
})
|
||||
|
||||
if not app_definitions:
|
||||
app_definitions.append({"empty": "< Empty >"})
|
||||
return app_definitions
|
||||
|
||||
|
||||
def tool_definitions_from_app_manager(app_manager):
|
||||
tools_data = []
|
||||
for tool_name, tool in app_manager.tools.items():
|
||||
tools_data.append({
|
||||
tool_name: tool.label
|
||||
})
|
||||
|
||||
# Make sure there is at least one item
|
||||
if not tools_data:
|
||||
tools_data.append({"empty": "< Empty >"})
|
||||
return tools_data
|
||||
|
||||
|
||||
def get_openpype_attr(session, split_hierarchical=True, query_keys=None):
|
||||
custom_attributes = []
|
||||
hier_custom_attributes = []
|
||||
if not query_keys:
|
||||
query_keys = [
|
||||
"id",
|
||||
"entity_type",
|
||||
"object_type_id",
|
||||
"is_hierarchical",
|
||||
"default"
|
||||
]
|
||||
# TODO remove deprecated "pype" group from query
|
||||
cust_attrs_query = (
|
||||
"select {}"
|
||||
" from CustomAttributeConfiguration"
|
||||
# Kept `pype` for Backwards Compatiblity
|
||||
" where group.name in (\"pype\", \"{}\")"
|
||||
).format(", ".join(query_keys), CUST_ATTR_GROUP)
|
||||
all_avalon_attr = session.query(cust_attrs_query).all()
|
||||
for cust_attr in all_avalon_attr:
|
||||
if split_hierarchical and cust_attr["is_hierarchical"]:
|
||||
hier_custom_attributes.append(cust_attr)
|
||||
continue
|
||||
|
||||
custom_attributes.append(cust_attr)
|
||||
|
||||
if split_hierarchical:
|
||||
# return tuple
|
||||
return custom_attributes, hier_custom_attributes
|
||||
|
||||
return custom_attributes
|
||||
|
|
@ -22,7 +22,6 @@ class LauncherAction(PypeModule, ITrayAction):
|
|||
# Register actions
|
||||
if self.tray_initialized:
|
||||
from openpype.tools.launcher import actions
|
||||
# actions.register_default_actions()
|
||||
actions.register_config_actions()
|
||||
actions_paths = self.manager.collect_plugin_paths()["actions"]
|
||||
actions.register_actions_from_paths(actions_paths)
|
||||
|
|
|
|||
|
|
@ -16,18 +16,20 @@ class ISettingsChangeListener:
|
|||
}
|
||||
"""
|
||||
@abstractmethod
|
||||
def on_system_settings_save(self, old_value, new_value, changes):
|
||||
def on_system_settings_save(
|
||||
self, old_value, new_value, changes, new_value_metadata
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def on_project_settings_save(
|
||||
self, old_value, new_value, changes, project_name
|
||||
self, old_value, new_value, changes, project_name, new_value_metadata
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def on_project_anatomy_save(
|
||||
self, old_value, new_value, changes, project_name
|
||||
self, old_value, new_value, changes, project_name, new_value_metadata
|
||||
):
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
from Qt import QtCore
|
||||
import attr
|
||||
import abc
|
||||
import six
|
||||
|
||||
from openpype.lib import PypeLogger
|
||||
|
||||
|
|
@ -20,8 +23,111 @@ ProviderRole = QtCore.Qt.UserRole + 2
|
|||
ProgressRole = QtCore.Qt.UserRole + 4
|
||||
DateRole = QtCore.Qt.UserRole + 6
|
||||
FailedRole = QtCore.Qt.UserRole + 8
|
||||
HeaderNameRole = QtCore.Qt.UserRole + 10
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class AbstractColumnFilter:
|
||||
|
||||
def __init__(self, column_name, dbcon=None):
|
||||
self.column_name = column_name
|
||||
self.dbcon = dbcon
|
||||
self._search_variants = []
|
||||
|
||||
def search_variants(self):
|
||||
"""
|
||||
Returns all flavors of search available for this column,
|
||||
"""
|
||||
return self._search_variants
|
||||
|
||||
@abc.abstractmethod
|
||||
def values(self):
|
||||
"""
|
||||
Returns dict of available values for filter {'label':'value'}
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def prepare_match_part(self, values):
|
||||
"""
|
||||
Prepares format valid for $match part from 'values
|
||||
|
||||
Args:
|
||||
values (dict): {'label': 'value'}
|
||||
Returns:
|
||||
(dict): {'COLUMN_NAME': {'$in': ['val1', 'val2']}}
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PredefinedSetFilter(AbstractColumnFilter):
|
||||
|
||||
def __init__(self, column_name, values):
|
||||
super().__init__(column_name)
|
||||
self._search_variants = ['checkbox']
|
||||
self._values = values
|
||||
if self._values and \
|
||||
list(self._values.keys())[0] == list(self._values.values())[0]:
|
||||
self._search_variants.append('text')
|
||||
|
||||
def values(self):
|
||||
return {k: v for k, v in self._values.items()}
|
||||
|
||||
def prepare_match_part(self, values):
|
||||
return {'$in': list(values.keys())}
|
||||
|
||||
|
||||
class RegexTextFilter(AbstractColumnFilter):
|
||||
|
||||
def __init__(self, column_name):
|
||||
super().__init__(column_name)
|
||||
self._search_variants = ['text']
|
||||
|
||||
def values(self):
|
||||
return {}
|
||||
|
||||
def prepare_match_part(self, values):
|
||||
""" values = {'text1 text2': 'text1 text2'} """
|
||||
if not values:
|
||||
return {}
|
||||
|
||||
regex_strs = set()
|
||||
text = list(values.keys())[0] # only single key always expected
|
||||
for word in text.split():
|
||||
regex_strs.add('.*{}.*'.format(word))
|
||||
|
||||
return {"$regex": "|".join(regex_strs),
|
||||
"$options": 'i'}
|
||||
|
||||
|
||||
class MultiSelectFilter(AbstractColumnFilter):
|
||||
|
||||
def __init__(self, column_name, values=None, dbcon=None):
|
||||
super().__init__(column_name)
|
||||
self._values = values
|
||||
self.dbcon = dbcon
|
||||
self._search_variants = ['checkbox']
|
||||
|
||||
def values(self):
|
||||
if self._values:
|
||||
return {k: v for k, v in self._values.items()}
|
||||
|
||||
recs = self.dbcon.find({'type': self.column_name}, {"name": 1,
|
||||
"_id": -1})
|
||||
values = {}
|
||||
for item in recs:
|
||||
values[item["name"]] = item["name"]
|
||||
return dict(sorted(values.items(), key=lambda it: it[1]))
|
||||
|
||||
def prepare_match_part(self, values):
|
||||
return {'$in': list(values.keys())}
|
||||
|
||||
|
||||
@attr.s
|
||||
class FilterDefinition:
|
||||
type = attr.ib()
|
||||
values = attr.ib(factory=list)
|
||||
|
||||
def pretty_size(value, suffix='B'):
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
if abs(value) < 1024.0:
|
||||
|
|
|
|||
|
|
@ -56,17 +56,31 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
"""Returns project"""
|
||||
return self._project
|
||||
|
||||
@property
|
||||
def column_filtering(self):
|
||||
return self._column_filtering
|
||||
|
||||
def rowCount(self, _index):
|
||||
return len(self._data)
|
||||
|
||||
def columnCount(self, _index):
|
||||
def columnCount(self, _index=None):
|
||||
return len(self._header)
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||
if section >= len(self.COLUMN_LABELS):
|
||||
return
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
if orientation == Qt.Horizontal:
|
||||
return self.COLUMN_LABELS[section][1]
|
||||
|
||||
if role == lib.HeaderNameRole:
|
||||
if orientation == Qt.Horizontal:
|
||||
return self.COLUMN_LABELS[section][0] # return name
|
||||
|
||||
def get_column(self, index):
|
||||
return self.COLUMN_LABELS[index]
|
||||
|
||||
def get_header_index(self, value):
|
||||
"""
|
||||
Returns index of 'value' in headers
|
||||
|
|
@ -103,7 +117,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
self._rec_loaded = 0
|
||||
|
||||
if not representations:
|
||||
self.query = self.get_default_query(load_records)
|
||||
self.query = self.get_query(load_records)
|
||||
representations = self.dbcon.aggregate(self.query)
|
||||
|
||||
self.add_page_records(self.local_site, self.remote_site,
|
||||
|
|
@ -138,7 +152,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
log.debug("fetchMore")
|
||||
items_to_fetch = min(self._total_records - self._rec_loaded,
|
||||
self.PAGE_SIZE)
|
||||
self.query = self.get_default_query(self._rec_loaded)
|
||||
self.query = self.get_query(self._rec_loaded)
|
||||
representations = self.dbcon.aggregate(self.query)
|
||||
self.beginInsertRows(index,
|
||||
self._rec_loaded,
|
||||
|
|
@ -171,7 +185,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
order = -1
|
||||
|
||||
self.sort = {self.SORT_BY_COLUMN[index]: order, '_id': 1}
|
||||
self.query = self.get_default_query()
|
||||
self.query = self.get_query()
|
||||
# import json
|
||||
# log.debug(json.dumps(self.query, indent=4).\
|
||||
# replace('False', 'false').\
|
||||
|
|
@ -180,16 +194,86 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
representations = self.dbcon.aggregate(self.query)
|
||||
self.refresh(representations)
|
||||
|
||||
def set_filter(self, word_filter):
|
||||
def set_word_filter(self, word_filter):
|
||||
"""
|
||||
Adds text value filtering
|
||||
|
||||
Args:
|
||||
word_filter (str): string inputted by user
|
||||
"""
|
||||
self.word_filter = word_filter
|
||||
self._word_filter = word_filter
|
||||
self.refresh()
|
||||
|
||||
def get_filters(self):
|
||||
"""
|
||||
Returns all available filter editors per column_name keys.
|
||||
"""
|
||||
filters = {}
|
||||
for column_name, _ in self.COLUMN_LABELS:
|
||||
filter_rec = self.COLUMN_FILTERS.get(column_name)
|
||||
if filter_rec:
|
||||
filter_rec.dbcon = self.dbcon
|
||||
filters[column_name] = filter_rec
|
||||
|
||||
return filters
|
||||
|
||||
def get_column_filter(self, index):
|
||||
"""
|
||||
Returns filter object for column 'index
|
||||
|
||||
Args:
|
||||
index(int): index of column in header
|
||||
|
||||
Returns:
|
||||
(AbstractColumnFilter)
|
||||
"""
|
||||
column_name = self._header[index]
|
||||
|
||||
filter_rec = self.COLUMN_FILTERS.get(column_name)
|
||||
if filter_rec:
|
||||
filter_rec.dbcon = self.dbcon # up-to-date db connection
|
||||
|
||||
return filter_rec
|
||||
|
||||
def set_column_filtering(self, checked_values):
|
||||
"""
|
||||
Sets dictionary used in '$match' part of MongoDB aggregate
|
||||
|
||||
Args:
|
||||
checked_values(dict): key:values ({'status':{1:"Foo",3:"Bar"}}
|
||||
|
||||
Modifies:
|
||||
self._column_filtering : {'status': {'$in': [1, 2, 3]}}
|
||||
"""
|
||||
filtering = {}
|
||||
for column_name, dict_value in checked_values.items():
|
||||
column_f = self.COLUMN_FILTERS.get(column_name)
|
||||
if not column_f:
|
||||
continue
|
||||
column_f.dbcon = self.dbcon
|
||||
filtering[column_name] = column_f.prepare_match_part(dict_value)
|
||||
|
||||
self._column_filtering = filtering
|
||||
|
||||
def get_column_filter_values(self, index):
|
||||
"""
|
||||
Returns list of available values for filtering in the column
|
||||
|
||||
Args:
|
||||
index(int): index of column in header
|
||||
|
||||
Returns:
|
||||
(dict) of value: label shown in filtering menu
|
||||
'value' is used in MongoDB query, 'label' is human readable for
|
||||
menu
|
||||
for some columns ('subset') might be 'value' and 'label' same
|
||||
"""
|
||||
filter_rec = self.get_column_filter(index)
|
||||
if not filter_rec:
|
||||
return {}
|
||||
|
||||
return filter_rec.values()
|
||||
|
||||
def set_project(self, project):
|
||||
"""
|
||||
Changes project, called after project selection is changed
|
||||
|
|
@ -251,7 +335,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
("files_count", "Files"),
|
||||
("files_size", "Size"),
|
||||
("priority", "Priority"),
|
||||
("state", "Status")
|
||||
("status", "Status")
|
||||
]
|
||||
|
||||
DEFAULT_SORT = {
|
||||
|
|
@ -259,18 +343,25 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
"_id": 1
|
||||
}
|
||||
SORT_BY_COLUMN = [
|
||||
"context.asset", # asset
|
||||
"context.subset", # subset
|
||||
"context.version", # version
|
||||
"context.representation", # representation
|
||||
"asset", # asset
|
||||
"subset", # subset
|
||||
"version", # version
|
||||
"representation", # representation
|
||||
"updated_dt_local", # local created_dt
|
||||
"updated_dt_remote", # remote created_dt
|
||||
"files_count", # count of files
|
||||
"files_size", # file size of all files
|
||||
"context.asset", # priority TODO
|
||||
"status" # state
|
||||
"status" # status
|
||||
]
|
||||
|
||||
COLUMN_FILTERS = {
|
||||
'status': lib.PredefinedSetFilter('status', lib.STATUS),
|
||||
'subset': lib.RegexTextFilter('subset'),
|
||||
'asset': lib.RegexTextFilter('asset'),
|
||||
'representation': lib.MultiSelectFilter('representation')
|
||||
}
|
||||
|
||||
refresh_started = QtCore.Signal()
|
||||
refresh_finished = QtCore.Signal()
|
||||
|
||||
|
|
@ -297,7 +388,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
files_count = attr.ib(default=None)
|
||||
files_size = attr.ib(default=None)
|
||||
priority = attr.ib(default=None)
|
||||
state = attr.ib(default=None)
|
||||
status = attr.ib(default=None)
|
||||
path = attr.ib(default=None)
|
||||
|
||||
def __init__(self, sync_server, header, project=None):
|
||||
|
|
@ -307,7 +398,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
self._project = project
|
||||
self._rec_loaded = 0
|
||||
self._total_records = 0 # how many documents query actually found
|
||||
self.word_filter = None
|
||||
self._word_filter = None
|
||||
self._column_filtering = {}
|
||||
|
||||
self._word_filter = None
|
||||
|
||||
self._initialized = False
|
||||
if not self._project or self._project == lib.DUMMY_PROJECT:
|
||||
|
|
@ -319,12 +413,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
self.local_site = self.sync_server.get_active_site(self.project)
|
||||
self.remote_site = self.sync_server.get_remote_site(self.project)
|
||||
|
||||
self.projection = self.get_default_projection()
|
||||
|
||||
self.sort = self.DEFAULT_SORT
|
||||
|
||||
self.query = self.get_default_query()
|
||||
self.default_query = list(self.get_default_query())
|
||||
self.query = self.get_query()
|
||||
self.default_query = list(self.get_query())
|
||||
|
||||
representations = self.dbcon.aggregate(self.query)
|
||||
self.refresh(representations)
|
||||
|
|
@ -359,9 +451,11 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
|
||||
if role == lib.FailedRole:
|
||||
if header_value == 'local_site':
|
||||
return item.state == lib.STATUS[2] and item.local_progress < 1
|
||||
return item.status == lib.STATUS[2] and \
|
||||
item.local_progress < 1
|
||||
if header_value == 'remote_site':
|
||||
return item.state == lib.STATUS[2] and item.remote_progress < 1
|
||||
return item.status == lib.STATUS[2] and \
|
||||
item.remote_progress < 1
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
# because of ImageDelegate
|
||||
|
|
@ -397,7 +491,6 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
remote_site)
|
||||
|
||||
for repre in result.get("paginatedResults"):
|
||||
context = repre.get("context").pop()
|
||||
files = repre.get("files", [])
|
||||
if isinstance(files, dict): # aggregate returns dictionary
|
||||
files = [files]
|
||||
|
|
@ -420,17 +513,17 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
avg_progress_local = lib.convert_progress(
|
||||
repre.get('avg_progress_local', '0'))
|
||||
|
||||
if context.get("version"):
|
||||
version = "v{:0>3d}".format(context.get("version"))
|
||||
if repre.get("version"):
|
||||
version = "v{:0>3d}".format(repre.get("version"))
|
||||
else:
|
||||
version = "master"
|
||||
|
||||
item = self.SyncRepresentation(
|
||||
repre.get("_id"),
|
||||
context.get("asset"),
|
||||
context.get("subset"),
|
||||
repre.get("asset"),
|
||||
repre.get("subset"),
|
||||
version,
|
||||
context.get("representation"),
|
||||
repre.get("representation"),
|
||||
local_updated,
|
||||
remote_updated,
|
||||
local_site,
|
||||
|
|
@ -449,7 +542,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
self._data.append(item)
|
||||
self._rec_loaded += 1
|
||||
|
||||
def get_default_query(self, limit=0):
|
||||
def get_query(self, limit=0):
|
||||
"""
|
||||
Returns basic aggregate query for main table.
|
||||
|
||||
|
|
@ -461,7 +554,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
'sync_dt' - same for remote side
|
||||
'local_site' - progress of repr on local side, 1 = finished
|
||||
'remote_site' - progress on remote side, calculates from files
|
||||
'state' -
|
||||
'status' -
|
||||
0 - in progress
|
||||
1 - failed
|
||||
2 - queued
|
||||
|
|
@ -481,7 +574,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
if limit == 0:
|
||||
limit = SyncRepresentationSummaryModel.PAGE_SIZE
|
||||
|
||||
return [
|
||||
aggr = [
|
||||
{"$match": self.get_match_part()},
|
||||
{'$unwind': '$files'},
|
||||
# merge potentially unwinded records back to single per repre
|
||||
|
|
@ -584,16 +677,26 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
'paused_local': {'$sum': '$paused_local'},
|
||||
'updated_dt_local': {'$max': "$updated_dt_local"}
|
||||
}},
|
||||
{"$project": self.projection},
|
||||
{"$sort": self.sort},
|
||||
{
|
||||
{"$project": self.projection}
|
||||
]
|
||||
|
||||
if self.column_filtering:
|
||||
aggr.append(
|
||||
{"$match": self.column_filtering}
|
||||
)
|
||||
|
||||
aggr.extend(
|
||||
[{"$sort": self.sort},
|
||||
{
|
||||
'$facet': {
|
||||
'paginatedResults': [{'$skip': self._rec_loaded},
|
||||
{'$limit': limit}],
|
||||
'totalCount': [{'$count': 'count'}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}]
|
||||
)
|
||||
|
||||
return aggr
|
||||
|
||||
def get_match_part(self):
|
||||
"""
|
||||
|
|
@ -614,22 +717,23 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
'files.sites.name': {'$all': [self.local_site,
|
||||
self.remote_site]}
|
||||
}
|
||||
if not self.word_filter:
|
||||
if not self._word_filter:
|
||||
return base_match
|
||||
else:
|
||||
regex_str = '.*{}.*'.format(self.word_filter)
|
||||
regex_str = '.*{}.*'.format(self._word_filter)
|
||||
base_match['$or'] = [
|
||||
{'context.subset': {'$regex': regex_str, '$options': 'i'}},
|
||||
{'context.asset': {'$regex': regex_str, '$options': 'i'}},
|
||||
{'context.representation': {'$regex': regex_str,
|
||||
'$options': 'i'}}]
|
||||
|
||||
if ObjectId.is_valid(self.word_filter):
|
||||
base_match['$or'] = [{'_id': ObjectId(self.word_filter)}]
|
||||
if ObjectId.is_valid(self._word_filter):
|
||||
base_match['$or'] = [{'_id': ObjectId(self._word_filter)}]
|
||||
|
||||
return base_match
|
||||
|
||||
def get_default_projection(self):
|
||||
@property
|
||||
def projection(self):
|
||||
"""
|
||||
Projection part for aggregate query.
|
||||
|
||||
|
|
@ -639,10 +743,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
(dict)
|
||||
"""
|
||||
return {
|
||||
"context.subset": 1,
|
||||
"context.asset": 1,
|
||||
"context.version": 1,
|
||||
"context.representation": 1,
|
||||
"subset": {"$first": "$context.subset"},
|
||||
"asset": {"$first": "$context.asset"},
|
||||
"version": {"$first": "$context.version"},
|
||||
"representation": {"$first": "$context.representation"},
|
||||
"data.path": 1,
|
||||
"files": 1,
|
||||
'files_count': 1,
|
||||
|
|
@ -721,7 +825,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
("remote_site", "Remote site"),
|
||||
("files_size", "Size"),
|
||||
("priority", "Priority"),
|
||||
("state", "Status")
|
||||
("status", "Status")
|
||||
]
|
||||
|
||||
PAGE_SIZE = 30
|
||||
|
|
@ -733,10 +837,15 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
"updated_dt_local", # local created_dt
|
||||
"updated_dt_remote", # remote created_dt
|
||||
"size", # remote progress
|
||||
"context.asset", # priority TODO
|
||||
"status" # state
|
||||
"size", # priority TODO
|
||||
"status" # status
|
||||
]
|
||||
|
||||
COLUMN_FILTERS = {
|
||||
'status': lib.PredefinedSetFilter('status', lib.STATUS),
|
||||
'file': lib.RegexTextFilter('file'),
|
||||
}
|
||||
|
||||
refresh_started = QtCore.Signal()
|
||||
refresh_finished = QtCore.Signal()
|
||||
|
||||
|
|
@ -759,7 +868,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
remote_progress = attr.ib(default=None)
|
||||
size = attr.ib(default=None)
|
||||
priority = attr.ib(default=None)
|
||||
state = attr.ib(default=None)
|
||||
status = attr.ib(default=None)
|
||||
tries = attr.ib(default=None)
|
||||
error = attr.ib(default=None)
|
||||
path = attr.ib(default=None)
|
||||
|
|
@ -772,9 +881,10 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
self._project = project
|
||||
self._rec_loaded = 0
|
||||
self._total_records = 0 # how many documents query actually found
|
||||
self.word_filter = None
|
||||
self._word_filter = None
|
||||
self._id = _id
|
||||
self._initialized = False
|
||||
self._column_filtering = {}
|
||||
|
||||
self.sync_server = sync_server
|
||||
# TODO think about admin mode
|
||||
|
|
@ -784,10 +894,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
|
||||
self.sort = self.DEFAULT_SORT
|
||||
|
||||
# in case we would like to hide/show some columns
|
||||
self.projection = self.get_default_projection()
|
||||
|
||||
self.query = self.get_default_query()
|
||||
self.query = self.get_query()
|
||||
representations = self.dbcon.aggregate(self.query)
|
||||
self.refresh(representations)
|
||||
|
||||
|
|
@ -821,9 +928,11 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
|
||||
if role == lib.FailedRole:
|
||||
if header_value == 'local_site':
|
||||
return item.state == lib.STATUS[2] and item.local_progress < 1
|
||||
return item.status == lib.STATUS[2] and \
|
||||
item.local_progress < 1
|
||||
if header_value == 'remote_site':
|
||||
return item.state == lib.STATUS[2] and item.remote_progress < 1
|
||||
return item.status == lib.STATUS[2] and \
|
||||
item.remote_progress < 1
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
# because of ImageDelegate
|
||||
|
|
@ -909,7 +1018,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
self._data.append(item)
|
||||
self._rec_loaded += 1
|
||||
|
||||
def get_default_query(self, limit=0):
|
||||
def get_query(self, limit=0):
|
||||
"""
|
||||
Gets query that gets used when no extra sorting, filtering or
|
||||
projecting is needed.
|
||||
|
|
@ -923,7 +1032,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
if limit == 0:
|
||||
limit = SyncRepresentationSummaryModel.PAGE_SIZE
|
||||
|
||||
return [
|
||||
aggr = [
|
||||
{"$match": self.get_match_part()},
|
||||
{"$unwind": "$files"},
|
||||
{'$addFields': {
|
||||
|
|
@ -1019,7 +1128,16 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
]}
|
||||
]}}
|
||||
}},
|
||||
{"$project": self.projection},
|
||||
{"$project": self.projection}
|
||||
]
|
||||
|
||||
if self.column_filtering:
|
||||
aggr.append(
|
||||
{"$match": self.column_filtering}
|
||||
)
|
||||
print(self.column_filtering)
|
||||
|
||||
aggr.extend([
|
||||
{"$sort": self.sort},
|
||||
{
|
||||
'$facet': {
|
||||
|
|
@ -1028,7 +1146,9 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
'totalCount': [{'$count': 'count'}]
|
||||
}
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
return aggr
|
||||
|
||||
def get_match_part(self):
|
||||
"""
|
||||
|
|
@ -1038,20 +1158,21 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
Returns:
|
||||
(dict)
|
||||
"""
|
||||
if not self.word_filter:
|
||||
if not self._word_filter:
|
||||
return {
|
||||
"type": "representation",
|
||||
"_id": self._id
|
||||
}
|
||||
else:
|
||||
regex_str = '.*{}.*'.format(self.word_filter)
|
||||
regex_str = '.*{}.*'.format(self._word_filter)
|
||||
return {
|
||||
"type": "representation",
|
||||
"_id": self._id,
|
||||
'$or': [{'files.path': {'$regex': regex_str, '$options': 'i'}}]
|
||||
}
|
||||
|
||||
def get_default_projection(self):
|
||||
@property
|
||||
def projection(self):
|
||||
"""
|
||||
Projection part for aggregate query.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from functools import partial
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
from Qt.QtCore import Qt
|
||||
|
|
@ -14,6 +15,7 @@ from openpype.api import get_local_site_id
|
|||
from openpype.lib import PypeLogger
|
||||
|
||||
from avalon.tools.delegates import pretty_timestamp
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
from openpype.modules.sync_server.tray.models import (
|
||||
SyncRepresentationSummaryModel,
|
||||
|
|
@ -40,6 +42,8 @@ class SyncProjectListWidget(ProjectListWidget):
|
|||
self.local_site = None
|
||||
self.icons = {}
|
||||
|
||||
self.layout().setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
def validate_context_change(self):
|
||||
return True
|
||||
|
||||
|
|
@ -91,7 +95,6 @@ class SyncProjectListWidget(ProjectListWidget):
|
|||
self.project_name = point_index.data(QtCore.Qt.DisplayRole)
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.setStyleSheet(style.load_stylesheet())
|
||||
actions_mapping = {}
|
||||
|
||||
if self.sync_server.is_project_paused(self.project_name):
|
||||
|
|
@ -141,16 +144,16 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
message_generated = QtCore.Signal(str)
|
||||
|
||||
default_widths = (
|
||||
("asset", 220),
|
||||
("subset", 190),
|
||||
("version", 55),
|
||||
("representation", 95),
|
||||
("local_site", 170),
|
||||
("remote_site", 170),
|
||||
("asset", 190),
|
||||
("subset", 170),
|
||||
("version", 60),
|
||||
("representation", 145),
|
||||
("local_site", 160),
|
||||
("remote_site", 160),
|
||||
("files_count", 50),
|
||||
("files_size", 60),
|
||||
("priority", 50),
|
||||
("state", 110)
|
||||
("priority", 70),
|
||||
("status", 110)
|
||||
)
|
||||
|
||||
def __init__(self, sync_server, project=None, parent=None):
|
||||
|
|
@ -162,13 +165,16 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
self.representation_id = None
|
||||
self.site_name = None # to pause/unpause representation
|
||||
|
||||
self.filter = QtWidgets.QLineEdit()
|
||||
self.filter.setPlaceholderText("Filter representations..")
|
||||
self.txt_filter = QtWidgets.QLineEdit()
|
||||
self.txt_filter.setPlaceholderText("Quick filter representations..")
|
||||
self.txt_filter.setClearButtonEnabled(True)
|
||||
self.txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"),
|
||||
QtWidgets.QLineEdit.LeadingPosition)
|
||||
|
||||
self._scrollbar_pos = None
|
||||
|
||||
top_bar_layout = QtWidgets.QHBoxLayout()
|
||||
top_bar_layout.addWidget(self.filter)
|
||||
top_bar_layout.addWidget(self.txt_filter)
|
||||
|
||||
self.table_view = QtWidgets.QTableView()
|
||||
headers = [item[0] for item in self.default_widths]
|
||||
|
|
@ -182,8 +188,6 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
QtWidgets.QAbstractItemView.SelectRows)
|
||||
self.table_view.horizontalHeader().setSortIndicator(
|
||||
-1, Qt.AscendingOrder)
|
||||
self.table_view.setSortingEnabled(True)
|
||||
self.table_view.horizontalHeader().setSortIndicatorShown(True)
|
||||
self.table_view.setAlternatingRowColors(True)
|
||||
self.table_view.verticalHeader().hide()
|
||||
|
||||
|
|
@ -195,32 +199,39 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
for column_name, width in self.default_widths:
|
||||
idx = model.get_header_index(column_name)
|
||||
self.table_view.setColumnWidth(idx, width)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(top_bar_layout)
|
||||
layout.addWidget(self.table_view)
|
||||
|
||||
self.table_view.doubleClicked.connect(self._double_clicked)
|
||||
self.filter.textChanged.connect(lambda: model.set_filter(
|
||||
self.filter.text()))
|
||||
self.txt_filter.textChanged.connect(lambda: model.set_word_filter(
|
||||
self.txt_filter.text()))
|
||||
self.table_view.customContextMenuRequested.connect(
|
||||
self._on_context_menu)
|
||||
|
||||
model.refresh_started.connect(self._save_scrollbar)
|
||||
model.refresh_finished.connect(self._set_scrollbar)
|
||||
self.table_view.model().modelReset.connect(self._set_selection)
|
||||
model.modelReset.connect(self._set_selection)
|
||||
|
||||
self.model = model
|
||||
|
||||
self.selection_model = self.table_view.selectionModel()
|
||||
self.selection_model.selectionChanged.connect(self._selection_changed)
|
||||
|
||||
horizontal_header = HorizontalHeader(self)
|
||||
|
||||
self.table_view.setHorizontalHeader(horizontal_header)
|
||||
self.table_view.setSortingEnabled(True)
|
||||
|
||||
for column_name, width in self.default_widths:
|
||||
idx = model.get_header_index(column_name)
|
||||
self.table_view.setColumnWidth(idx, width)
|
||||
|
||||
def _selection_changed(self, _new_selection):
|
||||
index = self.selection_model.currentIndex()
|
||||
self._selected_id = \
|
||||
self.table_view.model().data(index, Qt.UserRole)
|
||||
self.model.data(index, Qt.UserRole)
|
||||
|
||||
def _set_selection(self):
|
||||
"""
|
||||
|
|
@ -229,7 +240,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
Keep selection during model refresh.
|
||||
"""
|
||||
if self._selected_id:
|
||||
index = self.table_view.model().get_index(self._selected_id)
|
||||
index = self.model.get_index(self._selected_id)
|
||||
if index and index.isValid():
|
||||
mode = QtCore.QItemSelectionModel.Select | \
|
||||
QtCore.QItemSelectionModel.Rows
|
||||
|
|
@ -241,9 +252,9 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
"""
|
||||
Opens representation dialog with all files after doubleclick
|
||||
"""
|
||||
_id = self.table_view.model().data(index, Qt.UserRole)
|
||||
_id = self.model.data(index, Qt.UserRole)
|
||||
detail_window = SyncServerDetailWindow(
|
||||
self.sync_server, _id, self.table_view.model().project)
|
||||
self.sync_server, _id, self.model.project)
|
||||
detail_window.exec()
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
|
|
@ -254,13 +265,12 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
if not point_index.isValid():
|
||||
return
|
||||
|
||||
self.item = self.table_view.model()._data[point_index.row()]
|
||||
self.item = self.model._data[point_index.row()]
|
||||
self.representation_id = self.item._id
|
||||
log.debug("menu representation _id:: {}".
|
||||
format(self.representation_id))
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.setStyleSheet(style.load_stylesheet())
|
||||
actions_mapping = {}
|
||||
actions_kwargs_mapping = {}
|
||||
|
||||
|
|
@ -271,7 +281,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
|
||||
for site, progress in {local_site: local_progress,
|
||||
remote_site: remote_progress}.items():
|
||||
project = self.table_view.model().project
|
||||
project = self.model.project
|
||||
provider = self.sync_server.get_provider_for_site(project,
|
||||
site)
|
||||
if provider == 'local_drive':
|
||||
|
|
@ -291,17 +301,17 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
else:
|
||||
self.site_name = remote_site
|
||||
|
||||
if self.item.state in [lib.STATUS[0], lib.STATUS[1]]:
|
||||
if self.item.status in [lib.STATUS[0], lib.STATUS[1]]:
|
||||
action = QtWidgets.QAction("Pause")
|
||||
actions_mapping[action] = self._pause
|
||||
menu.addAction(action)
|
||||
|
||||
if self.item.state == lib.STATUS[3]:
|
||||
if self.item.status == lib.STATUS[3]:
|
||||
action = QtWidgets.QAction("Unpause")
|
||||
actions_mapping[action] = self._unpause
|
||||
menu.addAction(action)
|
||||
|
||||
# if self.item.state == lib.STATUS[1]:
|
||||
# if self.item.status == lib.STATUS[1]:
|
||||
# action = QtWidgets.QAction("Open error detail")
|
||||
# actions_mapping[action] = self._show_detail
|
||||
# menu.addAction(action)
|
||||
|
|
@ -337,10 +347,10 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
if to_run:
|
||||
to_run(**to_run_kwargs)
|
||||
|
||||
self.table_view.model().refresh()
|
||||
self.model.refresh()
|
||||
|
||||
def _pause(self):
|
||||
self.sync_server.pause_representation(self.table_view.model().project,
|
||||
self.sync_server.pause_representation(self.model.project,
|
||||
self.representation_id,
|
||||
self.site_name)
|
||||
self.site_name = None
|
||||
|
|
@ -348,7 +358,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
|
||||
def _unpause(self):
|
||||
self.sync_server.unpause_representation(
|
||||
self.table_view.model().project,
|
||||
self.model.project,
|
||||
self.representation_id,
|
||||
self.site_name)
|
||||
self.site_name = None
|
||||
|
|
@ -358,7 +368,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
# temporary here for testing, will be removed TODO
|
||||
def _add_site(self):
|
||||
log.info(self.representation_id)
|
||||
project_name = self.table_view.model().project
|
||||
project_name = self.model.project
|
||||
local_site_name = get_local_site_id()
|
||||
try:
|
||||
self.sync_server.add_site(
|
||||
|
|
@ -386,15 +396,15 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
try:
|
||||
local_site = get_local_site_id()
|
||||
self.sync_server.remove_site(
|
||||
self.table_view.model().project,
|
||||
self.model.project,
|
||||
self.representation_id,
|
||||
local_site,
|
||||
True)
|
||||
self.message_generated.emit("Site {} removed".format(local_site))
|
||||
except ValueError as exp:
|
||||
self.message_generated.emit("Error {}".format(str(exp)))
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
self.model.refresh(
|
||||
load_records=self.model._rec_loaded)
|
||||
|
||||
def _reset_local_site(self):
|
||||
"""
|
||||
|
|
@ -402,11 +412,11 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.model.project,
|
||||
self.representation_id,
|
||||
'local')
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
self.model.refresh(
|
||||
load_records=self.model._rec_loaded)
|
||||
|
||||
def _reset_remote_site(self):
|
||||
"""
|
||||
|
|
@ -414,18 +424,18 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.model.project,
|
||||
self.representation_id,
|
||||
'remote')
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
self.model.refresh(
|
||||
load_records=self.model._rec_loaded)
|
||||
|
||||
def _open_in_explorer(self, site):
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
fpath = self.item.path
|
||||
project = self.table_view.model().project
|
||||
project = self.model.project
|
||||
fpath = self.sync_server.get_local_file_path(project,
|
||||
site,
|
||||
fpath)
|
||||
|
|
@ -466,8 +476,8 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
("local_site", 185),
|
||||
("remote_site", 185),
|
||||
("size", 60),
|
||||
("priority", 25),
|
||||
("state", 110)
|
||||
("priority", 60),
|
||||
("status", 110)
|
||||
)
|
||||
|
||||
def __init__(self, sync_server, _id=None, project=None, parent=None):
|
||||
|
|
@ -482,64 +492,73 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
|
||||
self._selected_id = None
|
||||
|
||||
self.filter = QtWidgets.QLineEdit()
|
||||
self.filter.setPlaceholderText("Filter representation..")
|
||||
self.txt_filter = QtWidgets.QLineEdit()
|
||||
self.txt_filter.setPlaceholderText("Quick filter representation..")
|
||||
self.txt_filter.setClearButtonEnabled(True)
|
||||
self.txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"),
|
||||
QtWidgets.QLineEdit.LeadingPosition)
|
||||
|
||||
self._scrollbar_pos = None
|
||||
|
||||
top_bar_layout = QtWidgets.QHBoxLayout()
|
||||
top_bar_layout.addWidget(self.filter)
|
||||
top_bar_layout.addWidget(self.txt_filter)
|
||||
|
||||
self.table_view = QtWidgets.QTableView()
|
||||
table_view = QtWidgets.QTableView()
|
||||
headers = [item[0] for item in self.default_widths]
|
||||
|
||||
model = SyncRepresentationDetailModel(sync_server, headers, _id,
|
||||
project)
|
||||
self.table_view.setModel(model)
|
||||
self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.table_view.setSelectionMode(
|
||||
table_view.setModel(model)
|
||||
table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
table_view.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.SingleSelection)
|
||||
self.table_view.setSelectionBehavior(
|
||||
table_view.setSelectionBehavior(
|
||||
QtWidgets.QTableView.SelectRows)
|
||||
self.table_view.horizontalHeader().setSortIndicator(-1,
|
||||
Qt.AscendingOrder)
|
||||
self.table_view.setSortingEnabled(True)
|
||||
self.table_view.horizontalHeader().setSortIndicatorShown(True)
|
||||
self.table_view.setAlternatingRowColors(True)
|
||||
self.table_view.verticalHeader().hide()
|
||||
table_view.horizontalHeader().setSortIndicator(-1, Qt.AscendingOrder)
|
||||
table_view.horizontalHeader().setSortIndicatorShown(True)
|
||||
table_view.setAlternatingRowColors(True)
|
||||
table_view.verticalHeader().hide()
|
||||
|
||||
column = self.table_view.model().get_header_index("local_site")
|
||||
column = model.get_header_index("local_site")
|
||||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
column = self.table_view.model().get_header_index("remote_site")
|
||||
column = model.get_header_index("remote_site")
|
||||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
for column_name, width in self.default_widths:
|
||||
idx = model.get_header_index(column_name)
|
||||
self.table_view.setColumnWidth(idx, width)
|
||||
table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(top_bar_layout)
|
||||
layout.addWidget(self.table_view)
|
||||
layout.addWidget(table_view)
|
||||
|
||||
self.filter.textChanged.connect(lambda: model.set_filter(
|
||||
self.filter.text()))
|
||||
self.table_view.customContextMenuRequested.connect(
|
||||
self._on_context_menu)
|
||||
self.model = model
|
||||
|
||||
self.selection_model = table_view.selectionModel()
|
||||
self.selection_model.selectionChanged.connect(self._selection_changed)
|
||||
|
||||
horizontal_header = HorizontalHeader(self)
|
||||
|
||||
table_view.setHorizontalHeader(horizontal_header)
|
||||
table_view.setSortingEnabled(True)
|
||||
|
||||
for column_name, width in self.default_widths:
|
||||
idx = model.get_header_index(column_name)
|
||||
table_view.setColumnWidth(idx, width)
|
||||
|
||||
self.table_view = table_view
|
||||
|
||||
self.txt_filter.textChanged.connect(lambda: model.set_word_filter(
|
||||
self.txt_filter.text()))
|
||||
table_view.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
model.refresh_started.connect(self._save_scrollbar)
|
||||
model.refresh_finished.connect(self._set_scrollbar)
|
||||
self.table_view.model().modelReset.connect(self._set_selection)
|
||||
|
||||
self.selection_model = self.table_view.selectionModel()
|
||||
self.selection_model.selectionChanged.connect(self._selection_changed)
|
||||
model.modelReset.connect(self._set_selection)
|
||||
|
||||
def _selection_changed(self):
|
||||
index = self.selection_model.currentIndex()
|
||||
self._selected_id = self.table_view.model().data(index, Qt.UserRole)
|
||||
self._selected_id = self.model.data(index, Qt.UserRole)
|
||||
|
||||
def _set_selection(self):
|
||||
"""
|
||||
|
|
@ -548,7 +567,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
Keep selection during model refresh.
|
||||
"""
|
||||
if self._selected_id:
|
||||
index = self.table_view.model().get_index(self._selected_id)
|
||||
index = self.model.get_index(self._selected_id)
|
||||
if index and index.isValid():
|
||||
mode = QtCore.QItemSelectionModel.Select | \
|
||||
QtCore.QItemSelectionModel.Rows
|
||||
|
|
@ -576,10 +595,9 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
if not point_index.isValid():
|
||||
return
|
||||
|
||||
self.item = self.table_view.model()._data[point_index.row()]
|
||||
self.item = self.model._data[point_index.row()]
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.setStyleSheet(style.load_stylesheet())
|
||||
actions_mapping = {}
|
||||
actions_kwargs_mapping = {}
|
||||
|
||||
|
|
@ -590,7 +608,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
|
||||
for site, progress in {local_site: local_progress,
|
||||
remote_site: remote_progress}.items():
|
||||
project = self.table_view.model().project
|
||||
project = self.model.project
|
||||
provider = self.sync_server.get_provider_for_site(project,
|
||||
site)
|
||||
if provider == 'local_drive':
|
||||
|
|
@ -604,7 +622,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
actions_kwargs_mapping[action] = {'site': site}
|
||||
menu.addAction(action)
|
||||
|
||||
if self.item.state == lib.STATUS[2]:
|
||||
if self.item.status == lib.STATUS[2]:
|
||||
action = QtWidgets.QAction("Open error detail")
|
||||
actions_mapping[action] = self._show_detail
|
||||
menu.addAction(action)
|
||||
|
|
@ -637,12 +655,12 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.model.project,
|
||||
self.representation_id,
|
||||
'local',
|
||||
self.item._id)
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
self.model.refresh(
|
||||
load_records=self.model._rec_loaded)
|
||||
|
||||
def _reset_remote_site(self):
|
||||
"""
|
||||
|
|
@ -650,12 +668,12 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.model.project,
|
||||
self.representation_id,
|
||||
'remote',
|
||||
self.item._id)
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
self.model.refresh(
|
||||
load_records=self.model._rec_loaded)
|
||||
|
||||
def _open_in_explorer(self, site):
|
||||
if not self.item:
|
||||
|
|
@ -818,3 +836,274 @@ class SyncRepresentationErrorWindow(QtWidgets.QDialog):
|
|||
|
||||
self.setLayout(body_layout)
|
||||
self.setWindowTitle("Sync Representation Error Detail")
|
||||
|
||||
|
||||
class TransparentWidget(QtWidgets.QWidget):
|
||||
clicked = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, column_name, *args, **kwargs):
|
||||
super(TransparentWidget, self).__init__(*args, **kwargs)
|
||||
self.column_name = column_name
|
||||
# self.setStyleSheet("background: red;")
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.clicked.emit(self.column_name)
|
||||
|
||||
super(TransparentWidget, self).mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class HorizontalHeader(QtWidgets.QHeaderView):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(HorizontalHeader, self).__init__(QtCore.Qt.Horizontal, parent)
|
||||
self._parent = parent
|
||||
self.checked_values = {}
|
||||
|
||||
self.setModel(self._parent.model)
|
||||
|
||||
self.setSectionsClickable(True)
|
||||
|
||||
self.menu_items_dict = {}
|
||||
self.menu = None
|
||||
self.header_cells = []
|
||||
self.filter_buttons = {}
|
||||
|
||||
self.filter_icon = qtawesome.icon("fa.filter", color="gray")
|
||||
self.filter_set_icon = qtawesome.icon("fa.filter", color="white")
|
||||
|
||||
self.init_layout()
|
||||
|
||||
self._resetting = False
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Keep model synchronized with parent widget"""
|
||||
return self._parent.model
|
||||
|
||||
def init_layout(self):
|
||||
for column_idx in range(self.model.columnCount()):
|
||||
column_name, column_label = self.model.get_column(column_idx)
|
||||
filter_rec = self.model.get_filters().get(column_name)
|
||||
if not filter_rec:
|
||||
continue
|
||||
|
||||
icon = self.filter_icon
|
||||
button = QtWidgets.QPushButton(icon, "", self)
|
||||
|
||||
button.setFixedSize(24, 24)
|
||||
button.setStyleSheet(
|
||||
"QPushButton::menu-indicator{width:0px;}"
|
||||
"QPushButton{border: none;background: transparent;}")
|
||||
button.clicked.connect(partial(self._get_menu,
|
||||
column_name, column_idx))
|
||||
button.setFlat(True)
|
||||
self.filter_buttons[column_name] = button
|
||||
|
||||
def showEvent(self, event):
|
||||
super(HorizontalHeader, self).showEvent(event)
|
||||
|
||||
for i in range(len(self.header_cells)):
|
||||
cell_content = self.header_cells[i]
|
||||
cell_content.setGeometry(self.sectionViewportPosition(i), 0,
|
||||
self.sectionSize(i) - 1, self.height())
|
||||
|
||||
cell_content.show()
|
||||
|
||||
def _set_filter_icon(self, column_name):
|
||||
button = self.filter_buttons.get(column_name)
|
||||
if button:
|
||||
if self.checked_values.get(column_name):
|
||||
button.setIcon(self.filter_set_icon)
|
||||
else:
|
||||
button.setIcon(self.filter_icon)
|
||||
|
||||
def _reset_filter(self, column_name):
|
||||
"""
|
||||
Remove whole column from filter >> not in $match at all (faster)
|
||||
"""
|
||||
self._resetting = True # mark changes to consume them
|
||||
if self.checked_values.get(column_name) is not None:
|
||||
self.checked_values.pop(column_name)
|
||||
self._set_filter_icon(column_name)
|
||||
self._filter_and_refresh_model_and_menu(column_name, True, True)
|
||||
self._resetting = False
|
||||
|
||||
def _apply_filter(self, column_name, values, state):
|
||||
"""
|
||||
Sets 'values' to specific 'state' (checked/unchecked),
|
||||
sends to model.
|
||||
"""
|
||||
if self._resetting: # event triggered by _resetting, skip it
|
||||
return
|
||||
|
||||
self._update_checked_values(column_name, values, state)
|
||||
self._set_filter_icon(column_name)
|
||||
self._filter_and_refresh_model_and_menu(column_name, True, False)
|
||||
|
||||
def _apply_text_filter(self, column_name, items, line_edit):
|
||||
"""
|
||||
Resets all checkboxes, prefers inserted text.
|
||||
"""
|
||||
le_text = line_edit.text()
|
||||
self._update_checked_values(column_name, items, 0) # reset other
|
||||
if self.checked_values.get(column_name) is not None or \
|
||||
le_text == '':
|
||||
self.checked_values.pop(column_name) # reset during typing
|
||||
|
||||
if le_text:
|
||||
self._update_checked_values(column_name, {le_text: le_text}, 2)
|
||||
self._set_filter_icon(column_name)
|
||||
self._filter_and_refresh_model_and_menu(column_name, True, True)
|
||||
|
||||
def _filter_and_refresh_model_and_menu(self, column_name,
|
||||
model=True, menu=True):
|
||||
"""
|
||||
Refresh model and its content and possibly menu for big changes.
|
||||
"""
|
||||
if model:
|
||||
self.model.set_column_filtering(self.checked_values)
|
||||
self.model.refresh()
|
||||
if menu:
|
||||
self._menu_refresh(column_name)
|
||||
|
||||
def _get_menu(self, column_name, index):
|
||||
"""Prepares content of menu for 'column_name'"""
|
||||
menu = QtWidgets.QMenu(self)
|
||||
filter_rec = self.model.get_filters()[column_name]
|
||||
self.menu_items_dict[column_name] = filter_rec.values()
|
||||
|
||||
# text filtering only if labels same as values, not if codes are used
|
||||
if 'text' in filter_rec.search_variants():
|
||||
line_edit = QtWidgets.QLineEdit(menu)
|
||||
line_edit.setClearButtonEnabled(True)
|
||||
line_edit.addAction(self.filter_icon,
|
||||
QtWidgets.QLineEdit.LeadingPosition)
|
||||
|
||||
line_edit.setFixedHeight(line_edit.height())
|
||||
txt = ""
|
||||
if self.checked_values.get(column_name):
|
||||
txt = list(self.checked_values.get(column_name).keys())[0]
|
||||
line_edit.setText(txt)
|
||||
|
||||
action_le = QtWidgets.QWidgetAction(menu)
|
||||
action_le.setDefaultWidget(line_edit)
|
||||
line_edit.textChanged.connect(
|
||||
partial(self._apply_text_filter, column_name,
|
||||
filter_rec.values(), line_edit))
|
||||
menu.addAction(action_le)
|
||||
menu.addSeparator()
|
||||
|
||||
if 'checkbox' in filter_rec.search_variants():
|
||||
action_all = QtWidgets.QAction("All", self)
|
||||
action_all.triggered.connect(partial(self._reset_filter,
|
||||
column_name))
|
||||
menu.addAction(action_all)
|
||||
|
||||
action_none = QtWidgets.QAction("Unselect all", self)
|
||||
state_unchecked = 0
|
||||
action_none.triggered.connect(partial(self._apply_filter,
|
||||
column_name,
|
||||
filter_rec.values(),
|
||||
state_unchecked))
|
||||
menu.addAction(action_none)
|
||||
menu.addSeparator()
|
||||
|
||||
# nothing explicitly >> ALL implicitly >> first time
|
||||
if self.checked_values.get(column_name) is None:
|
||||
checked_keys = self.menu_items_dict[column_name].keys()
|
||||
else:
|
||||
checked_keys = self.checked_values[column_name]
|
||||
|
||||
for value, label in self.menu_items_dict[column_name].items():
|
||||
checkbox = QtWidgets.QCheckBox(str(label), menu)
|
||||
|
||||
# temp
|
||||
checkbox.setStyleSheet("QCheckBox{spacing: 5px;"
|
||||
"padding:5px 5px 5px 5px;}")
|
||||
if value in checked_keys:
|
||||
checkbox.setChecked(True)
|
||||
|
||||
action = QtWidgets.QWidgetAction(menu)
|
||||
action.setDefaultWidget(checkbox)
|
||||
|
||||
checkbox.stateChanged.connect(partial(self._apply_filter,
|
||||
column_name, {value: label}))
|
||||
menu.addAction(action)
|
||||
|
||||
self.menu = menu
|
||||
|
||||
self._show_menu(index, menu)
|
||||
|
||||
def _show_menu(self, index, menu):
|
||||
"""Shows 'menu' under header column of 'index'"""
|
||||
global_pos_point = self.mapToGlobal(
|
||||
QtCore.QPoint(self.sectionViewportPosition(index), 0))
|
||||
menu.setMinimumWidth(self.sectionSize(index))
|
||||
menu.setMinimumHeight(self.height())
|
||||
menu.exec_(QtCore.QPoint(global_pos_point.x(),
|
||||
global_pos_point.y() + self.height()))
|
||||
|
||||
def _menu_refresh(self, column_name):
|
||||
"""
|
||||
Reset boxes after big change - word filtering or reset
|
||||
"""
|
||||
for action in self.menu.actions():
|
||||
if not isinstance(action, QtWidgets.QWidgetAction):
|
||||
continue
|
||||
|
||||
widget = action.defaultWidget()
|
||||
if not isinstance(widget, QtWidgets.QCheckBox):
|
||||
continue
|
||||
|
||||
if not self.checked_values.get(column_name) or \
|
||||
widget.text() in self.checked_values[column_name].values():
|
||||
widget.setChecked(True)
|
||||
else:
|
||||
widget.setChecked(False)
|
||||
|
||||
def _update_checked_values(self, column_name, values, state):
|
||||
"""
|
||||
Modify dictionary of set values in columns for filtering.
|
||||
|
||||
Modifies 'self.checked_values'
|
||||
"""
|
||||
copy_menu_items = dict(self.menu_items_dict[column_name])
|
||||
checked = self.checked_values.get(column_name, copy_menu_items)
|
||||
set_items = dict(values.items()) # prevent dict change during loop
|
||||
for value, label in set_items.items():
|
||||
if state == 2 and label: # checked
|
||||
checked[value] = label
|
||||
elif state == 0 and checked.get(value):
|
||||
checked.pop(value)
|
||||
|
||||
self.checked_values[column_name] = checked
|
||||
|
||||
def paintEvent(self, event):
|
||||
self._fix_size()
|
||||
super(HorizontalHeader, self).paintEvent(event)
|
||||
|
||||
def _fix_size(self):
|
||||
for column_idx in range(self.model.columnCount()):
|
||||
vis_index = self.visualIndex(column_idx)
|
||||
index = self.logicalIndex(vis_index)
|
||||
section_width = self.sectionSize(index)
|
||||
|
||||
column_name = self.model.headerData(column_idx,
|
||||
QtCore.Qt.Horizontal,
|
||||
lib.HeaderNameRole)
|
||||
button = self.filter_buttons.get(column_name)
|
||||
if not button:
|
||||
continue
|
||||
|
||||
pos_x = self.sectionViewportPosition(
|
||||
index) + section_width - self.height()
|
||||
|
||||
pos_y = 0
|
||||
if button.height() < self.height():
|
||||
pos_y = int((self.height() - button.height()) / 2)
|
||||
button.setGeometry(
|
||||
pos_x,
|
||||
pos_y,
|
||||
self.height(),
|
||||
self.height())
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
from .user_module import (
|
||||
UserModule,
|
||||
IUserModule
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"UserModule",
|
||||
"IUserModule"
|
||||
)
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import json
|
||||
from aiohttp.web_response import Response
|
||||
|
||||
|
||||
class UserModuleRestApi:
|
||||
def __init__(self, user_module, server_manager):
|
||||
self.module = user_module
|
||||
self.server_manager = server_manager
|
||||
|
||||
self.prefix = "/user"
|
||||
|
||||
self.register()
|
||||
|
||||
def register(self):
|
||||
self.server_manager.add_route(
|
||||
"GET",
|
||||
self.prefix + "/username",
|
||||
self.get_username
|
||||
)
|
||||
self.server_manager.add_route(
|
||||
"GET",
|
||||
self.prefix + "/show_widget",
|
||||
self.show_user_widget
|
||||
)
|
||||
|
||||
async def get_username(self, request):
|
||||
return Response(
|
||||
status=200,
|
||||
body=json.dumps(self.module.cred, indent=4),
|
||||
content_type="application/json"
|
||||
)
|
||||
|
||||
async def show_user_widget(self, request):
|
||||
self.module.action_show_widget.trigger()
|
||||
return Response(status=200)
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
import getpass
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import six
|
||||
import appdirs
|
||||
|
||||
from .. import (
|
||||
PypeModule,
|
||||
ITrayModule,
|
||||
IWebServerRoutes
|
||||
)
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class IUserModule:
|
||||
"""Interface for other modules to use user change callbacks."""
|
||||
|
||||
@abstractmethod
|
||||
def on_pype_user_change(self, username):
|
||||
"""What should happen on Pype user change."""
|
||||
pass
|
||||
|
||||
|
||||
class UserModule(PypeModule, ITrayModule, IWebServerRoutes):
|
||||
cred_folder_path = os.path.normpath(
|
||||
appdirs.user_data_dir('pype-app', 'pype')
|
||||
)
|
||||
cred_filename = 'user_info.json'
|
||||
env_name = "OPENPYPE_USERNAME"
|
||||
|
||||
name = "user"
|
||||
|
||||
def initialize(self, modules_settings):
|
||||
user_settings = modules_settings[self.name]
|
||||
self.enabled = user_settings["enabled"]
|
||||
|
||||
self.callbacks_on_user_change = []
|
||||
self.cred = {}
|
||||
self.cred_path = os.path.normpath(os.path.join(
|
||||
self.cred_folder_path, self.cred_filename
|
||||
))
|
||||
|
||||
# Tray attributes
|
||||
self.widget_login = None
|
||||
self.action_show_widget = None
|
||||
|
||||
self.rest_api_obj = None
|
||||
|
||||
def tray_init(self):
|
||||
from .widget_user import UserWidget
|
||||
self.widget_login = UserWidget(self)
|
||||
|
||||
self.load_credentials()
|
||||
|
||||
def register_callback_on_user_change(self, callback):
|
||||
self.callbacks_on_user_change.append(callback)
|
||||
|
||||
def tray_start(self):
|
||||
"""Store credentials to env and preset them to widget"""
|
||||
username = ""
|
||||
if self.cred:
|
||||
username = self.cred.get("username") or ""
|
||||
|
||||
os.environ[self.env_name] = username
|
||||
self.widget_login.set_user(username)
|
||||
|
||||
def tray_exit(self):
|
||||
"""Nothing special for User."""
|
||||
return
|
||||
|
||||
def get_user(self):
|
||||
return self.cred.get("username") or getpass.getuser()
|
||||
|
||||
def webserver_initialization(self, server_manager):
|
||||
"""Implementation of IWebServerRoutes interface."""
|
||||
from .rest_api import UserModuleRestApi
|
||||
|
||||
self.rest_api_obj = UserModuleRestApi(self, server_manager)
|
||||
|
||||
def connect_with_modules(self, enabled_modules):
|
||||
for module in enabled_modules:
|
||||
if isinstance(module, IUserModule):
|
||||
self.callbacks_on_user_change.append(
|
||||
module.on_pype_user_change
|
||||
)
|
||||
|
||||
# Definition of Tray menu
|
||||
def tray_menu(self, parent_menu):
|
||||
from Qt import QtWidgets
|
||||
"""Add menu or action to Tray(or parent)'s menu"""
|
||||
action = QtWidgets.QAction("Username", parent_menu)
|
||||
action.triggered.connect(self.show_widget)
|
||||
parent_menu.addAction(action)
|
||||
parent_menu.addSeparator()
|
||||
|
||||
self.action_show_widget = action
|
||||
|
||||
def load_credentials(self):
|
||||
"""Get credentials from JSON file """
|
||||
credentials = {}
|
||||
try:
|
||||
file = open(self.cred_path, "r")
|
||||
credentials = json.load(file)
|
||||
file.close()
|
||||
|
||||
self.cred = credentials
|
||||
username = credentials.get("username")
|
||||
if username:
|
||||
self.log.debug("Loaded Username \"{}\"".format(username))
|
||||
else:
|
||||
self.log.debug("Pype Username is not set")
|
||||
|
||||
return credentials
|
||||
|
||||
except FileNotFoundError:
|
||||
return self.save_credentials(getpass.getuser())
|
||||
|
||||
except json.decoder.JSONDecodeError:
|
||||
self.log.warning((
|
||||
"File where users credentials should be stored"
|
||||
" has invalid json format. Loading system username."
|
||||
))
|
||||
return self.save_credentials(getpass.getuser())
|
||||
|
||||
def change_credentials(self, username):
|
||||
self.save_credentials(username)
|
||||
for callback in self.callbacks_on_user_change:
|
||||
try:
|
||||
callback(username)
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to execute callback \"{}\".".format(
|
||||
str(callback)
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def save_credentials(self, username):
|
||||
"""Save credentials to JSON file, env and widget"""
|
||||
if username is None:
|
||||
username = ""
|
||||
|
||||
username = str(username).strip()
|
||||
|
||||
self.cred = {"username": username}
|
||||
os.environ[self.env_name] = username
|
||||
if self.widget_login:
|
||||
self.widget_login.set_user(username)
|
||||
try:
|
||||
file = open(self.cred_path, "w")
|
||||
file.write(json.dumps(self.cred))
|
||||
file.close()
|
||||
self.log.debug("Username \"{}\" stored".format(username))
|
||||
except Exception:
|
||||
self.log.error(
|
||||
"Could not store username to file \"{}\"".format(
|
||||
self.cred_path
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
return self.cred
|
||||
|
||||
def show_widget(self):
|
||||
"""Show dialog to enter credentials"""
|
||||
self.widget_login.show()
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
from Qt import QtCore, QtGui, QtWidgets
|
||||
from avalon import style
|
||||
from openpype import resources
|
||||
|
||||
|
||||
class UserWidget(QtWidgets.QWidget):
|
||||
|
||||
MIN_WIDTH = 300
|
||||
|
||||
def __init__(self, module):
|
||||
|
||||
super(UserWidget, self).__init__()
|
||||
|
||||
self.module = module
|
||||
|
||||
# Style
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowTitle("Username Settings")
|
||||
self.setMinimumWidth(self.MIN_WIDTH)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowCloseButtonHint |
|
||||
QtCore.Qt.WindowMinimizeButtonHint
|
||||
)
|
||||
|
||||
self.setLayout(self._main())
|
||||
|
||||
def show(self, *args, **kwargs):
|
||||
super().show(*args, **kwargs)
|
||||
# Move widget to center of active screen on show
|
||||
screen = QtWidgets.QApplication.desktop().screen()
|
||||
screen_center = lambda self: (
|
||||
screen.rect().center() - self.rect().center()
|
||||
)
|
||||
self.move(screen_center(self))
|
||||
|
||||
def _main(self):
|
||||
main_layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
form_layout = QtWidgets.QFormLayout()
|
||||
form_layout.setContentsMargins(10, 15, 10, 5)
|
||||
|
||||
label_username = QtWidgets.QLabel("Username:")
|
||||
label_username.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
||||
label_username.setTextFormat(QtCore.Qt.RichText)
|
||||
|
||||
input_username = QtWidgets.QLineEdit()
|
||||
input_username.setPlaceholderText(
|
||||
QtCore.QCoreApplication.translate("main", "e.g. John Smith")
|
||||
)
|
||||
|
||||
form_layout.addRow(label_username, input_username)
|
||||
|
||||
btn_save = QtWidgets.QPushButton("Save")
|
||||
btn_save.clicked.connect(self.click_save)
|
||||
|
||||
btn_cancel = QtWidgets.QPushButton("Cancel")
|
||||
btn_cancel.clicked.connect(self.close)
|
||||
|
||||
btn_group = QtWidgets.QHBoxLayout()
|
||||
btn_group.addStretch(1)
|
||||
btn_group.addWidget(btn_save)
|
||||
btn_group.addWidget(btn_cancel)
|
||||
|
||||
main_layout.addLayout(form_layout)
|
||||
main_layout.addLayout(btn_group)
|
||||
|
||||
self.input_username = input_username
|
||||
|
||||
return main_layout
|
||||
|
||||
def set_user(self, username):
|
||||
self.input_username.setText(username)
|
||||
|
||||
def click_save(self):
|
||||
# all what should happen - validations and saving into appsdir
|
||||
username = self.input_username.text()
|
||||
self.module.change_credentials(username)
|
||||
self._close_widget()
|
||||
|
||||
def closeEvent(self, event):
|
||||
event.ignore()
|
||||
self._close_widget()
|
||||
|
||||
def _close_widget(self):
|
||||
self.hide()
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import getpass
|
||||
import pyblish.api
|
||||
from openpype.lib import get_openpype_username
|
||||
|
||||
|
||||
class CollectCurrentUserPype(pyblish.api.ContextPlugin):
|
||||
|
|
@ -11,9 +12,6 @@ class CollectCurrentUserPype(pyblish.api.ContextPlugin):
|
|||
label = "Collect Pype User"
|
||||
|
||||
def process(self, context):
|
||||
user = os.getenv("OPENPYPE_USERNAME", "").strip()
|
||||
if not user:
|
||||
user = context.data.get("user", getpass.getuser())
|
||||
|
||||
user = get_openpype_username()
|
||||
context.data["user"] = user
|
||||
self.log.debug("Colected user \"{}\"".format(user))
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
"harmony.palette",
|
||||
"editorial",
|
||||
"background",
|
||||
"camerarig"
|
||||
"camerarig",
|
||||
"redshiftproxy"
|
||||
]
|
||||
exclude_families = ["clip"]
|
||||
db_representation_context_keys = [
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
from .exceptions import (
|
||||
SaveWarningExc
|
||||
)
|
||||
from .lib import (
|
||||
get_system_settings,
|
||||
get_project_settings,
|
||||
get_current_project_settings,
|
||||
get_anatomy_settings,
|
||||
get_environments
|
||||
get_environments,
|
||||
get_local_settings
|
||||
)
|
||||
from .entities import (
|
||||
SystemSettings,
|
||||
|
|
@ -12,11 +16,14 @@ from .entities import (
|
|||
|
||||
|
||||
__all__ = (
|
||||
"SaveWarningExc",
|
||||
|
||||
"get_system_settings",
|
||||
"get_project_settings",
|
||||
"get_current_project_settings",
|
||||
"get_anatomy_settings",
|
||||
"get_environments",
|
||||
"get_local_settings",
|
||||
|
||||
"SystemSettings",
|
||||
"ProjectSettings"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
},
|
||||
"variants": {
|
||||
"2022": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\Maya2022\\bin\\maya.exe"
|
||||
|
|
@ -39,6 +40,7 @@
|
|||
}
|
||||
},
|
||||
"2020": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\Maya2020\\bin\\maya.exe"
|
||||
|
|
@ -58,6 +60,7 @@
|
|||
}
|
||||
},
|
||||
"2019": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\Maya2019\\bin\\maya.exe"
|
||||
|
|
@ -77,6 +80,7 @@
|
|||
}
|
||||
},
|
||||
"2018": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\Maya2018\\bin\\maya.exe"
|
||||
|
|
@ -118,6 +122,7 @@
|
|||
},
|
||||
"variants": {
|
||||
"13-0": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke13.0v1\\Nuke13.0.exe"
|
||||
|
|
@ -135,6 +140,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"12-2": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke12.2v3\\Nuke12.2.exe"
|
||||
|
|
@ -152,6 +158,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"12-0": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe"
|
||||
|
|
@ -169,6 +176,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"11-3": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe"
|
||||
|
|
@ -186,6 +194,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"11-2": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe"
|
||||
|
|
@ -227,6 +236,7 @@
|
|||
},
|
||||
"variants": {
|
||||
"13-0": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke13.0v1\\Nuke13.0.exe"
|
||||
|
|
@ -250,6 +260,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"12-2": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke12.2v3\\Nuke12.2.exe"
|
||||
|
|
@ -273,6 +284,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"12-0": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe"
|
||||
|
|
@ -296,6 +308,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"11-3": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe"
|
||||
|
|
@ -319,6 +332,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"11-2": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe"
|
||||
|
|
@ -366,6 +380,7 @@
|
|||
},
|
||||
"variants": {
|
||||
"13-0": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke13.0v1\\Nuke13.0.exe"
|
||||
|
|
@ -389,6 +404,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"12-2": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke12.2v3\\Nuke12.2.exe"
|
||||
|
|
@ -412,6 +428,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"12-0": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe"
|
||||
|
|
@ -435,6 +452,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"11-3": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe"
|
||||
|
|
@ -458,6 +476,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"11-2": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
|
|
@ -503,6 +522,7 @@
|
|||
},
|
||||
"variants": {
|
||||
"13-0": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke13.0v1\\Nuke13.0.exe"
|
||||
|
|
@ -526,6 +546,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"12-2": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke12.2v3\\Nuke12.2.exe"
|
||||
|
|
@ -549,6 +570,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"12-0": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe"
|
||||
|
|
@ -572,6 +594,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"11-3": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe"
|
||||
|
|
@ -595,6 +618,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"11-2": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe"
|
||||
|
|
@ -657,6 +681,7 @@
|
|||
"16": {
|
||||
"enabled": true,
|
||||
"variant_label": "16",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
|
|
@ -672,6 +697,7 @@
|
|||
"9": {
|
||||
"enabled": true,
|
||||
"variant_label": "9",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Blackmagic Design\\Fusion 9\\Fusion.exe"
|
||||
|
|
@ -735,6 +761,7 @@
|
|||
"16": {
|
||||
"enabled": true,
|
||||
"variant_label": "16",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:/Program Files/Blackmagic Design/DaVinci Resolve/Resolve.exe"
|
||||
|
|
@ -770,6 +797,7 @@
|
|||
},
|
||||
"variants": {
|
||||
"18-5": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Side Effects Software\\Houdini 18.5.499\\bin\\houdini.exe"
|
||||
|
|
@ -785,6 +813,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"18": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
|
|
@ -798,6 +827,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"17": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
|
|
@ -832,6 +862,7 @@
|
|||
},
|
||||
"variants": {
|
||||
"2-83": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Blender Foundation\\Blender 2.83\\blender.exe"
|
||||
|
|
@ -853,6 +884,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"2-90": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Blender Foundation\\Blender 2.90\\blender.exe"
|
||||
|
|
@ -874,6 +906,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"2-91": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Blender Foundation\\Blender 2.91\\blender.exe"
|
||||
|
|
@ -914,6 +947,7 @@
|
|||
"20": {
|
||||
"enabled": true,
|
||||
"variant_label": "20",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
|
|
@ -929,6 +963,7 @@
|
|||
"17": {
|
||||
"enabled": true,
|
||||
"variant_label": "17",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [
|
||||
|
|
@ -955,6 +990,7 @@
|
|||
},
|
||||
"variants": {
|
||||
"animation_11-64bits": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\TVPaint Developpement\\TVPaint Animation 11 (64bits)\\TVPaint Animation 11 (64bits).exe"
|
||||
|
|
@ -970,6 +1006,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"animation_11-32bits": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files (x86)\\TVPaint Developpement\\TVPaint Animation 11 (32bits)\\TVPaint Animation 11 (32bits).exe"
|
||||
|
|
@ -1005,6 +1042,7 @@
|
|||
"2020": {
|
||||
"enabled": true,
|
||||
"variant_label": "2020",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Adobe\\Adobe Photoshop 2020\\Photoshop.exe"
|
||||
|
|
@ -1022,6 +1060,7 @@
|
|||
"2021": {
|
||||
"enabled": true,
|
||||
"variant_label": "2021",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Adobe\\Adobe Photoshop 2021\\Photoshop.exe"
|
||||
|
|
@ -1053,6 +1092,7 @@
|
|||
"2020": {
|
||||
"enabled": true,
|
||||
"variant_label": "2020",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
""
|
||||
|
|
@ -1070,6 +1110,7 @@
|
|||
"2021": {
|
||||
"enabled": true,
|
||||
"variant_label": "2021",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Adobe\\Adobe After Effects 2021\\Support Files\\AfterFX.exe"
|
||||
|
|
@ -1098,6 +1139,7 @@
|
|||
"local": {
|
||||
"enabled": true,
|
||||
"variant_label": "Local",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
|
|
@ -1119,11 +1161,11 @@
|
|||
"host_name": "unreal",
|
||||
"environment": {
|
||||
"AVALON_UNREAL_PLUGIN": "{OPENPYPE_REPOS_ROOT}/repos/avalon-unreal-integration",
|
||||
"OPENPYPE_LOG_NO_COLORS": "True",
|
||||
"QT_PREFERRED_BINDING": "PySide"
|
||||
"OPENPYPE_LOG_NO_COLORS": "True"
|
||||
},
|
||||
"variants": {
|
||||
"4-24": {
|
||||
"4-26": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
|
|
@ -1143,6 +1185,7 @@
|
|||
"environment": {},
|
||||
"variants": {
|
||||
"python_3-7": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
|
|
@ -1156,6 +1199,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"python_2-7": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
|
|
@ -1169,6 +1213,7 @@
|
|||
"environment": {}
|
||||
},
|
||||
"terminal": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
|
|
@ -1195,6 +1240,7 @@
|
|||
"environment": {},
|
||||
"variants": {
|
||||
"1-1": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
|
|
|
|||
|
|
@ -161,9 +161,6 @@
|
|||
"log_viewer": {
|
||||
"enabled": true
|
||||
},
|
||||
"user": {
|
||||
"enabled": true
|
||||
},
|
||||
"standalonepublish_tool": {
|
||||
"enabled": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -376,7 +376,10 @@ class BoolEntity(InputEntity):
|
|||
|
||||
def _item_initalization(self):
|
||||
self.valid_value_types = (bool, )
|
||||
self.value_on_not_set = True
|
||||
value_on_not_set = self.convert_to_valid_type(
|
||||
self.schema_data.get("default", True)
|
||||
)
|
||||
self.value_on_not_set = value_on_not_set
|
||||
|
||||
|
||||
class TextEntity(InputEntity):
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from openpype.settings.constants import (
|
|||
PROJECT_ANATOMY_KEY,
|
||||
KEY_REGEX
|
||||
)
|
||||
from openpype.settings.exceptions import SaveWarningExc
|
||||
|
||||
from openpype.settings.lib import (
|
||||
DEFAULTS_DIR,
|
||||
|
|
@ -724,8 +725,19 @@ class ProjectSettings(RootEntity):
|
|||
project_settings = settings_value.get(PROJECT_SETTINGS_KEY) or {}
|
||||
project_anatomy = settings_value.get(PROJECT_ANATOMY_KEY) or {}
|
||||
|
||||
save_project_settings(self.project_name, project_settings)
|
||||
save_project_anatomy(self.project_name, project_anatomy)
|
||||
warnings = []
|
||||
try:
|
||||
save_project_settings(self.project_name, project_settings)
|
||||
except SaveWarningExc as exc:
|
||||
warnings.extend(exc.warnings)
|
||||
|
||||
try:
|
||||
save_project_anatomy(self.project_name, project_anatomy)
|
||||
except SaveWarningExc as exc:
|
||||
warnings.extend(exc.warnings)
|
||||
|
||||
if warnings:
|
||||
raise SaveWarningExc(warnings)
|
||||
|
||||
def _validate_defaults_to_save(self, value):
|
||||
"""Valiations of default values before save."""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
[
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "use_python_2",
|
||||
"label": "Use Python 2",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "path",
|
||||
"key": "executables",
|
||||
|
|
|
|||
|
|
@ -154,20 +154,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "user",
|
||||
"label": "User setting",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "standalonepublish_tool",
|
||||
|
|
|
|||
11
openpype/settings/exceptions.py
Normal file
11
openpype/settings/exceptions.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class SaveSettingsValidation(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SaveWarningExc(SaveSettingsValidation):
|
||||
def __init__(self, warnings):
|
||||
if isinstance(warnings, str):
|
||||
warnings = [warnings]
|
||||
self.warnings = warnings
|
||||
msg = " | ".join(warnings)
|
||||
super(SaveWarningExc, self).__init__(msg)
|
||||
|
|
@ -4,6 +4,9 @@ import functools
|
|||
import logging
|
||||
import platform
|
||||
import copy
|
||||
from .exceptions import (
|
||||
SaveWarningExc
|
||||
)
|
||||
from .constants import (
|
||||
M_OVERRIDEN_KEY,
|
||||
M_ENVIRONMENT_KEY,
|
||||
|
|
@ -101,8 +104,14 @@ def save_studio_settings(data):
|
|||
|
||||
For saving of data cares registered Settings handler.
|
||||
|
||||
Warning messages are not logged as module raising them should log it within
|
||||
it's logger.
|
||||
|
||||
Args:
|
||||
data(dict): Overrides data with metadata defying studio overrides.
|
||||
|
||||
Raises:
|
||||
SaveWarningExc: If any module raises the exception.
|
||||
"""
|
||||
# Notify Pype modules
|
||||
from openpype.modules import ModulesManager, ISettingsChangeListener
|
||||
|
|
@ -110,15 +119,25 @@ def save_studio_settings(data):
|
|||
old_data = get_system_settings()
|
||||
default_values = get_default_settings()[SYSTEM_SETTINGS_KEY]
|
||||
new_data = apply_overrides(default_values, copy.deepcopy(data))
|
||||
new_data_with_metadata = copy.deepcopy(new_data)
|
||||
clear_metadata_from_settings(new_data)
|
||||
|
||||
changes = calculate_changes(old_data, new_data)
|
||||
modules_manager = ModulesManager(_system_settings=new_data)
|
||||
|
||||
warnings = []
|
||||
for module in modules_manager.get_enabled_modules():
|
||||
if isinstance(module, ISettingsChangeListener):
|
||||
module.on_system_settings_save(old_data, new_data, changes)
|
||||
try:
|
||||
module.on_system_settings_save(
|
||||
old_data, new_data, changes, new_data_with_metadata
|
||||
)
|
||||
except SaveWarningExc as exc:
|
||||
warnings.extend(exc.warnings)
|
||||
|
||||
return _SETTINGS_HANDLER.save_studio_settings(data)
|
||||
_SETTINGS_HANDLER.save_studio_settings(data)
|
||||
if warnings:
|
||||
raise SaveWarningExc(warnings)
|
||||
|
||||
|
||||
@require_handler
|
||||
|
|
@ -130,10 +149,16 @@ def save_project_settings(project_name, overrides):
|
|||
|
||||
For saving of data cares registered Settings handler.
|
||||
|
||||
Warning messages are not logged as module raising them should log it within
|
||||
it's logger.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name for which overrides are passed.
|
||||
Default project's value is None.
|
||||
overrides(dict): Overrides data with metadata defying studio overrides.
|
||||
|
||||
Raises:
|
||||
SaveWarningExc: If any module raises the exception.
|
||||
"""
|
||||
# Notify Pype modules
|
||||
from openpype.modules import ModulesManager, ISettingsChangeListener
|
||||
|
|
@ -151,17 +176,29 @@ def save_project_settings(project_name, overrides):
|
|||
old_data = get_default_project_settings(exclude_locals=True)
|
||||
new_data = apply_overrides(default_values, copy.deepcopy(overrides))
|
||||
|
||||
new_data_with_metadata = copy.deepcopy(new_data)
|
||||
clear_metadata_from_settings(new_data)
|
||||
|
||||
changes = calculate_changes(old_data, new_data)
|
||||
modules_manager = ModulesManager()
|
||||
warnings = []
|
||||
for module in modules_manager.get_enabled_modules():
|
||||
if isinstance(module, ISettingsChangeListener):
|
||||
module.on_project_settings_save(
|
||||
old_data, new_data, project_name, changes
|
||||
)
|
||||
try:
|
||||
module.on_project_settings_save(
|
||||
old_data,
|
||||
new_data,
|
||||
project_name,
|
||||
changes,
|
||||
new_data_with_metadata
|
||||
)
|
||||
except SaveWarningExc as exc:
|
||||
warnings.extend(exc.warnings)
|
||||
|
||||
return _SETTINGS_HANDLER.save_project_settings(project_name, overrides)
|
||||
_SETTINGS_HANDLER.save_project_settings(project_name, overrides)
|
||||
|
||||
if warnings:
|
||||
raise SaveWarningExc(warnings)
|
||||
|
||||
|
||||
@require_handler
|
||||
|
|
@ -173,10 +210,16 @@ def save_project_anatomy(project_name, anatomy_data):
|
|||
|
||||
For saving of data cares registered Settings handler.
|
||||
|
||||
Warning messages are not logged as module raising them should log it within
|
||||
it's logger.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name for which overrides are passed.
|
||||
Default project's value is None.
|
||||
overrides(dict): Overrides data with metadata defying studio overrides.
|
||||
|
||||
Raises:
|
||||
SaveWarningExc: If any module raises the exception.
|
||||
"""
|
||||
# Notify Pype modules
|
||||
from openpype.modules import ModulesManager, ISettingsChangeListener
|
||||
|
|
@ -194,17 +237,29 @@ def save_project_anatomy(project_name, anatomy_data):
|
|||
old_data = get_default_anatomy_settings(exclude_locals=True)
|
||||
new_data = apply_overrides(default_values, copy.deepcopy(anatomy_data))
|
||||
|
||||
new_data_with_metadata = copy.deepcopy(new_data)
|
||||
clear_metadata_from_settings(new_data)
|
||||
|
||||
changes = calculate_changes(old_data, new_data)
|
||||
modules_manager = ModulesManager()
|
||||
warnings = []
|
||||
for module in modules_manager.get_enabled_modules():
|
||||
if isinstance(module, ISettingsChangeListener):
|
||||
module.on_project_anatomy_save(
|
||||
old_data, new_data, changes, project_name
|
||||
)
|
||||
try:
|
||||
module.on_project_anatomy_save(
|
||||
old_data,
|
||||
new_data,
|
||||
changes,
|
||||
project_name,
|
||||
new_data_with_metadata
|
||||
)
|
||||
except SaveWarningExc as exc:
|
||||
warnings.extend(exc.warnings)
|
||||
|
||||
return _SETTINGS_HANDLER.save_project_anatomy(project_name, anatomy_data)
|
||||
_SETTINGS_HANDLER.save_project_anatomy(project_name, anatomy_data)
|
||||
|
||||
if warnings:
|
||||
raise SaveWarningExc(warnings)
|
||||
|
||||
|
||||
@require_handler
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import importlib
|
||||
|
||||
from avalon import api, lib, style
|
||||
from avalon import api, style
|
||||
from openpype import PLUGINS_DIR
|
||||
from openpype.api import Logger, resources
|
||||
from openpype.lib import (
|
||||
ApplictionExecutableNotFound,
|
||||
|
|
@ -10,81 +10,6 @@ from openpype.lib import (
|
|||
from Qt import QtWidgets, QtGui
|
||||
|
||||
|
||||
class ProjectManagerAction(api.Action):
|
||||
name = "projectmanager"
|
||||
label = "Project Manager"
|
||||
icon = "gear"
|
||||
order = 999 # at the end
|
||||
|
||||
def is_compatible(self, session):
|
||||
return "AVALON_PROJECT" in session
|
||||
|
||||
def process(self, session, **kwargs):
|
||||
return lib.launch(
|
||||
executable="python",
|
||||
args=[
|
||||
"-u", "-m", "avalon.tools.projectmanager",
|
||||
session['AVALON_PROJECT']
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class LoaderAction(api.Action):
|
||||
name = "loader"
|
||||
label = "Loader"
|
||||
icon = "cloud-download"
|
||||
order = 998
|
||||
|
||||
def is_compatible(self, session):
|
||||
return "AVALON_PROJECT" in session
|
||||
|
||||
def process(self, session, **kwargs):
|
||||
return lib.launch(
|
||||
executable="python",
|
||||
args=[
|
||||
"-u", "-m", "avalon.tools.loader", session['AVALON_PROJECT']
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class LoaderLibrary(api.Action):
|
||||
name = "loader_os"
|
||||
label = "Library Loader"
|
||||
icon = "book"
|
||||
order = 997 # at the end
|
||||
|
||||
def is_compatible(self, session):
|
||||
return True
|
||||
|
||||
def process(self, session, **kwargs):
|
||||
return lib.launch(
|
||||
executable="python",
|
||||
args=["-u", "-m", "avalon.tools.libraryloader"]
|
||||
)
|
||||
|
||||
|
||||
def register_default_actions():
|
||||
"""Register default actions for Launcher"""
|
||||
api.register_plugin(api.Action, ProjectManagerAction)
|
||||
api.register_plugin(api.Action, LoaderAction)
|
||||
api.register_plugin(api.Action, LoaderLibrary)
|
||||
|
||||
|
||||
def register_config_actions():
|
||||
"""Register actions from the configuration for Launcher"""
|
||||
|
||||
module_name = os.environ["AVALON_CONFIG"]
|
||||
config = importlib.import_module(module_name)
|
||||
if not hasattr(config, "register_launcher_actions"):
|
||||
print(
|
||||
"Current configuration `%s` has no 'register_launcher_actions'"
|
||||
% config.__name__
|
||||
)
|
||||
return
|
||||
|
||||
config.register_launcher_actions()
|
||||
|
||||
|
||||
def register_actions_from_paths(paths):
|
||||
if not paths:
|
||||
return
|
||||
|
|
@ -106,6 +31,13 @@ def register_actions_from_paths(paths):
|
|||
api.register_plugin_path(api.Action, path)
|
||||
|
||||
|
||||
def register_config_actions():
|
||||
"""Register actions from the configuration for Launcher"""
|
||||
|
||||
actions_dir = os.path.join(PLUGINS_DIR, "actions")
|
||||
register_actions_from_paths([actions_dir])
|
||||
|
||||
|
||||
def register_environment_actions():
|
||||
"""Register actions from AVALON_ACTIONS for Launcher."""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import getpass
|
||||
|
||||
from Qt import QtWidgets
|
||||
|
||||
|
||||
|
|
@ -5,16 +7,29 @@ class LocalGeneralWidgets(QtWidgets.QWidget):
|
|||
def __init__(self, parent):
|
||||
super(LocalGeneralWidgets, self).__init__(parent)
|
||||
|
||||
username_input = QtWidgets.QLineEdit(self)
|
||||
username_input.setPlaceholderText(getpass.getuser())
|
||||
|
||||
layout = QtWidgets.QFormLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout.addRow("OpenPype Username", username_input)
|
||||
|
||||
self.username_input = username_input
|
||||
|
||||
def update_local_settings(self, value):
|
||||
return
|
||||
|
||||
# RETURNING EARLY TO HIDE WIDGET WITHOUT CONTENT
|
||||
username = ""
|
||||
if value:
|
||||
username = value.get("username", username)
|
||||
self.username_input.setText(username)
|
||||
|
||||
def settings_value(self):
|
||||
# Add changed
|
||||
# If these have changed then
|
||||
output = {}
|
||||
# TEMPORARILY EMPTY AS THERE IS NOTHING TO PUT HERE
|
||||
|
||||
username = self.username_input.text()
|
||||
if username:
|
||||
output["username"] = username
|
||||
# Do not return output yet since we don't have mechanism to save or
|
||||
# load these data through api calls
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ class LocalSettingsWidget(QtWidgets.QWidget):
|
|||
|
||||
general_widget = LocalGeneralWidgets(general_content)
|
||||
general_layout.addWidget(general_widget)
|
||||
general_expand_widget.hide()
|
||||
|
||||
self.main_layout.addWidget(general_expand_widget)
|
||||
|
||||
|
|
@ -127,9 +126,9 @@ class LocalSettingsWidget(QtWidgets.QWidget):
|
|||
self.system_settings.reset()
|
||||
self.project_settings.reset()
|
||||
|
||||
# self.general_widget.update_local_settings(
|
||||
# value.get(LOCAL_GENERAL_KEY)
|
||||
# )
|
||||
self.general_widget.update_local_settings(
|
||||
value.get(LOCAL_GENERAL_KEY)
|
||||
)
|
||||
self.app_widget.update_local_settings(
|
||||
value.get(LOCAL_APPS_KEY)
|
||||
)
|
||||
|
|
@ -139,9 +138,9 @@ class LocalSettingsWidget(QtWidgets.QWidget):
|
|||
|
||||
def settings_value(self):
|
||||
output = {}
|
||||
# general_value = self.general_widget.settings_value()
|
||||
# if general_value:
|
||||
# output[LOCAL_GENERAL_KEY] = general_value
|
||||
general_value = self.general_widget.settings_value()
|
||||
if general_value:
|
||||
output[LOCAL_GENERAL_KEY] = general_value
|
||||
|
||||
app_value = self.app_widget.settings_value()
|
||||
if app_value:
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ from openpype.settings.entities import (
|
|||
SchemaError
|
||||
)
|
||||
|
||||
from openpype.settings.lib import get_system_settings
|
||||
from openpype.settings import SaveWarningExc
|
||||
from .widgets import ProjectListWidget
|
||||
|
||||
from . import lib
|
||||
|
|
@ -272,6 +272,22 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
# not required.
|
||||
self.reset()
|
||||
|
||||
except SaveWarningExc as exc:
|
||||
warnings = [
|
||||
"<b>Settings were saved but few issues happened.</b>"
|
||||
]
|
||||
for item in exc.warnings:
|
||||
warnings.append(item.replace("\n", "<br>"))
|
||||
|
||||
msg = "<br><br>".join(warnings)
|
||||
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
dialog.setText(msg)
|
||||
dialog.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
dialog.exec_()
|
||||
|
||||
self.reset()
|
||||
|
||||
except Exception as exc:
|
||||
formatted_traceback = traceback.format_exception(*sys.exc_info())
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 911bd8999ab0030d0f7412dde6fd545c1a73b62d
|
||||
Subproject commit 807e8577a0268580a2934ba38889911adad26eb1
|
||||
|
|
@ -691,3 +691,27 @@ under selected hierarchies and match them with shapes loaded with rig (published
|
|||
under `input_SET`). This mechanism uses *cbId* attribute on those shapes.
|
||||
If match is found shapes are connected using their `outMesh` and `outMesh`. Thus you can easily connect existing animation to loaded rig.
|
||||
:::
|
||||
|
||||
## Using Redshift Proxies
|
||||
|
||||
OpenPype supports working with Redshift Proxy files. You can create Redshift Proxy from almost
|
||||
any hierarchy in Maya and it will be included there. Redshift can export animation
|
||||
proxy file per frame.
|
||||
|
||||
### Creating Redshift Proxy
|
||||
|
||||
To mark data to publish as Redshift Proxy, select them in Maya and - **OpenPype → Create ...** and
|
||||
then select **Redshift Proxy**. You can name your subset and hit **Create** button.
|
||||
|
||||
You can enable animation in Attribute Editor:
|
||||
|
||||

|
||||
|
||||
### Publishing Redshift Proxies
|
||||
|
||||
Once data are marked as Redshift Proxy instance, they can be published - **OpenPype → Publish ...**
|
||||
|
||||
### Using Redshift Proxies
|
||||
|
||||
Published proxy files can be loaded with OpenPype Loader. It will create mesh and attach Redshift Proxy
|
||||
parameters to it - Redshift will then represent proxy with bounding box.
|
||||
|
|
|
|||
BIN
website/docs/assets/maya-create_rs_proxy.jpg
Normal file
BIN
website/docs/assets/maya-create_rs_proxy.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Loading…
Add table
Add a link
Reference in a new issue