From 3d25f0cd837eb3742ed1c3082cf63a257e4fb178 Mon Sep 17 00:00:00 2001 From: mkolar Date: Wed, 14 Apr 2021 16:31:23 +0000 Subject: [PATCH 01/18] Create draft PR for #1345 From e25a06ea492be7744cc05f15e91a7756e5db09d7 Mon Sep 17 00:00:00 2001 From: simonebarbieri Date: Wed, 14 Apr 2021 16:31:26 +0000 Subject: [PATCH 02/18] Create draft PR for #1345 From e1efa554233db4b641f13333c5517449c7921f69 Mon Sep 17 00:00:00 2001 From: simonebarbieri Date: Wed, 14 Apr 2021 16:31:32 +0000 Subject: [PATCH 03/18] Create draft PR for #1345 From b3cfb1b68ccf9a7e673eca818e6bbbeb1e65850d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 16 Apr 2021 11:48:52 +0100 Subject: [PATCH 04/18] Added loader for Alembic --- .../unreal/plugins/load/load_staticmeshfbx.py | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py index dbea1d5951..b9efb6c0fc 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py @@ -155,3 +155,154 @@ class StaticMeshFBXLoader(api.Loader): if len(asset_content) == 0: unreal.EditorAssetLibrary.delete_directory(parent_path) + +class StaticMeshAlembicLoader(api.Loader): + """Load Unreal StaticMesh from Alembic""" + + families = ["model", "unrealStaticMesh"] + label = "Import Alembic Static Mesh" + representations = ["abc"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, data): + """ + Load and containerise representation into Content Browser. + + This is two step process. First, import FBX to temporary path and + then call `containerise()` on it - this moves all content to new + directory and then it will create AssetContainer there and imprint it + with metadata. This will mark this path as container. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + data (dict): Those would be data to be imprinted. This is not used + now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ + + # Create directory for asset and avalon container + root = "/Game/Avalon/Assets" + asset = context.get('asset').get('name') + suffix = "_CON" + if asset: + asset_name = "{}_{}".format(asset, name) + else: + asset_name = "{}".format(name) + + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + "{}/{}/{}".format(root, asset, name), suffix="") + + container_name += suffix + + unreal.EditorAssetLibrary.make_directory(asset_dir) + + task = unreal.AssetImportTask() + + task.set_editor_property('filename', self.fname) + task.set_editor_property('destination_path', asset_dir) + task.set_editor_property('destination_name', asset_name) + task.set_editor_property('replace_existing', False) + task.set_editor_property('automated', True) + task.set_editor_property('save', True) + + # set import options here + # TODO: it seems that Unreal is ignoring any setting from python, + # at least in Unreal 4.24. Need to test in 4.26. + options = unreal.AbcImportSettings() + options.set_editor_property( + 'import_type', unreal.AlembicImportType.STATIC_MESH) + + task.options = options + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + + # Create Asset Container + lib.create_avalon_container( + container=container_name, path=asset_dir) + + data = { + "schema": "openpype:container-2.0", + "id": pipeline.AVALON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"] + } + unreal_pipeline.imprint( + "{}/{}".format(asset_dir, container_name), data) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + return asset_content + + def update(self, container, representation): + name = container["asset_name"] + source_path = api.get_representation_path(representation) + destination_path = container["namespace"] + + task = unreal.AssetImportTask() + + task.set_editor_property('filename', source_path) + task.set_editor_property('destination_path', destination_path) + # strip suffix + task.set_editor_property('destination_name', name) + task.set_editor_property('replace_existing', True) + task.set_editor_property('automated', True) + task.set_editor_property('save', True) + + # set import options here + # TODO: it seems that Unreal is ignoring any setting from python, + # at least in Unreal 4.24. Need to test in 4.26. + options = unreal.AbcImportSettings() + options.set_editor_property( + 'import_type', unreal.AlembicImportType.STATIC_MESH) + + task.options = options + # do import fbx and replace existing data + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + container_path = "{}/{}".format(container["namespace"], + container["objectName"]) + # update metadata + unreal_pipeline.imprint( + container_path, + { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + }) + + asset_content = unreal.EditorAssetLibrary.list_assets( + destination_path, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + def remove(self, container): + path = container["namespace"] + parent_path = os.path.dirname(path) + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) From 74fe75264072db7c74c00515face4490a4f731b5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 16 Apr 2021 11:53:41 +0100 Subject: [PATCH 05/18] Hound fixes --- openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py index b9efb6c0fc..539edee286 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py @@ -156,6 +156,7 @@ class StaticMeshFBXLoader(api.Loader): if len(asset_content) == 0: unreal.EditorAssetLibrary.delete_directory(parent_path) + class StaticMeshAlembicLoader(api.Loader): """Load Unreal StaticMesh from Alembic""" @@ -215,7 +216,7 @@ class StaticMeshAlembicLoader(api.Loader): task.set_editor_property('save', True) # set import options here - # TODO: it seems that Unreal is ignoring any setting from python, + # TODO: it seems that Unreal is ignoring any setting from python, # at least in Unreal 4.24. Need to test in 4.26. options = unreal.AbcImportSettings() options.set_editor_property( @@ -268,7 +269,7 @@ class StaticMeshAlembicLoader(api.Loader): task.set_editor_property('save', True) # set import options here - # TODO: it seems that Unreal is ignoring any setting from python, + # TODO: it seems that Unreal is ignoring any setting from python, # at least in Unreal 4.24. Need to test in 4.26. options = unreal.AbcImportSettings() options.set_editor_property( From 45aec6ebbc498c6b2e7b933f31b5c373359ce0f7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 16 Apr 2021 14:36:50 +0100 Subject: [PATCH 06/18] Updated comments --- openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py index 539edee286..07961a9140 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py @@ -216,8 +216,7 @@ class StaticMeshAlembicLoader(api.Loader): task.set_editor_property('save', True) # set import options here - # TODO: it seems that Unreal is ignoring any setting from python, - # at least in Unreal 4.24. Need to test in 4.26. + # Unreal 4.24 ignores the settings. It works with Unreal 4.26 options = unreal.AbcImportSettings() options.set_editor_property( 'import_type', unreal.AlembicImportType.STATIC_MESH) @@ -269,8 +268,7 @@ class StaticMeshAlembicLoader(api.Loader): task.set_editor_property('save', True) # set import options here - # TODO: it seems that Unreal is ignoring any setting from python, - # at least in Unreal 4.24. Need to test in 4.26. + # Unreal 4.24 ignores the settings. It works with Unreal 4.26 options = unreal.AbcImportSettings() options.set_editor_property( 'import_type', unreal.AlembicImportType.STATIC_MESH) From 5f36346079f82bd57bc042d8825716de30293806 Mon Sep 17 00:00:00 2001 From: kalisp Date: Thu, 22 Apr 2021 08:51:22 +0000 Subject: [PATCH 07/18] Create draft PR for #1336 From 01966abfef38cb3c65dba99139ef585e489879fb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 22 Apr 2021 15:10:29 +0100 Subject: [PATCH 08/18] Loading of alembics as Static or Skeletal Mesh, or Geometry Cache --- .../load/load_alembic_geometrycache.py | 162 ++++++++++++++++++ .../plugins/load/load_alembic_skeletalmesh.py | 156 +++++++++++++++++ .../plugins/load/load_alembic_staticmesh.py | 156 +++++++++++++++++ .../unreal/plugins/load/load_staticmeshfbx.py | 151 ---------------- 4 files changed, 474 insertions(+), 151 deletions(-) create mode 100644 openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py create mode 100644 openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py create mode 100644 openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py b/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py new file mode 100644 index 0000000000..a9279bf6e0 --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py @@ -0,0 +1,162 @@ +import os + +from avalon import api, pipeline +from avalon.unreal import lib +from avalon.unreal import pipeline as unreal_pipeline +import unreal + + +class PointCacheAlembicLoader(api.Loader): + """Load Point Cache from Alembic""" + + families = ["model", "pointcache"] + label = "Import Alembic Point Cache" + representations = ["abc"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, data): + """ + Load and containerise representation into Content Browser. + + This is two step process. First, import FBX to temporary path and + then call `containerise()` on it - this moves all content to new + directory and then it will create AssetContainer there and imprint it + with metadata. This will mark this path as container. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + data (dict): Those would be data to be imprinted. This is not used + now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ + + # Create directory for asset and avalon container + root = "/Game/Avalon/Assets" + asset = context.get('asset').get('name') + suffix = "_CON" + if asset: + asset_name = "{}_{}".format(asset, name) + else: + asset_name = "{}".format(name) + + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + "{}/{}/{}".format(root, asset, name), suffix="") + + container_name += suffix + + unreal.EditorAssetLibrary.make_directory(asset_dir) + + task = unreal.AssetImportTask() + + task.set_editor_property('filename', self.fname) + task.set_editor_property('destination_path', asset_dir) + task.set_editor_property('destination_name', asset_name) + task.set_editor_property('replace_existing', False) + task.set_editor_property('automated', True) + task.set_editor_property('save', True) + + # set import options here + # Unreal 4.24 ignores the settings. It works with Unreal 4.26 + options = unreal.AbcImportSettings() + options.set_editor_property( + 'import_type', unreal.AlembicImportType.GEOMETRY_CACHE) + + options.geometry_cache_settings.set_editor_property( + 'flatten_tracks', False) + + task.options = options + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + + # Create Asset Container + lib.create_avalon_container( + container=container_name, path=asset_dir) + + data = { + "schema": "openpype:container-2.0", + "id": pipeline.AVALON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"] + } + unreal_pipeline.imprint( + "{}/{}".format(asset_dir, container_name), data) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + return asset_content + + def update(self, container, representation): + name = container["asset_name"] + source_path = api.get_representation_path(representation) + destination_path = container["namespace"] + + task = unreal.AssetImportTask() + + task.set_editor_property('filename', source_path) + task.set_editor_property('destination_path', destination_path) + # strip suffix + task.set_editor_property('destination_name', name) + task.set_editor_property('replace_existing', True) + task.set_editor_property('automated', True) + task.set_editor_property('save', True) + + # set import options here + # Unreal 4.24 ignores the settings. It works with Unreal 4.26 + options = unreal.AbcImportSettings() + options.set_editor_property( + 'import_type', unreal.AlembicImportType.GEOMETRY_CACHE) + + options.geometry_cache_settings.set_editor_property( + 'flatten_tracks', False) + + task.options = options + # do import fbx and replace existing data + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + container_path = "{}/{}".format(container["namespace"], + container["objectName"]) + # update metadata + unreal_pipeline.imprint( + container_path, + { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + }) + + asset_content = unreal.EditorAssetLibrary.list_assets( + destination_path, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + def remove(self, container): + path = container["namespace"] + parent_path = os.path.dirname(path) + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py new file mode 100644 index 0000000000..b652af0b89 --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py @@ -0,0 +1,156 @@ +import os + +from avalon import api, pipeline +from avalon.unreal import lib +from avalon.unreal import pipeline as unreal_pipeline +import unreal + + +class SkeletalMeshAlembicLoader(api.Loader): + """Load Unreal SkeletalMesh from Alembic""" + + families = ["pointcache"] + label = "Import Alembic Skeletal Mesh" + representations = ["abc"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, data): + """ + Load and containerise representation into Content Browser. + + This is two step process. First, import FBX to temporary path and + then call `containerise()` on it - this moves all content to new + directory and then it will create AssetContainer there and imprint it + with metadata. This will mark this path as container. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + data (dict): Those would be data to be imprinted. This is not used + now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ + + # Create directory for asset and avalon container + root = "/Game/Avalon/Assets" + asset = context.get('asset').get('name') + suffix = "_CON" + if asset: + asset_name = "{}_{}".format(asset, name) + else: + asset_name = "{}".format(name) + + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + "{}/{}/{}".format(root, asset, name), suffix="") + + container_name += suffix + + unreal.EditorAssetLibrary.make_directory(asset_dir) + + task = unreal.AssetImportTask() + + task.set_editor_property('filename', self.fname) + task.set_editor_property('destination_path', asset_dir) + task.set_editor_property('destination_name', asset_name) + task.set_editor_property('replace_existing', False) + task.set_editor_property('automated', True) + task.set_editor_property('save', True) + + # set import options here + # Unreal 4.24 ignores the settings. It works with Unreal 4.26 + options = unreal.AbcImportSettings() + options.set_editor_property( + 'import_type', unreal.AlembicImportType.SKELETAL) + + task.options = options + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + + # Create Asset Container + lib.create_avalon_container( + container=container_name, path=asset_dir) + + data = { + "schema": "openpype:container-2.0", + "id": pipeline.AVALON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"] + } + unreal_pipeline.imprint( + "{}/{}".format(asset_dir, container_name), data) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + return asset_content + + def update(self, container, representation): + name = container["asset_name"] + source_path = api.get_representation_path(representation) + destination_path = container["namespace"] + + task = unreal.AssetImportTask() + + task.set_editor_property('filename', source_path) + task.set_editor_property('destination_path', destination_path) + # strip suffix + task.set_editor_property('destination_name', name) + task.set_editor_property('replace_existing', True) + task.set_editor_property('automated', True) + task.set_editor_property('save', True) + + # set import options here + # Unreal 4.24 ignores the settings. It works with Unreal 4.26 + options = unreal.AbcImportSettings() + options.set_editor_property( + 'import_type', unreal.AlembicImportType.SKELETAL) + + task.options = options + # do import fbx and replace existing data + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + container_path = "{}/{}".format(container["namespace"], + container["objectName"]) + # update metadata + unreal_pipeline.imprint( + container_path, + { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + }) + + asset_content = unreal.EditorAssetLibrary.list_assets( + destination_path, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + def remove(self, container): + path = container["namespace"] + parent_path = os.path.dirname(path) + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py new file mode 100644 index 0000000000..12b9320f72 --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py @@ -0,0 +1,156 @@ +import os + +from avalon import api, pipeline +from avalon.unreal import lib +from avalon.unreal import pipeline as unreal_pipeline +import unreal + + +class StaticMeshAlembicLoader(api.Loader): + """Load Unreal StaticMesh from Alembic""" + + families = ["model"] + label = "Import Alembic Static Mesh" + representations = ["abc"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, data): + """ + Load and containerise representation into Content Browser. + + This is two step process. First, import FBX to temporary path and + then call `containerise()` on it - this moves all content to new + directory and then it will create AssetContainer there and imprint it + with metadata. This will mark this path as container. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + data (dict): Those would be data to be imprinted. This is not used + now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ + + # Create directory for asset and avalon container + root = "/Game/Avalon/Assets" + asset = context.get('asset').get('name') + suffix = "_CON" + if asset: + asset_name = "{}_{}".format(asset, name) + else: + asset_name = "{}".format(name) + + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + "{}/{}/{}".format(root, asset, name), suffix="") + + container_name += suffix + + unreal.EditorAssetLibrary.make_directory(asset_dir) + + task = unreal.AssetImportTask() + + task.set_editor_property('filename', self.fname) + task.set_editor_property('destination_path', asset_dir) + task.set_editor_property('destination_name', asset_name) + task.set_editor_property('replace_existing', False) + task.set_editor_property('automated', True) + task.set_editor_property('save', True) + + # set import options here + # Unreal 4.24 ignores the settings. It works with Unreal 4.26 + options = unreal.AbcImportSettings() + options.set_editor_property( + 'import_type', unreal.AlembicImportType.STATIC_MESH) + + task.options = options + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + + # Create Asset Container + lib.create_avalon_container( + container=container_name, path=asset_dir) + + data = { + "schema": "openpype:container-2.0", + "id": pipeline.AVALON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"] + } + unreal_pipeline.imprint( + "{}/{}".format(asset_dir, container_name), data) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + return asset_content + + def update(self, container, representation): + name = container["asset_name"] + source_path = api.get_representation_path(representation) + destination_path = container["namespace"] + + task = unreal.AssetImportTask() + + task.set_editor_property('filename', source_path) + task.set_editor_property('destination_path', destination_path) + # strip suffix + task.set_editor_property('destination_name', name) + task.set_editor_property('replace_existing', True) + task.set_editor_property('automated', True) + task.set_editor_property('save', True) + + # set import options here + # Unreal 4.24 ignores the settings. It works with Unreal 4.26 + options = unreal.AbcImportSettings() + options.set_editor_property( + 'import_type', unreal.AlembicImportType.STATIC_MESH) + + task.options = options + # do import fbx and replace existing data + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + container_path = "{}/{}".format(container["namespace"], + container["objectName"]) + # update metadata + unreal_pipeline.imprint( + container_path, + { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + }) + + asset_content = unreal.EditorAssetLibrary.list_assets( + destination_path, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + def remove(self, container): + path = container["namespace"] + parent_path = os.path.dirname(path) + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py index 07961a9140..dcb566fa4c 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py @@ -1,7 +1,6 @@ import os from avalon import api, pipeline -from avalon import unreal as avalon_unreal from avalon.unreal import lib from avalon.unreal import pipeline as unreal_pipeline import unreal @@ -155,153 +154,3 @@ class StaticMeshFBXLoader(api.Loader): if len(asset_content) == 0: unreal.EditorAssetLibrary.delete_directory(parent_path) - - -class StaticMeshAlembicLoader(api.Loader): - """Load Unreal StaticMesh from Alembic""" - - families = ["model", "unrealStaticMesh"] - label = "Import Alembic Static Mesh" - representations = ["abc"] - icon = "cube" - color = "orange" - - def load(self, context, name, namespace, data): - """ - Load and containerise representation into Content Browser. - - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - - Args: - context (dict): application context - name (str): subset name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - - # Create directory for asset and avalon container - root = "/Game/Avalon/Assets" - asset = context.get('asset').get('name') - suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") - - container_name += suffix - - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = unreal.AssetImportTask() - - task.set_editor_property('filename', self.fname) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', False) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - # set import options here - # Unreal 4.24 ignores the settings. It works with Unreal 4.26 - options = unreal.AbcImportSettings() - options.set_editor_property( - 'import_type', unreal.AlembicImportType.STATIC_MESH) - - task.options = options - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - - # Create Asset Container - lib.create_avalon_container( - container=container_name, path=asset_dir) - - data = { - "schema": "openpype:container-2.0", - "id": pipeline.AVALON_CONTAINER_ID, - "asset": asset, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] - } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, representation): - name = container["asset_name"] - source_path = api.get_representation_path(representation) - destination_path = container["namespace"] - - task = unreal.AssetImportTask() - - task.set_editor_property('filename', source_path) - task.set_editor_property('destination_path', destination_path) - # strip suffix - task.set_editor_property('destination_name', name) - task.set_editor_property('replace_existing', True) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - # set import options here - # Unreal 4.24 ignores the settings. It works with Unreal 4.26 - options = unreal.AbcImportSettings() - options.set_editor_property( - 'import_type', unreal.AlembicImportType.STATIC_MESH) - - task.options = options - # do import fbx and replace existing data - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) - - asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) From 448259c2d83e3b22d1042ccad2583e8cc2172f7b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 22 Apr 2021 16:30:13 +0200 Subject: [PATCH 09/18] added letterbox to extract review settings schema --- .../defaults/project_settings/global.json | 9 +++- .../schemas/schema_global_publish.json | 48 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index ca1b258e72..9623ba2c5b 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -45,7 +45,14 @@ ] }, "width": 0, - "height": 0 + "height": 0, + "letter_box": { + "enabled": false, + "ratio": 0.0, + "state": "letterbox", + "thickness": "fill", + "color": "black" + } } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 3c079a130d..68a88b34af 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -203,6 +203,54 @@ "default": 0, "minimum": 0, "maximum": 100000 + }, + { + "key": "letter_box", + "label": "Letter box", + "type": "dict", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled", + "default": false + }, + { + "key": "ratio", + "label": "Letter box ratio", + "type": "number", + "decimal": 4, + "default": 0, + "minimum": 0, + "maximum": 10000 + }, + { + "key": "state", + "label": "Type", + "type": "enum", + "enum_items": [ + { + "letterbox": "Letterbox" + }, + { + "pillar": "Pillar" + } + ] + }, + { + "key": "thickness", + "label": "Thickness", + "type": "text", + "default": "fill" + }, + { + "key": "color", + "label": "Color", + "type": "text", + "default": "#000000" + } + ] } ] } From c1de9281449d87c15847f98d22213a630abe2f8f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 22 Apr 2021 16:51:27 +0200 Subject: [PATCH 10/18] converted logic from PR https://github.com/pypeclub/pype/pull/1371/files --- openpype/plugins/publish/extract_review.py | 96 ++++++++++++++++------ 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index a71b1db66b..f6042a5de9 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -704,6 +704,59 @@ class ExtractReview(pyblish.api.InstancePlugin): return audio_in_args, audio_filters, audio_out_args + def get_letterbox_filters( + self, + letter_box_def, + input_res_ratio, + output_res_ratio, + pixel_aspect, + scale_factor_by_width, + scale_factor_by_height + ): + output = [] + + ratio = letter_box_def["ratio"] + state = letter_box_def["state"] + thickness = letter_box_def["thickness"] + color = letter_box_def["color"] + + if input_res_ratio == output_res_ratio: + ratio /= pixel_aspect + elif input_res_ratio < output_res_ratio: + ratio /= scale_factor_by_width + else: + ratio /= scale_factor_by_height + + if state == "letterbox": + top_box = ( + "drawbox=0:0:iw:round((ih-(iw*(1/{})))/2):t={}:c={}" + ).format(ratio, thickness, color) + + bottom_box = ( + "drawbox=0:ih-round((ih-(iw*(1/{0})))/2)" + ":iw:round((ih-(iw*(1/{0})))/2):t={1}:c={2}" + ).format(ratio, thickness, color) + + output.extend([top_box, bottom_box]) + + elif state == "pillar": + right_box = ( + "drawbox=0:0:round((iw-(ih*{}))/2):ih:t={}:c={}" + ).format(ratio, thickness, color) + + left_box = ( + "drawbox=(round(ih*{0})+round((iw-(ih*{0}))/2))" + ":0:round((iw-(ih*{0}))/2):ih:t={1}:c={2}" + ).format(ratio, thickness, color) + + output.extend([right_box, left_box]) + else: + raise ValueError( + "Letterbox state \"{}\" is not recognized".format(state) + ) + + return output + def rescaling_filters(self, temp_data, output_def, new_repre): """Prepare vieo filters based on tags in new representation. @@ -715,7 +768,8 @@ class ExtractReview(pyblish.api.InstancePlugin): """ filters = [] - letter_box = output_def.get("letter_box") + letter_box_def = output_def["letter_box"] + letter_box_enabled = letter_box_def["enabled"] # Get instance data pixel_aspect = temp_data["pixel_aspect"] @@ -795,7 +849,7 @@ class ExtractReview(pyblish.api.InstancePlugin): if ( output_width == input_width and output_height == input_height - and not letter_box + and not letter_box_enabled and pixel_aspect == 1 ): self.log.debug( @@ -834,30 +888,24 @@ class ExtractReview(pyblish.api.InstancePlugin): ) # letter_box - if letter_box: - if input_res_ratio == output_res_ratio: - letter_box /= pixel_aspect - elif input_res_ratio < output_res_ratio: - letter_box /= scale_factor_by_width - else: - letter_box /= scale_factor_by_height - - scale_filter = "scale={}x{}:flags=lanczos".format( - output_width, output_height + if letter_box_enabled: + filters.extend([ + "scale={}x{}:flags=lanczos".format( + output_width, output_height + ), + "setsar=1" + ]) + filters.extend( + self.get_letterbox_filters( + letter_box_def, + input_res_ratio, + output_res_ratio, + pixel_aspect, + scale_factor_by_width, + scale_factor_by_height + ) ) - top_box = ( - "drawbox=0:0:iw:round((ih-(iw*(1/{})))/2):t=fill:c=black" - ).format(letter_box) - - bottom_box = ( - "drawbox=0:ih-round((ih-(iw*(1/{0})))/2)" - ":iw:round((ih-(iw*(1/{0})))/2):t=fill:c=black" - ).format(letter_box) - - # Add letter box filters - filters.extend([scale_filter, "setsar=1", top_box, bottom_box]) - # scaling none square pixels and 1920 width if ( input_height != output_height From 4036eefe7b9b5a3e8dc472c536d8073367840097 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 22 Apr 2021 19:26:10 +0200 Subject: [PATCH 11/18] SyncServer GUI - multiselect for Summary --- openpype/modules/sync_server/tray/lib.py | 1 + openpype/modules/sync_server/tray/models.py | 19 +- openpype/modules/sync_server/tray/widgets.py | 321 +++++++++++-------- 3 files changed, 201 insertions(+), 140 deletions(-) diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 3597213b31..5e8a3fdd31 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -24,6 +24,7 @@ ProgressRole = QtCore.Qt.UserRole + 4 DateRole = QtCore.Qt.UserRole + 6 FailedRole = QtCore.Qt.UserRole + 8 HeaderNameRole = QtCore.Qt.UserRole + 10 +FullItemRole = QtCore.Qt.UserRole + 12 @six.add_metaclass(abc.ABCMeta) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 981299c6cf..80c6345263 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -120,7 +120,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): self.query = self.get_query(load_records) representations = self.dbcon.aggregate(self.query) - self.add_page_records(self.local_site, self.remote_site, + self.add_page_records(self.active_site, self.remote_site, representations) self.endResetModel() self.refresh_finished.emit() @@ -158,7 +158,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): self._rec_loaded, self._rec_loaded + items_to_fetch - 1) - self.add_page_records(self.local_site, self.remote_site, + self.add_page_records(self.active_site, self.remote_site, representations) self.endInsertRows() @@ -283,7 +283,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): """ self._project = project self.sync_server.set_sync_project_settings() - self.local_site = self.sync_server.get_active_site(self.project) + self.active_site = self.sync_server.get_active_site(self.project) self.remote_site = self.sync_server.get_remote_site(self.project) self.refresh() @@ -410,7 +410,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self.sync_server = sync_server # TODO think about admin mode # this is for regular user, always only single local and single remote - self.local_site = self.sync_server.get_active_site(self.project) + self.active_site = self.sync_server.get_active_site(self.project) self.remote_site = self.sync_server.get_remote_site(self.project) self.sort = self.DEFAULT_SORT @@ -428,6 +428,9 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): def data(self, index, role): item = self._data[index.row()] + if role == lib.FullItemRole: + return item + header_value = self._header[index.column()] if role == lib.ProviderRole: if header_value == 'local_site': @@ -585,7 +588,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): }}, 'order_local': { '$filter': {'input': '$files.sites', 'as': 'p', - 'cond': {'$eq': ['$$p.name', self.local_site]} + 'cond': {'$eq': ['$$p.name', self.active_site]} }} }}, {'$addFields': { @@ -714,7 +717,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): """ base_match = { "type": "representation", - 'files.sites.name': {'$all': [self.local_site, + 'files.sites.name': {'$all': [self.active_site, self.remote_site]} } if not self._word_filter: @@ -889,7 +892,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): self.sync_server = sync_server # TODO think about admin mode # this is for regular user, always only single local and single remote - self.local_site = self.sync_server.get_active_site(self.project) + self.active_site = self.sync_server.get_active_site(self.project) self.remote_site = self.sync_server.get_remote_site(self.project) self.sort = self.DEFAULT_SORT @@ -1042,7 +1045,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): }}, 'order_local': { '$filter': {'input': '$files.sites', 'as': 'p', - 'cond': {'$eq': ['$$p.name', self.local_site]} + 'cond': {'$eq': ['$$p.name', self.active_site]} }} }}, {'$addFields': { diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 6d8348becb..a0c054a67e 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -161,9 +161,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.sync_server = sync_server - self._selected_id = None # keep last selected _id - self.representation_id = None - self.site_name = None # to pause/unpause representation + self._selected_ids = [] # keep last selected _id self.txt_filter = QtWidgets.QLineEdit() self.txt_filter.setPlaceholderText("Quick filter representations..") @@ -183,7 +181,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.table_view.setModel(model) self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.table_view.setSelectionMode( - QtWidgets.QAbstractItemView.SingleSelection) + QtWidgets.QAbstractItemView.ExtendedSelection) self.table_view.setSelectionBehavior( QtWidgets.QAbstractItemView.SelectRows) self.table_view.horizontalHeader().setSortIndicator( @@ -228,10 +226,12 @@ class SyncRepresentationWidget(QtWidgets.QWidget): 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.model.data(index, Qt.UserRole) + def _selection_changed(self, new_selected, all_selected): + idxs = self.selection_model.selectedRows() + self._selected_ids = [] + + for index in idxs: + self._selected_ids.append(self.model.data(index, Qt.UserRole)) def _set_selection(self): """ @@ -239,14 +239,16 @@ class SyncRepresentationWidget(QtWidgets.QWidget): Keep selection during model refresh. """ - if self._selected_id: - index = self.model.get_index(self._selected_id) + existing_ids = [] + for selected_id in self._selected_ids: + index = self.model.get_index(selected_id) if index and index.isValid(): mode = QtCore.QItemSelectionModel.Select | \ QtCore.QItemSelectionModel.Rows - self.selection_model.setCurrentIndex(index, mode) - else: - self._selected_id = None + self.selection_model.select(index, mode) + existing_ids.append(selected_id) + + self._selected_ids = existing_ids def _double_clicked(self, index): """ @@ -256,59 +258,62 @@ class SyncRepresentationWidget(QtWidgets.QWidget): detail_window = SyncServerDetailWindow( self.sync_server, _id, self.model.project) detail_window.exec() - + def _on_context_menu(self, point): """ Shows menu with loader actions on Right-click. + + Supports multiple selects - adds all available actions, each + action handles if it appropriate for item itself, if not it skips. """ + is_multi = len(self._selected_ids) > 1 point_index = self.table_view.indexAt(point) - if not point_index.isValid(): + if not point_index.isValid() and not is_multi: return - self.item = self.model._data[point_index.row()] - self.representation_id = self.item._id - log.debug("menu representation _id:: {}". - format(self.representation_id)) + if is_multi: + index = self.model.get_index(self._selected_ids[0]) + self.item = self.model.data(index, lib.FullItemRole) + else: + self.item = self.model.data(point_index, lib.FullItemRole) menu = QtWidgets.QMenu() actions_mapping = {} - actions_kwargs_mapping = {} + action_kwarg_map = {} + + active_site = self.model.active_site + remote_site = self.model.remote_site - local_site = self.item.local_site local_progress = self.item.local_progress - remote_site = self.item.remote_site remote_progress = self.item.remote_progress - for site, progress in {local_site: local_progress, + project = self.model.project + for site, progress in {active_site: local_progress, remote_site: remote_progress}.items(): - project = self.model.project - provider = self.sync_server.get_provider_for_site(project, - site) + provider = self.sync_server.get_provider_for_site(project, site) if provider == 'local_drive': if 'studio' in site: txt = " studio version" else: txt = " local version" action = QtWidgets.QAction("Open in explorer" + txt) - if progress == 1.0: + if progress == 1.0 or is_multi: actions_mapping[action] = self._open_in_explorer - actions_kwargs_mapping[action] = {'site': site} + action_kwarg_map[action] = \ + self._get_action_kwargs(site) menu.addAction(action) - # progress smaller then 1.0 --> in progress or queued - if local_progress < 1.0: - self.site_name = local_site - else: - self.site_name = remote_site - - if self.item.status in [lib.STATUS[0], lib.STATUS[1]]: - action = QtWidgets.QAction("Pause") + if self.item.status in [lib.STATUS[0], lib.STATUS[1]] or is_multi: + action = QtWidgets.QAction("Pause in queue") actions_mapping[action] = self._pause + # pause handles which site_name it will pause itself + action_kwarg_map[action] = {"repre_ids": self._selected_ids} menu.addAction(action) - if self.item.status == lib.STATUS[3]: - action = QtWidgets.QAction("Unpause") + if self.item.status == lib.STATUS[3] or is_multi: + action = QtWidgets.QAction("Unpause in queue") actions_mapping[action] = self._unpause + action_kwarg_map[action] = {"repre_ids": self._selected_ids} menu.addAction(action) # if self.item.status == lib.STATUS[1]: @@ -316,24 +321,29 @@ class SyncRepresentationWidget(QtWidgets.QWidget): # actions_mapping[action] = self._show_detail # menu.addAction(action) - if remote_progress == 1.0: + if remote_progress == 1.0 or is_multi: action = QtWidgets.QAction("Re-sync Active site") - actions_mapping[action] = self._reset_local_site + action_kwarg_map[action] = self._get_action_kwargs(active_site) + actions_mapping[action] = self._reset_site menu.addAction(action) - if local_progress == 1.0: + if local_progress == 1.0 or is_multi: action = QtWidgets.QAction("Re-sync Remote site") - actions_mapping[action] = self._reset_remote_site + action_kwarg_map[action] = self._get_action_kwargs(remote_site) + actions_mapping[action] = self._reset_site menu.addAction(action) - if local_site != self.sync_server.DEFAULT_SITE: + if active_site == get_local_site_id(): action = QtWidgets.QAction("Completely remove from local") + action_kwarg_map[action] = self._get_action_kwargs(active_site) actions_mapping[action] = self._remove_site menu.addAction(action) - else: - action = QtWidgets.QAction("Mark for sync to local") - actions_mapping[action] = self._add_site - menu.addAction(action) + + # # temp for testing only !!! + # action = QtWidgets.QAction("Download") + # action_kwarg_map[action] = self._get_action_kwargs(active_site) + # actions_mapping[action] = self._add_site + # menu.addAction(action) if not actions_mapping: action = QtWidgets.QAction("< No action >") @@ -343,46 +353,65 @@ class SyncRepresentationWidget(QtWidgets.QWidget): result = menu.exec_(QtGui.QCursor.pos()) if result: to_run = actions_mapping[result] - to_run_kwargs = actions_kwargs_mapping.get(result, {}) + to_run_kwargs = action_kwarg_map.get(result, {}) if to_run: to_run(**to_run_kwargs) self.model.refresh() - def _pause(self): - self.sync_server.pause_representation(self.model.project, - self.representation_id, - self.site_name) - self.site_name = None - self.message_generated.emit("Paused {}".format(self.representation_id)) + def _pause(self, repre_ids=None): + log.debug("Pause {}".format(repre_ids)) + for representation_id in repre_ids: + item = self._get_item_by_repre_id(representation_id) + if item.status not in [lib.STATUS[0], lib.STATUS[1]]: + continue + for site_name in [self.model.active_site, self.model.remote_site]: + check_progress = self._get_progress(item, site_name) + if check_progress < 1: + self.sync_server.pause_representation(self.model.project, + representation_id, + site_name) - def _unpause(self): - self.sync_server.unpause_representation( - self.model.project, - self.representation_id, - self.site_name) - self.site_name = None - self.message_generated.emit("Unpaused {}".format( - self.representation_id)) + self.message_generated.emit("Paused {}".format(representation_id)) + + def _unpause(self, repre_ids=None): + log.debug("UnPause {}".format(repre_ids)) + for representation_id in repre_ids: + item = self._get_item_by_repre_id(representation_id) + if item.status not in lib.STATUS[3]: + continue + for site_name in [self.model.active_site, self.model.remote_site]: + check_progress = self._get_progress(item, site_name) + if check_progress < 1: + self.sync_server.unpause_representation( + self.model.project, + representation_id, + site_name) + + self.message_generated.emit("Unpause {}".format(representation_id)) # temporary here for testing, will be removed TODO - def _add_site(self): - log.info(self.representation_id) - project_name = self.model.project - local_site_name = get_local_site_id() - try: - self.sync_server.add_site( - project_name, - self.representation_id, - local_site_name - ) - self.message_generated.emit( - "Site {} added for {}".format(local_site_name, - self.representation_id)) - except ValueError as exp: - self.message_generated.emit("Error {}".format(str(exp))) + def _add_site(self, repre_ids=None, site_name=None): + log.debug("Add site {}:{}".format(repre_ids, site_name)) + for representation_id in repre_ids: + item = self._get_item_by_repre_id(representation_id) + if item.local_site == site_name or item.remote_site == site_name: + # site already exists skip + continue - def _remove_site(self): + try: + self.sync_server.add_site( + self.model.project, + representation_id, + site_name + ) + self.message_generated.emit( + "Site {} added for {}".format(site_name, + representation_id)) + except ValueError as exp: + self.message_generated.emit("Error {}".format(str(exp))) + + def _remove_site(self, repre_ids=None, site_name=None): """ Removes site record AND files. @@ -392,65 +421,93 @@ class SyncRepresentationWidget(QtWidgets.QWidget): This could only happen when artist work on local machine, not connected to studio mounted drives. """ - log.info("Removing {}".format(self.representation_id)) - try: - local_site = get_local_site_id() - self.sync_server.remove_site( + log.debug("Remove site {}:{}".format(repre_ids, site_name)) + for representation_id in repre_ids: + log.info("Removing {}".format(representation_id)) + try: + self.sync_server.remove_site( + self.model.project, + representation_id, + site_name, + True) + self.message_generated.emit( + "Site {} removed".format(site_name)) + except ValueError as exp: + self.message_generated.emit("Error {}".format(str(exp))) + + self.model.refresh( + load_records=self.model._rec_loaded) + + def _reset_site(self, repre_ids=None, site_name=None): + """ + Removes errors or success metadata for particular file >> forces + redo of upload/download + """ + log.debug("Reset site {}:{}".format(repre_ids, site_name)) + for representation_id in repre_ids: + item = self._get_item_by_repre_id(representation_id) + check_progress = self._get_progress(item, site_name, True) + + # do not reset if opposite side is not fully there + if check_progress != 1: + log.debug("Not fully available {} on other side, skipping". + format(check_progress)) + continue + + self.sync_server.reset_provider_for_file( 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))) + representation_id, + site_name=site_name, + force=True) + self.model.refresh( load_records=self.model._rec_loaded) - def _reset_local_site(self): - """ - Removes errors or success metadata for particular file >> forces - redo of upload/download - """ - self.sync_server.reset_provider_for_file( - self.model.project, - self.representation_id, - 'local') - self.model.refresh( - load_records=self.model._rec_loaded) + def _open_in_explorer(self, repre_ids=None, site_name=None): + log.debug("Open in Explorer {}:{}".format(repre_ids, site_name)) + for representation_id in repre_ids: + item = self._get_item_by_repre_id(representation_id) + if not item: + return - def _reset_remote_site(self): - """ - Removes errors or success metadata for particular file >> forces - redo of upload/download - """ - self.sync_server.reset_provider_for_file( - self.model.project, - self.representation_id, - 'remote') - self.model.refresh( - load_records=self.model._rec_loaded) + fpath = item.path + project = self.model.project + fpath = self.sync_server.get_local_file_path(project, + site_name, + fpath) - def _open_in_explorer(self, site): - if not self.item: - return + fpath = os.path.normpath(os.path.dirname(fpath)) + if os.path.isdir(fpath): + if 'win' in sys.platform: # windows + subprocess.Popen('explorer "%s"' % fpath) + elif sys.platform == 'darwin': # macOS + subprocess.Popen(['open', fpath]) + else: # linux + try: + subprocess.Popen(['xdg-open', fpath]) + except OSError: + raise OSError('unsupported xdg-open call??') - fpath = self.item.path - project = self.model.project - fpath = self.sync_server.get_local_file_path(project, - site, - fpath) + def _get_progress(self, item, site_name, opposite=False): + """Returns progress value according to site (side)""" + progress = {'local': item.local_progress, + 'remote': item.remote_progress} + side = 'remote' + if site_name == self.model.active_site: + side = 'local' + if opposite: + side = 'remote' if side == 'local' else 'local' - fpath = os.path.normpath(os.path.dirname(fpath)) - if os.path.isdir(fpath): - if 'win' in sys.platform: # windows - subprocess.Popen('explorer "%s"' % fpath) - elif sys.platform == 'darwin': # macOS - subprocess.Popen(['open', fpath]) - else: # linux - try: - subprocess.Popen(['xdg-open', fpath]) - except OSError: - raise OSError('unsupported xdg-open call??') + return progress[side] + + def _get_item_by_repre_id(self, representation_id): + index = self.model.get_index(representation_id) + item = self.model.data(index, lib.FullItemRole) + return item + + def _get_action_kwargs(self, site_name): + """Default format of kwargs for action""" + return {"repre_ids": self._selected_ids, "site_name": site_name} def _save_scrollbar(self): self._scrollbar_pos = self.table_view.verticalScrollBar().value() @@ -599,7 +656,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): menu = QtWidgets.QMenu() actions_mapping = {} - actions_kwargs_mapping = {} + action_kwarg_map = {} local_site = self.item.local_site local_progress = self.item.local_progress @@ -619,7 +676,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): action = QtWidgets.QAction("Open in explorer" + txt) if progress == 1: actions_mapping[action] = self._open_in_explorer - actions_kwargs_mapping[action] = {'site': site} + action_kwarg_map[action] = {'site': site} menu.addAction(action) if self.item.status == lib.STATUS[2]: @@ -645,7 +702,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): result = menu.exec_(QtGui.QCursor.pos()) if result: to_run = actions_mapping[result] - to_run_kwargs = actions_kwargs_mapping.get(result, {}) + to_run_kwargs = action_kwarg_map.get(result, {}) if to_run: to_run(**to_run_kwargs) From 0759b667f6095c859e7c6a043f7a416ab9f5990e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 22 Apr 2021 19:53:53 +0200 Subject: [PATCH 12/18] it is possible to define box and line with different color and transparency --- openpype/plugins/publish/extract_review.py | 82 +++++++++++++++---- .../defaults/project_settings/global.json | 15 +++- .../schemas/schema_global_publish.json | 31 +++++-- 3 files changed, 100 insertions(+), 28 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index f6042a5de9..00b671199d 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -717,8 +717,20 @@ class ExtractReview(pyblish.api.InstancePlugin): ratio = letter_box_def["ratio"] state = letter_box_def["state"] - thickness = letter_box_def["thickness"] - color = letter_box_def["color"] + fill_color = letter_box_def["fill_color"] + f_red, f_green, f_blue, f_alpha = fill_color + fill_color_hex = "{0:0>2X}{1:0>2X}{2:0>2X}".format( + f_red, f_green, f_blue + ) + fill_color_alpha = float(f_alpha) / 255 + + line_thickness = letter_box_def["line_thickness"] + line_color = letter_box_def["line_color"] + l_red, l_green, l_blue, l_alpha = line_color + line_color_hex = "{0:0>2X}{1:0>2X}{2:0>2X}".format( + l_red, l_green, l_blue + ) + line_color_alpha = float(l_alpha) / 255 if input_res_ratio == output_res_ratio: ratio /= pixel_aspect @@ -728,28 +740,62 @@ class ExtractReview(pyblish.api.InstancePlugin): ratio /= scale_factor_by_height if state == "letterbox": - top_box = ( - "drawbox=0:0:iw:round((ih-(iw*(1/{})))/2):t={}:c={}" - ).format(ratio, thickness, color) + if fill_color_alpha > 0: + top_box = ( + "drawbox=0:0:iw:round((ih-(iw*(1/{})))/2):t=fill:c={}@{}" + ).format(ratio, fill_color_hex, fill_color_alpha) - bottom_box = ( - "drawbox=0:ih-round((ih-(iw*(1/{0})))/2)" - ":iw:round((ih-(iw*(1/{0})))/2):t={1}:c={2}" - ).format(ratio, thickness, color) + bottom_box = ( + "drawbox=0:ih-round((ih-(iw*(1/{0})))/2)" + ":iw:round((ih-(iw*(1/{0})))/2):t=fill:c={1}@{2}" + ).format(ratio, fill_color_hex, fill_color_alpha) - output.extend([top_box, bottom_box]) + output.extend([top_box, bottom_box]) + + if line_color_alpha > 0 and line_thickness > 0: + top_line = ( + "drawbox=0:round((ih-(iw*(1/{0})))/2)-{1}:iw:{1}:" + "t=fill:c={2}@{3}" + ).format( + ratio, line_thickness, line_color_hex, line_color_alpha + ) + bottom_line = ( + "drawbox=0:ih-round((ih-(iw*(1/{})))/2)" + ":iw:{}:t=fill:c={}@{}" + ).format( + ratio, line_thickness, line_color_hex, line_color_alpha + ) + output.extend([top_line, bottom_line]) elif state == "pillar": - right_box = ( - "drawbox=0:0:round((iw-(ih*{}))/2):ih:t={}:c={}" - ).format(ratio, thickness, color) + if fill_color_alpha > 0: + left_box = ( + "drawbox=0:0:round((iw-(ih*{}))/2):ih:t=fill:c={}@{}" + ).format(ratio, fill_color_hex, fill_color_alpha) - left_box = ( - "drawbox=(round(ih*{0})+round((iw-(ih*{0}))/2))" - ":0:round((iw-(ih*{0}))/2):ih:t={1}:c={2}" - ).format(ratio, thickness, color) + right_box = ( + "drawbox=iw-round((iw-(ih*{0}))/2))" + ":0:round((iw-(ih*{0}))/2):ih:t=fill:c={1}@{2}" + ).format(ratio, fill_color_hex, fill_color_alpha) + + output.extend([left_box, right_box]) + + if line_color_alpha > 0 and line_thickness > 0: + left_line = ( + "drawbox=round((iw-(ih*{}))/2):0:{}:ih:t=fill:c={}@{}" + ).format( + ratio, line_thickness, line_color_hex, line_color_alpha + ) + + right_line = ( + "drawbox=iw-round((iw-(ih*{}))/2))" + ":0:{}:ih:t=fill:c={}@{}" + ).format( + ratio, line_thickness, line_color_hex, line_color_alpha + ) + + output.extend([left_line, right_line]) - output.extend([right_box, left_box]) else: raise ValueError( "Letterbox state \"{}\" is not recognized".format(state) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 9623ba2c5b..4fb42c6ff3 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -50,8 +50,19 @@ "enabled": false, "ratio": 0.0, "state": "letterbox", - "thickness": "fill", - "color": "black" + "fill_color": [ + 0, + 0, + 0, + 255 + ], + "line_thickness": 0, + "line_color": [ + 255, + 0, + 0, + 255 + ] } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 68a88b34af..e6be868068 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -239,16 +239,31 @@ ] }, { - "key": "thickness", - "label": "Thickness", - "type": "text", - "default": "fill" + "type": "schema_template", + "name": "template_rgba_color", + "template_data": [ + { + "label": "Fill Color", + "name": "fill_color" + } + ] }, { - "key": "color", - "label": "Color", - "type": "text", - "default": "#000000" + "key": "line_thickness", + "label": "Line Thickness", + "type": "number", + "minimum": 0, + "maximum": 1000 + }, + { + "type": "schema_template", + "name": "template_rgba_color", + "template_data": [ + { + "label": "Line Color", + "name": "line_color" + } + ] } ] } From d2a27fd8580cfabbb39f2214bedf7e7b2f0cd9b9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 23 Apr 2021 10:35:41 +0200 Subject: [PATCH 13/18] SyncServer GUI - multiselect for Detail Refactor --- openpype/modules/sync_server/tray/app.py | 4 +- openpype/modules/sync_server/tray/lib.py | 7 + openpype/modules/sync_server/tray/models.py | 3 + openpype/modules/sync_server/tray/widgets.py | 633 +++++++++---------- 4 files changed, 296 insertions(+), 351 deletions(-) diff --git a/openpype/modules/sync_server/tray/app.py b/openpype/modules/sync_server/tray/app.py index 25fbf0e49a..d91ba76335 100644 --- a/openpype/modules/sync_server/tray/app.py +++ b/openpype/modules/sync_server/tray/app.py @@ -7,7 +7,7 @@ from openpype import resources from openpype.modules.sync_server.tray.widgets import ( SyncProjectListWidget, - SyncRepresentationWidget + SyncRepresentationSummaryWidget ) log = PypeLogger().get_logger("SyncServer") @@ -47,7 +47,7 @@ class SyncServerWindow(QtWidgets.QDialog): left_column_layout.addWidget(self.pause_btn) left_column.setLayout(left_column_layout) - repres = SyncRepresentationWidget( + repres = SyncRepresentationSummaryWidget( sync_server, project=self.projects.current_project, parent=self) diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 5e8a3fdd31..04bd1f568e 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -129,6 +129,7 @@ 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: @@ -157,3 +158,9 @@ def translate_provider_for_icon(sync_server, project, site): if site == sync_server.DEFAULT_SITE: return sync_server.DEFAULT_SITE return sync_server.get_provider_for_site(project, site) + + +def get_item_by_id(model, object_id): + index = model.get_index(object_id) + item = model.data(index, FullItemRole) + return item diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 80c6345263..ffd81a1588 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -908,6 +908,9 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): def data(self, index, role): item = self._data[index.row()] + if role == lib.FullItemRole: + return item + header_value = self._header[index.column()] if role == lib.ProviderRole: if header_value == 'local_site': diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index a0c054a67e..c552904244 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -135,7 +135,7 @@ class SyncProjectListWidget(ProjectListWidget): self.refresh() -class SyncRepresentationWidget(QtWidgets.QWidget): +class _SyncRepresentationWidget(QtWidgets.QWidget): """ Summary dialog with list of representations that matches current settings 'local_site' and 'remote_site'. @@ -143,90 +143,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): active_changed = QtCore.Signal() # active index changed message_generated = QtCore.Signal(str) - default_widths = ( - ("asset", 190), - ("subset", 170), - ("version", 60), - ("representation", 145), - ("local_site", 160), - ("remote_site", 160), - ("files_count", 50), - ("files_size", 60), - ("priority", 70), - ("status", 110) - ) - - def __init__(self, sync_server, project=None, parent=None): - super(SyncRepresentationWidget, self).__init__(parent) - - self.sync_server = sync_server - - self._selected_ids = [] # keep last selected _id - - 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.txt_filter) - - self.table_view = QtWidgets.QTableView() - headers = [item[0] for item in self.default_widths] - - model = SyncRepresentationSummaryModel(sync_server, headers, project) - self.table_view.setModel(model) - self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.table_view.setSelectionMode( - QtWidgets.QAbstractItemView.ExtendedSelection) - self.table_view.setSelectionBehavior( - QtWidgets.QAbstractItemView.SelectRows) - self.table_view.horizontalHeader().setSortIndicator( - -1, Qt.AscendingOrder) - self.table_view.setAlternatingRowColors(True) - self.table_view.verticalHeader().hide() - - column = self.table_view.model().get_header_index("local_site") - delegate = ImageDelegate(self) - self.table_view.setItemDelegateForColumn(column, delegate) - - column = self.table_view.model().get_header_index("remote_site") - delegate = ImageDelegate(self) - self.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) - - self.table_view.doubleClicked.connect(self._double_clicked) - 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) - 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_selected, all_selected): + def _selection_changed(self, _new_selected, _all_selected): idxs = self.selection_model.selectedRows() self._selected_ids = [] @@ -273,21 +190,36 @@ class SyncRepresentationWidget(QtWidgets.QWidget): if is_multi: index = self.model.get_index(self._selected_ids[0]) - self.item = self.model.data(index, lib.FullItemRole) + item = self.model.data(index, lib.FullItemRole) else: - self.item = self.model.data(point_index, lib.FullItemRole) + item = self.model.data(point_index, lib.FullItemRole) + action_kwarg_map, actions_mapping, menu = self._prepare_menu(item, + is_multi) + + result = menu.exec_(QtGui.QCursor.pos()) + if result: + to_run = actions_mapping[result] + to_run_kwargs = action_kwarg_map.get(result, {}) + if to_run: + to_run(**to_run_kwargs) + + self.model.refresh() + + def _prepare_menu(self, item, is_multi): menu = QtWidgets.QMenu() + actions_mapping = {} action_kwarg_map = {} active_site = self.model.active_site remote_site = self.model.remote_site - local_progress = self.item.local_progress - remote_progress = self.item.remote_progress + local_progress = item.local_progress + remote_progress = item.remote_progress project = self.model.project + for site, progress in {active_site: local_progress, remote_site: remote_progress}.items(): provider = self.sync_server.get_provider_for_site(project, site) @@ -303,24 +235,6 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self._get_action_kwargs(site) menu.addAction(action) - if self.item.status in [lib.STATUS[0], lib.STATUS[1]] or is_multi: - action = QtWidgets.QAction("Pause in queue") - actions_mapping[action] = self._pause - # pause handles which site_name it will pause itself - action_kwarg_map[action] = {"repre_ids": self._selected_ids} - menu.addAction(action) - - if self.item.status == lib.STATUS[3] or is_multi: - action = QtWidgets.QAction("Unpause in queue") - actions_mapping[action] = self._unpause - action_kwarg_map[action] = {"repre_ids": self._selected_ids} - menu.addAction(action) - - # if self.item.status == lib.STATUS[1]: - # action = QtWidgets.QAction("Open error detail") - # actions_mapping[action] = self._show_detail - # menu.addAction(action) - if remote_progress == 1.0 or is_multi: action = QtWidgets.QAction("Re-sync Active site") action_kwarg_map[action] = self._get_action_kwargs(active_site) @@ -350,19 +264,12 @@ class SyncRepresentationWidget(QtWidgets.QWidget): actions_mapping[action] = None menu.addAction(action) - result = menu.exec_(QtGui.QCursor.pos()) - if result: - to_run = actions_mapping[result] - to_run_kwargs = action_kwarg_map.get(result, {}) - if to_run: - to_run(**to_run_kwargs) + return action_kwarg_map, actions_mapping, menu - self.model.refresh() - - def _pause(self, repre_ids=None): - log.debug("Pause {}".format(repre_ids)) - for representation_id in repre_ids: - item = self._get_item_by_repre_id(representation_id) + def _pause(self, selected_ids=None): + log.debug("Pause {}".format(selected_ids)) + for representation_id in selected_ids: + item = lib.get_item_by_id(self.model, representation_id) if item.status not in [lib.STATUS[0], lib.STATUS[1]]: continue for site_name in [self.model.active_site, self.model.remote_site]: @@ -374,10 +281,10 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.message_generated.emit("Paused {}".format(representation_id)) - def _unpause(self, repre_ids=None): - log.debug("UnPause {}".format(repre_ids)) - for representation_id in repre_ids: - item = self._get_item_by_repre_id(representation_id) + def _unpause(self, selected_ids=None): + log.debug("UnPause {}".format(selected_ids)) + for representation_id in selected_ids: + item = lib.get_item_by_id(self.model, representation_id) if item.status not in lib.STATUS[3]: continue for site_name in [self.model.active_site, self.model.remote_site]: @@ -391,10 +298,10 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.message_generated.emit("Unpause {}".format(representation_id)) # temporary here for testing, will be removed TODO - def _add_site(self, repre_ids=None, site_name=None): - log.debug("Add site {}:{}".format(repre_ids, site_name)) - for representation_id in repre_ids: - item = self._get_item_by_repre_id(representation_id) + def _add_site(self, selected_ids=None, site_name=None): + log.debug("Add site {}:{}".format(selected_ids, site_name)) + for representation_id in selected_ids: + item = lib.get_item_by_id(self.model, representation_id) if item.local_site == site_name or item.remote_site == site_name: # site already exists skip continue @@ -411,7 +318,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): except ValueError as exp: self.message_generated.emit("Error {}".format(str(exp))) - def _remove_site(self, repre_ids=None, site_name=None): + def _remove_site(self, selected_ids=None, site_name=None): """ Removes site record AND files. @@ -421,8 +328,8 @@ class SyncRepresentationWidget(QtWidgets.QWidget): This could only happen when artist work on local machine, not connected to studio mounted drives. """ - log.debug("Remove site {}:{}".format(repre_ids, site_name)) - for representation_id in repre_ids: + log.debug("Remove site {}:{}".format(selected_ids, site_name)) + for representation_id in selected_ids: log.info("Removing {}".format(representation_id)) try: self.sync_server.remove_site( @@ -438,14 +345,14 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.model.refresh( load_records=self.model._rec_loaded) - def _reset_site(self, repre_ids=None, site_name=None): + def _reset_site(self, selected_ids=None, site_name=None): """ Removes errors or success metadata for particular file >> forces redo of upload/download """ - log.debug("Reset site {}:{}".format(repre_ids, site_name)) - for representation_id in repre_ids: - item = self._get_item_by_repre_id(representation_id) + log.debug("Reset site {}:{}".format(selected_ids, site_name)) + for representation_id in selected_ids: + item = lib.get_item_by_id(self.model, representation_id) check_progress = self._get_progress(item, site_name, True) # do not reset if opposite side is not fully there @@ -463,10 +370,10 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.model.refresh( load_records=self.model._rec_loaded) - def _open_in_explorer(self, repre_ids=None, site_name=None): - log.debug("Open in Explorer {}:{}".format(repre_ids, site_name)) - for representation_id in repre_ids: - item = self._get_item_by_repre_id(representation_id) + def _open_in_explorer(self, selected_ids=None, site_name=None): + log.debug("Open in Explorer {}:{}".format(selected_ids, site_name)) + for selected_id in selected_ids: + item = lib.get_item_by_id(self.model, selected_id) if not item: return @@ -500,14 +407,9 @@ class SyncRepresentationWidget(QtWidgets.QWidget): return progress[side] - def _get_item_by_repre_id(self, representation_id): - index = self.model.get_index(representation_id) - item = self.model.data(index, lib.FullItemRole) - return item - def _get_action_kwargs(self, site_name): """Default format of kwargs for action""" - return {"repre_ids": self._selected_ids, "site_name": site_name} + return {"selected_ids": self._selected_ids, "site_name": site_name} def _save_scrollbar(self): self._scrollbar_pos = self.table_view.verticalScrollBar().value() @@ -517,7 +419,155 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.table_view.verticalScrollBar().setValue(self._scrollbar_pos) -class SyncRepresentationDetailWidget(QtWidgets.QWidget): +class SyncRepresentationSummaryWidget(_SyncRepresentationWidget): + + default_widths = ( + ("asset", 190), + ("subset", 170), + ("version", 60), + ("representation", 145), + ("local_site", 160), + ("remote_site", 160), + ("files_count", 50), + ("files_size", 60), + ("priority", 70), + ("status", 110) + ) + + def __init__(self, sync_server, project=None, parent=None): + super(SyncRepresentationSummaryWidget, self).__init__(parent) + + self.sync_server = sync_server + + self._selected_ids = [] # keep last selected _id + + txt_filter = QtWidgets.QLineEdit() + txt_filter.setPlaceholderText("Quick filter representations..") + txt_filter.setClearButtonEnabled(True) + txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"), + QtWidgets.QLineEdit.LeadingPosition) + self.txt_filter = txt_filter + + self._scrollbar_pos = None + + top_bar_layout = QtWidgets.QHBoxLayout() + top_bar_layout.addWidget(self.txt_filter) + + table_view = QtWidgets.QTableView() + headers = [item[0] for item in self.default_widths] + + model = SyncRepresentationSummaryModel(sync_server, headers, project) + table_view.setModel(model) + table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + table_view.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection) + table_view.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectRows) + table_view.horizontalHeader().setSortIndicator( + -1, Qt.AscendingOrder) + table_view.setAlternatingRowColors(True) + table_view.verticalHeader().hide() + + column = table_view.model().get_header_index("local_site") + delegate = ImageDelegate(self) + table_view.setItemDelegateForColumn(column, delegate) + + column = table_view.model().get_header_index("remote_site") + delegate = ImageDelegate(self) + table_view.setItemDelegateForColumn(column, delegate) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(top_bar_layout) + layout.addWidget(table_view) + + self.table_view = table_view + self.model = model + + 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) + + table_view.doubleClicked.connect(self._double_clicked) + 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) + model.modelReset.connect(self._set_selection) + + self.selection_model = self.table_view.selectionModel() + self.selection_model.selectionChanged.connect(self._selection_changed) + + + def _prepare_menu(self, item, is_multi): + action_kwarg_map, actions_mapping, menu = \ + super()._prepare_menu(item, is_multi) + + if item.status in [lib.STATUS[0], lib.STATUS[1]] or is_multi: + action = QtWidgets.QAction("Pause in queue") + actions_mapping[action] = self._pause + # pause handles which site_name it will pause itself + action_kwarg_map[action] = {"selected_ids": self._selected_ids} + menu.addAction(action) + + if item.status == lib.STATUS[3] or is_multi: + action = QtWidgets.QAction("Unpause in queue") + actions_mapping[action] = self._unpause + action_kwarg_map[action] = {"selected_ids": self._selected_ids} + menu.addAction(action) + + return action_kwarg_map, actions_mapping, menu + + +class SyncServerDetailWindow(QtWidgets.QDialog): + """Wrapper window for SyncRepresentationDetailWidget + + Creates standalone window with list of files for selected repre_id. + """ + def __init__(self, sync_server, _id, project, parent=None): + log.debug( + "!!! SyncServerDetailWindow _id:: {}".format(_id)) + super(SyncServerDetailWindow, self).__init__(parent) + self.setWindowFlags(QtCore.Qt.Window) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + self.setStyleSheet(style.load_stylesheet()) + self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) + self.resize(1000, 400) + + body = QtWidgets.QWidget() + footer = QtWidgets.QWidget() + footer.setFixedHeight(20) + + container = SyncRepresentationDetailWidget(sync_server, _id, project, + parent=self) + body_layout = QtWidgets.QHBoxLayout(body) + body_layout.addWidget(container) + body_layout.setContentsMargins(0, 0, 0, 0) + + self.message = QtWidgets.QLabel() + self.message.hide() + + footer_layout = QtWidgets.QVBoxLayout(footer) + footer_layout.addWidget(self.message) + footer_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(body) + layout.addWidget(footer) + + self.setLayout(body_layout) + self.setWindowTitle("Sync Representation Detail") + + +class SyncRepresentationDetailWidget(_SyncRepresentationWidget): """ Widget to display list of synchronizable files for single repre. @@ -541,13 +591,12 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): super(SyncRepresentationDetailWidget, self).__init__(parent) log.debug("Representation_id:{}".format(_id)) - self.representation_id = _id - self.item = None # set to item that mouse was clicked over self.project = project self.sync_server = sync_server - self._selected_id = None + self.representation_id = _id + self._selected_ids = [] self.txt_filter = QtWidgets.QLineEdit() self.txt_filter.setPlaceholderText("Quick filter representation..") @@ -568,7 +617,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): table_view.setModel(model) table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) table_view.setSelectionMode( - QtWidgets.QAbstractItemView.SingleSelection) + QtWidgets.QAbstractItemView.ExtendedSelection) table_view.setSelectionBehavior( QtWidgets.QTableView.SelectRows) table_view.horizontalHeader().setSortIndicator(-1, Qt.AscendingOrder) @@ -613,171 +662,118 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): model.refresh_finished.connect(self._set_scrollbar) model.modelReset.connect(self._set_selection) - def _selection_changed(self): - index = self.selection_model.currentIndex() - self._selected_id = self.model.data(index, Qt.UserRole) - - def _set_selection(self): - """ - Sets selection to 'self._selected_id' if exists. - - Keep selection during model refresh. - """ - if self._selected_id: - index = self.model.get_index(self._selected_id) - if index and index.isValid(): - mode = QtCore.QItemSelectionModel.Select | \ - QtCore.QItemSelectionModel.Rows - self.selection_model.setCurrentIndex(index, mode) - else: - self._selected_id = None - - def _show_detail(self): + def _show_detail(self, selected_ids=None): """ Shows windows with error message for failed sync of a file. """ - dt = max(self.item.created_dt, self.item.sync_dt) - detail_window = SyncRepresentationErrorWindow(self.item._id, - self.project, - dt, - self.item.tries, - self.item.error) + detail_window = SyncRepresentationErrorWindow(self.model, selected_ids) + detail_window.exec() - def _on_context_menu(self, point): - """ - Shows menu with loader actions on Right-click. - """ - point_index = self.table_view.indexAt(point) - if not point_index.isValid(): - return + def _prepare_menu(self, item, is_multi): + """Adds view (and model) dependent actions to default ones""" + action_kwarg_map, actions_mapping, menu = \ + super()._prepare_menu(item, is_multi) - self.item = self.model._data[point_index.row()] - - menu = QtWidgets.QMenu() - actions_mapping = {} - action_kwarg_map = {} - - local_site = self.item.local_site - local_progress = self.item.local_progress - remote_site = self.item.remote_site - remote_progress = self.item.remote_progress - - for site, progress in {local_site: local_progress, - remote_site: remote_progress}.items(): - project = self.model.project - provider = self.sync_server.get_provider_for_site(project, - site) - if provider == 'local_drive': - if 'studio' in site: - txt = " studio version" - else: - txt = " local version" - action = QtWidgets.QAction("Open in explorer" + txt) - if progress == 1: - actions_mapping[action] = self._open_in_explorer - action_kwarg_map[action] = {'site': site} - menu.addAction(action) - - if self.item.status == lib.STATUS[2]: + if item.status == lib.STATUS[2] or is_multi: action = QtWidgets.QAction("Open error detail") actions_mapping[action] = self._show_detail + action_kwarg_map[action] = {"selected_ids": self._selected_ids} + menu.addAction(action) - if float(remote_progress) == 1.0: - action = QtWidgets.QAction("Re-sync active site") - actions_mapping[action] = self._reset_local_site - menu.addAction(action) - - if float(local_progress) == 1.0: - action = QtWidgets.QAction("Re-sync remote site") - actions_mapping[action] = self._reset_remote_site - menu.addAction(action) - - if not actions_mapping: - action = QtWidgets.QAction("< No action >") - actions_mapping[action] = None - menu.addAction(action) - - result = menu.exec_(QtGui.QCursor.pos()) - if result: - to_run = actions_mapping[result] - to_run_kwargs = action_kwarg_map.get(result, {}) - if to_run: - to_run(**to_run_kwargs) - - def _reset_local_site(self): + return action_kwarg_map, actions_mapping, menu + + def _reset_site(self, selected_ids=None, site_name=None): """ Removes errors or success metadata for particular file >> forces redo of upload/download """ - self.sync_server.reset_provider_for_file( - self.model.project, - self.representation_id, - 'local', - self.item._id) + for file_id in selected_ids: + item = lib.get_item_by_id(self.model, file_id) + check_progress = self._get_progress(item, site_name, True) + + # do not reset if opposite side is not fully there + if check_progress != 1: + log.debug("Not fully available {} on other side, skipping". + format(check_progress)) + continue + + self.sync_server.reset_provider_for_file( + self.model.project, + self.representation_id, + site_name=site_name, + file_id=file_id, + force=True) self.model.refresh( load_records=self.model._rec_loaded) - def _reset_remote_site(self): - """ - Removes errors or success metadata for particular file >> forces - redo of upload/download - """ - self.sync_server.reset_provider_for_file( - self.model.project, - self.representation_id, - 'remote', - self.item._id) - self.model.refresh( - load_records=self.model._rec_loaded) - def _open_in_explorer(self, site): - if not self.item: - return +class SyncRepresentationErrorWindow(QtWidgets.QDialog): + """Wrapper window to show errors during sync on file(s)""" + def __init__(self, model, selected_ids, parent=None): + super(SyncRepresentationErrorWindow, self).__init__(parent) + self.setWindowFlags(QtCore.Qt.Window) + self.setFocusPolicy(QtCore.Qt.StrongFocus) - fpath = self.item.path - project = self.project - fpath = self.sync_server.get_local_file_path(project, site, fpath) + self.setStyleSheet(style.load_stylesheet()) + self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) + self.resize(900, 150) - fpath = os.path.normpath(os.path.dirname(fpath)) - if os.path.isdir(fpath): - if 'win' in sys.platform: # windows - subprocess.Popen('explorer "%s"' % fpath) - elif sys.platform == 'darwin': # macOS - subprocess.Popen(['open', fpath]) - else: # linux - try: - subprocess.Popen(['xdg-open', fpath]) - except OSError: - raise OSError('unsupported xdg-open call??') + body = QtWidgets.QWidget() - def _save_scrollbar(self): - self._scrollbar_pos = self.table_view.verticalScrollBar().value() + container = SyncRepresentationErrorWidget(model, + selected_ids, + parent=self) + body_layout = QtWidgets.QHBoxLayout(body) + body_layout.addWidget(container) + body_layout.setContentsMargins(0, 0, 0, 0) - def _set_scrollbar(self): - if self._scrollbar_pos: - self.table_view.verticalScrollBar().setValue(self._scrollbar_pos) + message = QtWidgets.QLabel() + message.hide() + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(body) + + self.setLayout(body_layout) + self.setWindowTitle("Sync Representation Error Detail") class SyncRepresentationErrorWidget(QtWidgets.QWidget): """ - Dialog to show when sync error happened, prints error message + Dialog to show when sync error happened, prints formatted error message """ - - def __init__(self, _id, dt, tries, msg, parent=None): + def __init__(self, model, selected_ids, parent=None): super(SyncRepresentationErrorWidget, self).__init__(parent) - layout = QtWidgets.QHBoxLayout(self) + layout = QtWidgets.QVBoxLayout(self) - txts = [] - txts.append("{}: {}".format("Last update date", pretty_timestamp(dt))) - txts.append("{}: {}".format("Retries", str(tries))) - txts.append("{}: {}".format("Error message", msg)) + no_errors = True + for file_id in selected_ids: + item = lib.get_item_by_id(model, file_id) + if not item.created_dt or not item.sync_dt or not item.error: + continue - text_area = QtWidgets.QPlainTextEdit("\n\n".join(txts)) - text_area.setReadOnly(True) - layout.addWidget(text_area) + no_errors = False + dt = max(item.created_dt, item.sync_dt) + + txts = [] + txts.append("{}: {}
".format("Last update date", + pretty_timestamp(dt))) + txts.append("{}: {}
".format("Retries", + str(item.tries))) + txts.append("{}: {}
".format("Error message", + item.error)) + + text_area = QtWidgets.QTextEdit("\n\n".join(txts)) + text_area.setReadOnly(True) + layout.addWidget(text_area) + + if no_errors: + text_area = QtWidgets.QTextEdit() + text_area.setText("

No errors located

") + text_area.setReadOnly(True) + layout.addWidget(text_area) class ImageDelegate(QtWidgets.QStyledItemDelegate): @@ -830,72 +826,8 @@ class ImageDelegate(QtWidgets.QStyledItemDelegate): QtGui.QBrush(QtGui.QColor(255, 0, 0, 35))) -class SyncServerDetailWindow(QtWidgets.QDialog): - def __init__(self, sync_server, _id, project, parent=None): - log.debug( - "!!! SyncServerDetailWindow _id:: {}".format(_id)) - super(SyncServerDetailWindow, self).__init__(parent) - self.setWindowFlags(QtCore.Qt.Window) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - self.setStyleSheet(style.load_stylesheet()) - self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) - self.resize(1000, 400) - - body = QtWidgets.QWidget() - footer = QtWidgets.QWidget() - footer.setFixedHeight(20) - - container = SyncRepresentationDetailWidget(sync_server, _id, project, - parent=self) - body_layout = QtWidgets.QHBoxLayout(body) - body_layout.addWidget(container) - body_layout.setContentsMargins(0, 0, 0, 0) - - self.message = QtWidgets.QLabel() - self.message.hide() - - footer_layout = QtWidgets.QVBoxLayout(footer) - footer_layout.addWidget(self.message) - footer_layout.setContentsMargins(0, 0, 0, 0) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(body) - layout.addWidget(footer) - - self.setLayout(body_layout) - self.setWindowTitle("Sync Representation Detail") - - -class SyncRepresentationErrorWindow(QtWidgets.QDialog): - def __init__(self, _id, project, dt, tries, msg, parent=None): - super(SyncRepresentationErrorWindow, self).__init__(parent) - self.setWindowFlags(QtCore.Qt.Window) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - self.setStyleSheet(style.load_stylesheet()) - self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) - self.resize(900, 150) - - body = QtWidgets.QWidget() - - container = SyncRepresentationErrorWidget(_id, dt, tries, msg, - parent=self) - body_layout = QtWidgets.QHBoxLayout(body) - body_layout.addWidget(container) - body_layout.setContentsMargins(0, 0, 0, 0) - - message = QtWidgets.QLabel() - message.hide() - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(body) - - self.setLayout(body_layout) - self.setWindowTitle("Sync Representation Error Detail") - - class TransparentWidget(QtWidgets.QWidget): + """Used for header cell for resizing to work properly""" clicked = QtCore.Signal(str) def __init__(self, column_name, *args, **kwargs): @@ -911,7 +843,7 @@ class TransparentWidget(QtWidgets.QWidget): class HorizontalHeader(QtWidgets.QHeaderView): - + """Reiplemented QHeaderView to contain clickable changeable button""" def __init__(self, parent=None): super(HorizontalHeader, self).__init__(QtCore.Qt.Horizontal, parent) self._parent = parent @@ -939,6 +871,7 @@ class HorizontalHeader(QtWidgets.QHeaderView): return self._parent.model def init_layout(self): + """Initial preparation of header's content""" 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) @@ -958,6 +891,7 @@ class HorizontalHeader(QtWidgets.QHeaderView): self.filter_buttons[column_name] = button def showEvent(self, event): + """Paint header""" super(HorizontalHeader, self).showEvent(event) for i in range(len(self.header_cells)): @@ -968,6 +902,7 @@ class HorizontalHeader(QtWidgets.QHeaderView): cell_content.show() def _set_filter_icon(self, column_name): + """Set different states of button depending on its engagement""" button = self.filter_buttons.get(column_name) if button: if self.checked_values.get(column_name): From e08745a1d19038a4130c93cf5fc451eb4e237e1f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 23 Apr 2021 10:39:06 +0200 Subject: [PATCH 14/18] Hound --- openpype/modules/sync_server/tray/widgets.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index c552904244..21236dc64a 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -310,8 +310,7 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): self.sync_server.add_site( self.model.project, representation_id, - site_name - ) + site_name) self.message_generated.emit( "Site {} added for {}".format(site_name, representation_id)) @@ -444,7 +443,8 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget): txt_filter = QtWidgets.QLineEdit() txt_filter.setPlaceholderText("Quick filter representations..") txt_filter.setClearButtonEnabled(True) - txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"), + txt_filter.addAction( + qtawesome.icon("fa.filter", color="gray"), QtWidgets.QLineEdit.LeadingPosition) self.txt_filter = txt_filter @@ -505,7 +505,6 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget): self.selection_model = self.table_view.selectionModel() self.selection_model.selectionChanged.connect(self._selection_changed) - def _prepare_menu(self, item, is_multi): action_kwarg_map, actions_mapping, menu = \ super()._prepare_menu(item, is_multi) @@ -683,7 +682,7 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget): menu.addAction(action) return action_kwarg_map, actions_mapping, menu - + def _reset_site(self, selected_ids=None, site_name=None): """ Removes errors or success metadata for particular file >> forces From bf9479aa5319cc04f48f353991ae0066e5e15a32 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 23 Apr 2021 12:25:38 +0200 Subject: [PATCH 15/18] modified pyside installation to be independent on application name stored in settings --- .../hosts/blender/hooks/pre_pyside_install.py | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py index 088a27566d..613dc154c5 100644 --- a/openpype/hosts/blender/hooks/pre_pyside_install.py +++ b/openpype/hosts/blender/hooks/pre_pyside_install.py @@ -1,4 +1,5 @@ import os +import re import subprocess from openpype.lib import PreLaunchHook @@ -31,10 +32,46 @@ class InstallPySideToBlender(PreLaunchHook): def inner_execute(self): # Get blender's python directory + version_regex = re.compile("^2\.[0-9]{2}$") + executable = self.launch_context.executable.executable_path - # Blender installation contain subfolder named with it's version where - # python binaries are stored. - version_subfolder = self.launch_context.app_name.split("_")[1] + if os.path.basename(executable).lower() != "blender.exe": + self.log.info(( + "Executable does not lead to blender.exe file. Can't determine" + " blender's python to check/install PySide2." + )) + return + + executable_dir = os.path.dirname(executable) + version_subfolders = [] + for name in os.listdir(executable_dir): + fullpath = os.path.join(name, executable_dir) + if not os.path.isdir(fullpath): + continue + + if not version_regex.match(name): + continue + + version_subfolders.append(name) + + if not version_subfolders: + self.log.info( + "Didn't find version subfolder next to Blender executable" + ) + return + + if len(version_subfolders) > 1: + self.log.info(( + "Found more than one version subfolder next" + " to blender executable. {}" + ).format(", ".join([ + '"./{}"'.format(name) + for name in version_subfolders + ]))) + return + + version_subfolder = version_subfolders[0] + pythond_dir = os.path.join( os.path.dirname(executable), version_subfolder, From 2b4811020b9b3df2517edcf75d665987a31f34b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 23 Apr 2021 12:30:24 +0200 Subject: [PATCH 16/18] fixed regex string --- openpype/hosts/blender/hooks/pre_pyside_install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py index 613dc154c5..6d253300d9 100644 --- a/openpype/hosts/blender/hooks/pre_pyside_install.py +++ b/openpype/hosts/blender/hooks/pre_pyside_install.py @@ -32,7 +32,7 @@ class InstallPySideToBlender(PreLaunchHook): def inner_execute(self): # Get blender's python directory - version_regex = re.compile("^2\.[0-9]{2}$") + version_regex = re.compile(r"^2\.[0-9]{2}$") executable = self.launch_context.executable.executable_path if os.path.basename(executable).lower() != "blender.exe": @@ -102,6 +102,7 @@ class InstallPySideToBlender(PreLaunchHook): # Check if PySide2 is installed and skip if yes if self.is_pyside_installed(python_executable): + self.log.debug("Blender has already installed PySide2.") return # Install PySide2 in blender's python From b16c920b5d5d9a3f8bc10fd8d40d2dc25047a04e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 23 Apr 2021 17:24:25 +0200 Subject: [PATCH 17/18] Added missing RGBA template --- .../schemas/template_rgba_color.json | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_rgba_color.json diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_rgba_color.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_rgba_color.json new file mode 100644 index 0000000000..ffe530175a --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_rgba_color.json @@ -0,0 +1,33 @@ +[ + { + "type": "list-strict", + "key": "{name}", + "label": "{label}", + "object_types": [ + { + "label": "R", + "type": "number", + "minimum": 0, + "maximum": 255 + }, + { + "label": "G", + "type": "number", + "minimum": 0, + "maximum": 255 + }, + { + "label": "B", + "type": "number", + "minimum": 0, + "maximum": 255 + }, + { + "label": "A", + "type": "number", + "minimum": 0, + "maximum": 255 + } + ] + } +] From a647eedf86b7c0c4bbae51fffd74a72ae198f814 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Apr 2021 11:31:11 +0200 Subject: [PATCH 18/18] changed label --- .../schemas/projects_schema/schemas/schema_global_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index e6be868068..1bd028ac79 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -218,7 +218,7 @@ }, { "key": "ratio", - "label": "Letter box ratio", + "label": "Ratio", "type": "number", "decimal": 4, "default": 0,