From fa6c491d5286002e7eb6018a1b6f2c5f6dc721a2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 14 Apr 2021 10:23:39 +0100 Subject: [PATCH 01/58] Fixed alembic extraction --- openpype/hosts/blender/plugins/publish/extract_abc.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index 6a89c6019b..e1eef61560 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -18,7 +18,7 @@ class ExtractABC(openpype.api.Extractor): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.fbx" + filename = f"{instance.name}.abc" filepath = os.path.join(stagingdir, filename) context = bpy.context @@ -72,9 +72,7 @@ class ExtractABC(openpype.api.Extractor): # We export the abc bpy.ops.wm.alembic_export( new_context, - filepath=filepath, - start=1, - end=1 + filepath=filepath ) view_layer.active_layer_collection = old_active_layer_collection From b1b7eda2acc33e564137d1c0690f6056b69ceaaf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 14 Apr 2021 11:13:00 +0100 Subject: [PATCH 02/58] Implemented Alembic loader --- .../hosts/blender/plugins/load/load_model.py | 77 +++++++++++++------ 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_model.py b/openpype/hosts/blender/plugins/load/load_model.py index 7297e459a6..d679807534 100644 --- a/openpype/hosts/blender/plugins/load/load_model.py +++ b/openpype/hosts/blender/plugins/load/load_model.py @@ -7,6 +7,7 @@ from typing import Dict, List, Optional from avalon import api, blender import bpy +import openpype.api import openpype.hosts.blender.api.plugin as plugin @@ -108,6 +109,10 @@ class BlendModelLoader(plugin.AssetLoader): self.__class__.__name__, ) + print(lib_container) + print(container) + + container_metadata = container.get( blender.pipeline.AVALON_PROPERTY) @@ -271,36 +276,62 @@ class CacheModelLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - raise NotImplementedError( - "Loading of Alembic files is not yet implemented.") - # TODO (jasper): implement Alembic import. libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - # TODO (jasper): evaluate use of namespace which is 'alien' to Blender. - lib_container = container_name = ( - plugin.asset_name(asset, subset, namespace) - ) - relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - libpath, link=True, relative=relative - ) as (data_from, data_to): - data_to.collections = [lib_container] + lib_container = plugin.asset_name( + asset, subset + ) + unique_number = plugin.get_unique_number( + asset, subset + ) + namespace = namespace or f"{asset}_{unique_number}" + container_name = plugin.asset_name( + asset, subset, unique_number + ) + + container = bpy.data.collections.new(lib_container) + container.name = container_name + blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + bpy.ops.object.select_all(action='DESELECT') + + view_layer = bpy.context.view_layer + view_layer_collection = view_layer.active_layer_collection.collection + + relative = bpy.context.preferences.filepaths.use_relative_paths + bpy.ops.wm.alembic_import( + filepath=libpath, + relative_path=relative + ) + + parent = bpy.data.collections.new(container_name) + for obj in bpy.context.selected_objects: + parent.objects.link(obj) + view_layer_collection.objects.unlink(obj) + obj.name = f"{obj.name}:{container_name}" + obj.data.name = f"{obj.data.name}:{container_name}" + + bpy.ops.object.select_all(action='DESELECT') scene = bpy.context.scene - instance_empty = bpy.data.objects.new( - container_name, None - ) - scene.collection.objects.link(instance_empty) - instance_empty.instance_type = 'COLLECTION' - collection = bpy.data.collections[lib_container] - collection.name = container_name - instance_empty.instance_collection = collection + scene.collection.children.link(parent) - nodes = list(collection.objects) - nodes.append(collection) - nodes.append(instance_empty) + nodes = list(parent.objects) + nodes.append(parent) self[:] = nodes return nodes From 5e54d09ca1ce64d3f6cd5a9dd2248315330002f8 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 14 Apr 2021 11:37:36 +0100 Subject: [PATCH 03/58] Alembic loader is now more consistent with loaders for other formats --- .../hosts/blender/plugins/load/load_model.py | 69 +++++++++++-------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_model.py b/openpype/hosts/blender/plugins/load/load_model.py index d679807534..f587a20fa1 100644 --- a/openpype/hosts/blender/plugins/load/load_model.py +++ b/openpype/hosts/blender/plugins/load/load_model.py @@ -7,7 +7,6 @@ from typing import Dict, List, Optional from avalon import api, blender import bpy -import openpype.api import openpype.hosts.blender.api.plugin as plugin @@ -109,10 +108,6 @@ class BlendModelLoader(plugin.AssetLoader): self.__class__.__name__, ) - print(lib_container) - print(container) - - container_metadata = container.get( blender.pipeline.AVALON_PROPERTY) @@ -265,6 +260,41 @@ class CacheModelLoader(plugin.AssetLoader): icon = "code-fork" color = "orange" + def _process(self, libpath, container_name, parent_collection): + bpy.ops.object.select_all(action='DESELECT') + + view_layer = bpy.context.view_layer + view_layer_collection = view_layer.active_layer_collection.collection + + relative = bpy.context.preferences.filepaths.use_relative_paths + bpy.ops.wm.alembic_import( + filepath=libpath, + relative_path=relative + ) + + parent = parent_collection + + if parent is None: + parent = bpy.context.scene.collection + + model_container = bpy.data.collections.new(container_name) + parent.children.link(model_container) + for obj in bpy.context.selected_objects: + model_container.objects.link(obj) + view_layer_collection.objects.unlink(obj) + obj.name = f"{obj.name}:{container_name}" + obj.data.name = f"{obj.data.name}:{container_name}" + + if not obj.get(blender.pipeline.AVALON_PROPERTY): + obj[blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + bpy.ops.object.select_all(action='DESELECT') + + return model_container + def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, options: Optional[Dict] = None @@ -308,30 +338,15 @@ class CacheModelLoader(plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - bpy.ops.object.select_all(action='DESELECT') + obj_container = self._process( + libpath, container_name, None) - view_layer = bpy.context.view_layer - view_layer_collection = view_layer.active_layer_collection.collection + container_metadata["obj_container"] = obj_container - relative = bpy.context.preferences.filepaths.use_relative_paths - bpy.ops.wm.alembic_import( - filepath=libpath, - relative_path=relative - ) + # Save the list of objects in the metadata container + container_metadata["objects"] = obj_container.all_objects - parent = bpy.data.collections.new(container_name) - for obj in bpy.context.selected_objects: - parent.objects.link(obj) - view_layer_collection.objects.unlink(obj) - obj.name = f"{obj.name}:{container_name}" - obj.data.name = f"{obj.data.name}:{container_name}" - - bpy.ops.object.select_all(action='DESELECT') - - scene = bpy.context.scene - scene.collection.children.link(parent) - - nodes = list(parent.objects) - nodes.append(parent) + nodes = list(container.objects) + nodes.append(container) self[:] = nodes return nodes From e08b04a5ba137190cb16e14d95f2c7a024d6a401 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 14 Apr 2021 15:13:31 +0100 Subject: [PATCH 04/58] Implemented update and remove functions --- .../hosts/blender/plugins/load/load_model.py | 136 +++++++++++++++++- 1 file changed, 134 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_model.py b/openpype/hosts/blender/plugins/load/load_model.py index f587a20fa1..db12585efb 100644 --- a/openpype/hosts/blender/plugins/load/load_model.py +++ b/openpype/hosts/blender/plugins/load/load_model.py @@ -260,6 +260,12 @@ class CacheModelLoader(plugin.AssetLoader): icon = "code-fork" color = "orange" + def _remove(self, objects, container): + for obj in list(objects): + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(container) + def _process(self, libpath, container_name, parent_collection): bpy.ops.object.select_all(action='DESELECT') @@ -282,8 +288,20 @@ class CacheModelLoader(plugin.AssetLoader): for obj in bpy.context.selected_objects: model_container.objects.link(obj) view_layer_collection.objects.unlink(obj) - obj.name = f"{obj.name}:{container_name}" - obj.data.name = f"{obj.data.name}:{container_name}" + + name = obj.name + data_name = obj.data.name + obj.name = f"{name}:{container_name}" + obj.data.name = f"{data_name}:{container_name}" + + # Blender handles alembic with a modifier linked to a cache file. + # Here we create the modifier for the object and link it with the + # loaded cache file. + modifier = obj.modifiers.new( + name="MeshSequenceCache", type='MESH_SEQUENCE_CACHE') + cache_file = bpy.path.basename(libpath) + modifier.cache_file = bpy.data.cache_files[cache_file] + modifier.object_path = f"/{name}/{data_name}" if not obj.get(blender.pipeline.AVALON_PROPERTY): obj[blender.pipeline.AVALON_PROPERTY] = dict() @@ -350,3 +368,117 @@ class CacheModelLoader(plugin.AssetLoader): nodes.append(container) self[:] = nodes return nodes + + def update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + collection = bpy.data.collections.get( + container["objectName"] + ) + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + self.log.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert not (collection.children), ( + "Nested collections are not supported." + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + collection_metadata = collection.get( + blender.pipeline.AVALON_PROPERTY) + collection_libpath = collection_metadata["libpath"] + + obj_container = plugin.get_local_collection_with_name( + collection_metadata["obj_container"].name + ) + objects = obj_container.all_objects + + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + self.log.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + self.log.info("Library already loaded, not updating...") + return + + # Check if the cache file has already been loaded + if bpy.path.basename(str(libpath)) not in bpy.data.cache_files: + bpy.ops.cachefile.open(filepath=str(libpath)) + + # Set the new cache file in the objects that use the modifier + for obj in objects: + for modifier in obj.modifiers: + if modifier.type == 'MESH_SEQUENCE_CACHE': + cache_file = bpy.path.basename(str(libpath)) + modifier.cache_file = bpy.data.cache_files[cache_file] + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (openpype:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + + collection_metadata = collection.get( + blender.pipeline.AVALON_PROPERTY) + + obj_container = plugin.get_local_collection_with_name( + collection_metadata["obj_container"].name + ) + objects = obj_container.all_objects + + self._remove(objects, obj_container) + + bpy.data.collections.remove(collection) + + # We should delete the cache file used in the modifier too, + # but Blender does not allow to do that from python. + + return True From 4a8339dd12ccec175eee24219db5bd9eab90617a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 14 Apr 2021 15:14:07 +0100 Subject: [PATCH 05/58] Added .abc as valid extension --- openpype/hosts/blender/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index eb88e7af63..de30da3319 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -9,7 +9,7 @@ from avalon import api import avalon.blender from openpype.api import PypeCreatorMixin -VALID_EXTENSIONS = [".blend", ".json"] +VALID_EXTENSIONS = [".blend", ".json", ".abc"] def asset_name( From 5f58f18cd95d71835614ff51f7107dece5202121 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 14 Apr 2021 15:59:15 +0100 Subject: [PATCH 06/58] Fixed problem that published objects not in the instance --- openpype/hosts/blender/plugins/publish/extract_abc.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index e1eef61560..a7653d9f5a 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -52,6 +52,8 @@ class ExtractABC(openpype.api.Extractor): old_scale = scene.unit_settings.scale_length + bpy.ops.object.select_all(action='DESELECT') + selected = list() for obj in instance: @@ -67,12 +69,11 @@ class ExtractABC(openpype.api.Extractor): # We set the scale of the scene for the export scene.unit_settings.scale_length = 0.01 - self.log.info(new_context) - # We export the abc bpy.ops.wm.alembic_export( new_context, - filepath=filepath + filepath=filepath, + selected=True ) view_layer.active_layer_collection = old_active_layer_collection From 5b907a9d661ea376fba05de86cd29b338aa21fc5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 15 Apr 2021 15:39:55 +0200 Subject: [PATCH 07/58] Initial version of column filtering --- openpype/modules/sync_server/tray/lib.py | 7 + openpype/modules/sync_server/tray/models.py | 140 +++++++++++++---- openpype/modules/sync_server/tray/widgets.py | 155 ++++++++++++++++++- 3 files changed, 262 insertions(+), 40 deletions(-) diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 0282d79ea1..051567ed6c 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -1,4 +1,5 @@ from Qt import QtCore +import attr from openpype.lib import PypeLogger @@ -20,8 +21,14 @@ ProviderRole = QtCore.Qt.UserRole + 2 ProgressRole = QtCore.Qt.UserRole + 4 DateRole = QtCore.Qt.UserRole + 6 FailedRole = QtCore.Qt.UserRole + 8 +HeaderNameRole = QtCore.Qt.UserRole + 10 +@attr.s +class FilterDefinition: + type = attr.ib() + values = attr.ib(factory=list) + def pretty_size(value, suffix='B'): for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: if abs(value) < 1024.0: diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 3cc53c6ec4..444422c56a 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -56,6 +56,10 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): """Returns project""" return self._project + @property + def column_filtering(self): + return self._column_filtering + def rowCount(self, _index): return len(self._data) @@ -65,7 +69,15 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): def headerData(self, section, orientation, role): if role == Qt.DisplayRole: if orientation == Qt.Horizontal: - return self.COLUMN_LABELS[section][1] + name = self.COLUMN_LABELS[section][0] + txt = "" + if name in self.column_filtering.keys(): + txt = "(F)" + return self.COLUMN_LABELS[section][1] + txt # return label + + if role == lib.HeaderNameRole: + if orientation == Qt.Horizontal: + return self.COLUMN_LABELS[section][0] # return name def get_header_index(self, value): """ @@ -190,6 +202,35 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): self.word_filter = word_filter self.refresh() + def get_column_filter_values(self, index): + """ + Returns list of available values for filtering in the column + + Args: + index(int): index of column in header + + Returns: + (dict) of value: label shown in filtering menu + 'value' is used in MongoDB query, 'label' is human readable for + menu + for some columns ('subset') might be 'value' and 'label' same + """ + column_name = self._header[index] + + filter_def = self.COLUMN_FILTERS.get(column_name) + if not filter_def: + return {} + + if filter_def['type'] == 'predefined_set': + return dict(filter_def['values']) + elif filter_def['type'] == 'available_values': + recs = self.dbcon.find({'type': column_name}, {"name": 1, + "_id": -1}) + values = {} + for item in recs: + values[item["name"]] = item["name"] + return dict(sorted(values.items(), key=lambda item: item[1])) + def set_project(self, project): """ Changes project, called after project selection is changed @@ -251,7 +292,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): ("files_count", "Files"), ("files_size", "Size"), ("priority", "Priority"), - ("state", "Status") + ("status", "Status") ] DEFAULT_SORT = { @@ -259,18 +300,26 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): "_id": 1 } SORT_BY_COLUMN = [ - "context.asset", # asset - "context.subset", # subset - "context.version", # version - "context.representation", # representation + "asset", # asset + "subset", # subset + "version", # version + "representation", # representation "updated_dt_local", # local created_dt "updated_dt_remote", # remote created_dt "files_count", # count of files "files_size", # file size of all files "context.asset", # priority TODO - "status" # state + "status" # status ] + COLUMN_FILTERS = { + 'status': {'type': 'predefined_set', + 'values': {k: v for k, v in lib.STATUS.items()}}, + 'subset': {'type': 'available_values'}, + 'asset': {'type': 'available_values'}, + 'representation': {'type': 'available_values'} + } + refresh_started = QtCore.Signal() refresh_finished = QtCore.Signal() @@ -297,7 +346,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): files_count = attr.ib(default=None) files_size = attr.ib(default=None) priority = attr.ib(default=None) - state = attr.ib(default=None) + status = attr.ib(default=None) path = attr.ib(default=None) def __init__(self, sync_server, header, project=None): @@ -308,6 +357,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self._rec_loaded = 0 self._total_records = 0 # how many documents query actually found self.word_filter = None + self._column_filtering = {} self._initialized = False if not self._project or self._project == lib.DUMMY_PROJECT: @@ -359,9 +409,9 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): if role == lib.FailedRole: if header_value == 'local_site': - return item.state == lib.STATUS[2] and item.local_progress < 1 + return item.status == lib.STATUS[2] and item.local_progress < 1 if header_value == 'remote_site': - return item.state == lib.STATUS[2] and item.remote_progress < 1 + return item.status == lib.STATUS[2] and item.remote_progress < 1 if role == Qt.DisplayRole: # because of ImageDelegate @@ -397,7 +447,6 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): remote_site) for repre in result.get("paginatedResults"): - context = repre.get("context").pop() files = repre.get("files", []) if isinstance(files, dict): # aggregate returns dictionary files = [files] @@ -420,17 +469,17 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): avg_progress_local = lib.convert_progress( repre.get('avg_progress_local', '0')) - if context.get("version"): - version = "v{:0>3d}".format(context.get("version")) + if repre.get("version"): + version = "v{:0>3d}".format(repre.get("version")) else: version = "master" item = self.SyncRepresentation( repre.get("_id"), - context.get("asset"), - context.get("subset"), + repre.get("asset"), + repre.get("subset"), version, - context.get("representation"), + repre.get("representation"), local_updated, remote_updated, local_site, @@ -461,7 +510,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): 'sync_dt' - same for remote side 'local_site' - progress of repr on local side, 1 = finished 'remote_site' - progress on remote side, calculates from files - 'state' - + 'status' - 0 - in progress 1 - failed 2 - queued @@ -481,7 +530,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): if limit == 0: limit = SyncRepresentationSummaryModel.PAGE_SIZE - return [ + aggr = [ {"$match": self.get_match_part()}, {'$unwind': '$files'}, # merge potentially unwinded records back to single per repre @@ -584,16 +633,43 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): 'paused_local': {'$sum': '$paused_local'}, 'updated_dt_local': {'$max': "$updated_dt_local"} }}, - {"$project": self.projection}, - {"$sort": self.sort}, - { + {"$project": self.projection} + ] + + if self.column_filtering: + aggr.append( + {"$match": self.column_filtering} + ) + print(self.column_filtering) + + aggr.extend( + [{"$sort": self.sort}, + { '$facet': { 'paginatedResults': [{'$skip': self._rec_loaded}, {'$limit': limit}], 'totalCount': [{'$count': 'count'}] } - } - ] + }] + ) + + return aggr + + def set_column_filtering(self, checked_values): + """ + Sets dictionary used in '$match' part of MongoDB aggregate + + Args: + checked_values(dict): key:values ({'status':{1:"Foo",3:"Bar"}} + + Modifies: + self._column_filtering : {'status': {'$in': [1, 2, 3]}} + """ + filtering = {} + for key, dict_value in checked_values.items(): + filtering[key] = {'$in': list(dict_value.keys())} + + self._column_filtering = filtering def get_match_part(self): """ @@ -639,10 +715,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): (dict) """ return { - "context.subset": 1, - "context.asset": 1, - "context.version": 1, - "context.representation": 1, + "subset": {"$first": "$context.subset"}, + "asset": {"$first": "$context.asset"}, + "version": {"$first": "$context.version"}, + "representation": {"$first": "$context.representation"}, "data.path": 1, "files": 1, 'files_count': 1, @@ -721,7 +797,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): ("remote_site", "Remote site"), ("files_size", "Size"), ("priority", "Priority"), - ("state", "Status") + ("status", "Status") ] PAGE_SIZE = 30 @@ -734,7 +810,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): "updated_dt_remote", # remote created_dt "size", # remote progress "context.asset", # priority TODO - "status" # state + "status" # status ] refresh_started = QtCore.Signal() @@ -759,7 +835,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): remote_progress = attr.ib(default=None) size = attr.ib(default=None) priority = attr.ib(default=None) - state = attr.ib(default=None) + status = attr.ib(default=None) tries = attr.ib(default=None) error = attr.ib(default=None) path = attr.ib(default=None) @@ -821,9 +897,9 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): if role == lib.FailedRole: if header_value == 'local_site': - return item.state == lib.STATUS[2] and item.local_progress < 1 + return item.status == lib.STATUS[2] and item.local_progress < 1 if header_value == 'remote_site': - return item.state == lib.STATUS[2] and item.remote_progress < 1 + return item.status == lib.STATUS[2] and item.remote_progress < 1 if role == Qt.DisplayRole: # because of ImageDelegate diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 5071ffa2b0..5719d13716 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -1,6 +1,7 @@ import os import subprocess import sys +from functools import partial from Qt import QtWidgets, QtCore, QtGui from Qt.QtCore import Qt @@ -91,7 +92,7 @@ class SyncProjectListWidget(ProjectListWidget): self.project_name = point_index.data(QtCore.Qt.DisplayRole) menu = QtWidgets.QMenu() - menu.setStyleSheet(style.load_stylesheet()) + #menu.setStyleSheet(style.load_stylesheet()) actions_mapping = {} if self.sync_server.is_project_paused(self.project_name): @@ -150,7 +151,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): ("files_count", 50), ("files_size", 60), ("priority", 50), - ("state", 110) + ("status", 110) ) def __init__(self, sync_server, project=None, parent=None): @@ -217,6 +218,14 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.selection_model = self.table_view.selectionModel() self.selection_model.selectionChanged.connect(self._selection_changed) + self.checked_values = {} + + self.horizontal_header = self.table_view.horizontalHeader() + self.horizontal_header.setContextMenuPolicy( + QtCore.Qt.CustomContextMenu) + self.horizontal_header.customContextMenuRequested.connect( + self._on_section_clicked) + def _selection_changed(self, _new_selection): index = self.selection_model.currentIndex() self._selected_id = \ @@ -246,6 +255,136 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.sync_server, _id, self.table_view.model().project) detail_window.exec() + def _on_section_clicked(self, point): + + logical_index = self.horizontal_header.logicalIndexAt(point) + + model = self.table_view.model() + column_name = model.headerData(logical_index, + Qt.Horizontal, lib.HeaderNameRole) + items_dict = model.get_column_filter_values(logical_index) + + if not items_dict: + return + + menu = QtWidgets.QMenu(self) + + # text filtering only if labels same as values, not if codes are used + if list(items_dict.keys())[0] == list(items_dict.values())[0]: + self.line_edit = QtWidgets.QLineEdit(self) + self.line_edit.setPlaceholderText("Type and enter...") + action_le = QtWidgets.QWidgetAction(menu) + action_le.setDefaultWidget(self.line_edit) + self.line_edit.returnPressed.connect( + partial(self._apply_text_filter, column_name, items_dict)) + menu.addAction(action_le) + menu.addSeparator() + + action_all = QtWidgets.QAction("All", self) + state_checked = 2 + # action_all.triggered.connect(partial(self._apply_filter, column_name, + # items_dict, state_checked)) + action_all.triggered.connect(partial(self._reset_filter, column_name)) + menu.addAction(action_all) + + action_none = QtWidgets.QAction("Unselect all", self) + state_unchecked = 0 + action_none.triggered.connect(partial(self._apply_filter, column_name, + items_dict, state_unchecked)) + menu.addAction(action_none) + menu.addSeparator() + + # nothing explicitly >> ALL implicitly >> first time + if self.checked_values.get(column_name) is None: + checked_keys = items_dict.keys() + else: + checked_keys = self.checked_values[column_name] + + for value, label in items_dict.items(): + checkbox = QtWidgets.QCheckBox(str(label), menu) + if value in checked_keys: + checkbox.setChecked(True) + + action = QtWidgets.QWidgetAction(menu) + action.setDefaultWidget(checkbox) + + checkbox.stateChanged.connect(partial(self._apply_filter, + column_name, {value: label})) + menu.addAction(action) + + self.menu = menu + self.menu_items_dict = items_dict # all available items + self.menu.exec_(QtGui.QCursor.pos()) + + def _reset_filter(self, column_name): + """ + Remove whole column from filter >> not in $match at all (faster) + """ + if self.checked_values.get(column_name) is not None: + self.checked_values.pop(column_name) + self._refresh_model_and_menu(column_name, True, True) + + def _apply_filter(self, column_name, values, state): + """ + Sets 'values' to specific 'state' (checked/unchecked), + sends to model. + """ + self._update_checked_values(column_name, values, state) + self._refresh_model_and_menu(column_name, True, False) + + def _apply_text_filter(self, column_name, items): + """ + Resets all checkboxes, prefers inserted text. + """ + self._update_checked_values(column_name, items, 0) # reset other + text_item = {self.line_edit.text(): self.line_edit.text()} + self._update_checked_values(column_name, text_item, 2) + self._refresh_model_and_menu(column_name, True, True) + + def _refresh_model_and_menu(self, column_name, model=True, menu=True): + """ + Refresh model and its content and possibly menu for big changes. + """ + if model: + self.table_view.model().set_column_filtering(self.checked_values) + self.table_view.model().refresh() + if menu: + self._menu_refresh(column_name) + + def _menu_refresh(self, column_name): + """ + Reset boxes after big change - word filtering or reset + """ + for action in self.menu.actions(): + if not isinstance(action, QtWidgets.QWidgetAction): + continue + + widget = action.defaultWidget() + if not isinstance(widget, QtWidgets.QCheckBox): + continue + + if not self.checked_values.get(column_name) or \ + widget.text() in self.checked_values[column_name].values(): + widget.setChecked(True) + else: + widget.setChecked(False) + + def _update_checked_values(self, column_name, values, state): + """ + Modify dictionary of set values in columns for filtering. + + Modifies 'self.checked_values' + """ + checked = self.checked_values.get(column_name, self.menu_items_dict) + set_items = dict(values.items()) # prevent dictionary change during iter + for value, label in set_items.items(): + if state == 2: # checked + checked[value] = label + elif state == 0 and checked.get(value): + checked.pop(value) + + self.checked_values[column_name] = checked + def _on_context_menu(self, point): """ Shows menu with loader actions on Right-click. @@ -291,17 +430,17 @@ class SyncRepresentationWidget(QtWidgets.QWidget): else: self.site_name = remote_site - if self.item.state in [lib.STATUS[0], lib.STATUS[1]]: + if self.item.status in [lib.STATUS[0], lib.STATUS[1]]: action = QtWidgets.QAction("Pause") actions_mapping[action] = self._pause menu.addAction(action) - if self.item.state == lib.STATUS[3]: + if self.item.status == lib.STATUS[3]: action = QtWidgets.QAction("Unpause") actions_mapping[action] = self._unpause menu.addAction(action) - # if self.item.state == lib.STATUS[1]: + # if self.item.status == lib.STATUS[1]: # action = QtWidgets.QAction("Open error detail") # actions_mapping[action] = self._show_detail # menu.addAction(action) @@ -467,7 +606,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): ("remote_site", 185), ("size", 60), ("priority", 25), - ("state", 110) + ("status", 110) ) def __init__(self, sync_server, _id=None, project=None, parent=None): @@ -579,7 +718,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): self.item = self.table_view.model()._data[point_index.row()] menu = QtWidgets.QMenu() - menu.setStyleSheet(style.load_stylesheet()) + #menu.setStyleSheet(style.load_stylesheet()) actions_mapping = {} actions_kwargs_mapping = {} @@ -604,7 +743,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): actions_kwargs_mapping[action] = {'site': site} menu.addAction(action) - if self.item.state == lib.STATUS[2]: + if self.item.status == lib.STATUS[2]: action = QtWidgets.QAction("Open error detail") actions_mapping[action] = self._show_detail menu.addAction(action) From 313e0fbf1a074f8e8ced117ed67a720a2e1a534b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 16 Apr 2021 10:12:43 +0100 Subject: [PATCH 08/58] Support for grouping and changed the update process --- .../hosts/blender/plugins/load/load_model.py | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_model.py b/openpype/hosts/blender/plugins/load/load_model.py index db12585efb..168bdf9321 100644 --- a/openpype/hosts/blender/plugins/load/load_model.py +++ b/openpype/hosts/blender/plugins/load/load_model.py @@ -262,7 +262,10 @@ class CacheModelLoader(plugin.AssetLoader): def _remove(self, objects, container): for obj in list(objects): - bpy.data.meshes.remove(obj.data) + if obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + elif obj.type == 'EMPTY': + bpy.data.objects.remove(obj) bpy.data.collections.remove(container) @@ -290,18 +293,12 @@ class CacheModelLoader(plugin.AssetLoader): view_layer_collection.objects.unlink(obj) name = obj.name - data_name = obj.data.name obj.name = f"{name}:{container_name}" - obj.data.name = f"{data_name}:{container_name}" - # Blender handles alembic with a modifier linked to a cache file. - # Here we create the modifier for the object and link it with the - # loaded cache file. - modifier = obj.modifiers.new( - name="MeshSequenceCache", type='MESH_SEQUENCE_CACHE') - cache_file = bpy.path.basename(libpath) - modifier.cache_file = bpy.data.cache_files[cache_file] - modifier.object_path = f"/{name}/{data_name}" + # Groups are imported as Empty objects in Blender + if obj.type == 'MESH': + data_name = obj.data.name + obj.data.name = f"{data_name}:{container_name}" if not obj.get(blender.pipeline.AVALON_PROPERTY): obj[blender.pipeline.AVALON_PROPERTY] = dict() @@ -418,6 +415,8 @@ class CacheModelLoader(plugin.AssetLoader): ) objects = obj_container.all_objects + container_name = obj_container.name + normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -433,16 +432,17 @@ class CacheModelLoader(plugin.AssetLoader): self.log.info("Library already loaded, not updating...") return - # Check if the cache file has already been loaded - if bpy.path.basename(str(libpath)) not in bpy.data.cache_files: - bpy.ops.cachefile.open(filepath=str(libpath)) + parent = plugin.get_parent_collection(obj_container) - # Set the new cache file in the objects that use the modifier - for obj in objects: - for modifier in obj.modifiers: - if modifier.type == 'MESH_SEQUENCE_CACHE': - cache_file = bpy.path.basename(str(libpath)) - modifier.cache_file = bpy.data.cache_files[cache_file] + self._remove(objects, obj_container) + + obj_container = self._process( + str(libpath), container_name, parent) + + collection_metadata["obj_container"] = obj_container + collection_metadata["objects"] = obj_container.all_objects + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -478,7 +478,4 @@ class CacheModelLoader(plugin.AssetLoader): bpy.data.collections.remove(collection) - # We should delete the cache file used in the modifier too, - # but Blender does not allow to do that from python. - return True From 0949c2c68af7d89d0f940bf2e7d3886d84ac3510 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 19 Apr 2021 12:11:15 +0200 Subject: [PATCH 09/58] AE added scene duration validation --- openpype/hosts/aftereffects/api/__init__.py | 68 ++++++++++- .../plugins/publish/collect_render.py | 8 +- .../publish/validate_scene_settings.py | 110 ++++++++++++++++++ 3 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py diff --git a/openpype/hosts/aftereffects/api/__init__.py b/openpype/hosts/aftereffects/api/__init__.py index 9a80801652..7ad10cde25 100644 --- a/openpype/hosts/aftereffects/api/__init__.py +++ b/openpype/hosts/aftereffects/api/__init__.py @@ -5,7 +5,7 @@ import logging from avalon import io from avalon import api as avalon from avalon.vendor import Qt -from openpype import lib +from openpype import lib, api import pyblish.api as pyblish import openpype.hosts.aftereffects @@ -81,3 +81,69 @@ def uninstall(): def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle layer visibility on instance toggles.""" instance[0].Visible = new_value + + +def get_asset_settings(): + """Get settings on current asset from database. + + Returns: + dict: Scene data. + + """ + asset_data = lib.get_asset()["data"] + fps = asset_data.get("fps") + frame_start = asset_data.get("frameStart") + frame_end = asset_data.get("frameEnd") + handle_start = asset_data.get("handleStart") + handle_end = asset_data.get("handleEnd") + resolution_width = asset_data.get("resolutionWidth") + resolution_height = asset_data.get("resolutionHeight") + duration = frame_end + handle_end - min(frame_start - handle_start, 0) + entity_type = asset_data.get("entityType") + + scene_data = { + "fps": fps, + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": handle_start, + "handleEnd": handle_end, + "resolutionWidth": resolution_width, + "resolutionHeight": resolution_height, + "duration": duration + } + + try: + # temporary, in pype3 replace with api.get_current_project_settings + skip_resolution_check = ( + api.get_current_project_settings() + ["plugins"] + ["aftereffects"] + ["publish"] + ["ValidateSceneSettings"] + ["skip_resolution_check"] + ) + skip_timelines_check = ( + api.get_current_project_settings() + ["plugins"] + ["aftereffects"] + ["publish"] + ["ValidateSceneSettings"] + ["skip_timelines_check"] + ) + except KeyError: + skip_resolution_check = ['*'] + skip_timelines_check = ['*'] + + if os.getenv('AVALON_TASK') in skip_resolution_check or \ + '*' in skip_timelines_check: + scene_data.pop("resolutionWidth") + scene_data.pop("resolutionHeight") + + if entity_type in skip_timelines_check or '*' in skip_timelines_check: + scene_data.pop('fps', None) + scene_data.pop('frameStart', None) + scene_data.pop('frameEnd', None) + scene_data.pop('handleStart', None) + scene_data.pop('handleEnd', None) + + return scene_data diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index ba64551283..baac64ed0c 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -12,6 +12,7 @@ class AERenderInstance(RenderInstance): # extend generic, composition name is needed comp_name = attr.ib(default=None) comp_id = attr.ib(default=None) + fps = attr.ib(default=None) class CollectAERender(abstract_collect_render.AbstractCollectRender): @@ -45,6 +46,7 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): raise ValueError("Couldn't find id, unable to publish. " + "Please recreate instance.") item_id = inst["members"][0] + work_area_info = self.stub.get_work_area(int(item_id)) if not work_area_info: @@ -57,6 +59,8 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): frameEnd = round(work_area_info.workAreaStart + float(work_area_info.workAreaDuration) * float(work_area_info.frameRate)) - 1 + fps = work_area_info.frameRate + # TODO add resolution when supported by extension if inst["family"] == "render" and inst["active"]: instance = AERenderInstance( @@ -86,7 +90,8 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): frameStart=frameStart, frameEnd=frameEnd, frameStep=1, - toBeRenderedOn='deadline' + toBeRenderedOn='deadline', + fps=fps ) comp = compositions_by_id.get(int(item_id)) @@ -102,7 +107,6 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): instances.append(instance) - self.log.debug("instances::{}".format(instances)) return instances def get_expected_files(self, render_instance): diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py b/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py new file mode 100644 index 0000000000..cc7db3141f --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +"""Validate scene settings.""" +import os + +import pyblish.api + +from avalon import aftereffects + +import openpype.hosts.aftereffects.api as api + +stub = aftereffects.stub() + + +class ValidateSceneSettings(pyblish.api.InstancePlugin): + """ + Ensures that Composition Settings (right mouse on comp) are same as + in FTrack on task. + + By default checks only duration - how many frames should be rendered. + Compares: + Frame start - Frame end + 1 from FTrack + against + Duration in Composition Settings. + + If this complains: + Check error message where is discrepancy. + Check FTrack task 'pype' section of task attributes for expected + values. + Check/modify rendered Composition Settings. + + If you know what you are doing run publishing again, uncheck this + validation before Validation phase. + """ + + """ + Dev docu: + Could be configured by 'presets/plugins/aftereffects/publish' + + skip_timelines_check - fill task name for which skip validation of + frameStart + frameEnd + fps + handleStart + handleEnd + skip_resolution_check - fill entity type ('asset') to skip validation + resolutionWidth + resolutionHeight + TODO support in extension is missing for now + + By defaults validates duration (how many frames should be published) + """ + + order = pyblish.api.ValidatorOrder + label = "Validate Scene Settings" + families = ["render.farm"] + hosts = ["aftereffects"] + optional = True + + skip_timelines_check = ["*"] # * >> skip for all + skip_resolution_check = ["*"] + + def process(self, instance): + """Plugin entry point.""" + expected_settings = api.get_asset_settings() + self.log.info("expected_settings::{}".format(expected_settings)) + + # handle case where ftrack uses only two decimal places + # 23.976023976023978 vs. 23.98 + fps = instance.data.get("fps") + if fps: + if isinstance(fps, float): + fps = float( + "{:.2f}".format(fps)) + expected_settings["fps"] = fps + + duration = instance.data.get("frameEndHandle") - \ + instance.data.get("frameStartHandle") + 1 + + current_settings = { + "fps": fps, + "frameStartHandle": instance.data.get("frameStartHandle"), + "frameEndHandle": instance.data.get("frameEndHandle"), + "resolutionWidth": instance.data.get("resolutionWidth"), + "resolutionHeight": instance.data.get("resolutionHeight"), + "duration": duration + } + self.log.info("current_settings:: {}".format(current_settings)) + + invalid_settings = [] + for key, value in expected_settings.items(): + if value != current_settings[key]: + invalid_settings.append( + "{} expected: {} found: {}".format(key, value, + current_settings[key]) + ) + + if ((expected_settings.get("handleStart") + or expected_settings.get("handleEnd")) + and invalid_settings): + msg = "Handles included in calculation. Remove handles in DB " +\ + "or extend frame range in Composition Setting." + invalid_settings[-1]["reason"] = msg + + msg = "Found invalid settings:\n{}".format( + "\n".join(invalid_settings) + ) + assert not invalid_settings, msg + assert os.path.exists(instance.data.get("source")), ( + "Scene file not found (saved under wrong name)" + ) From 0ec066af7ed1a34f40d162a6e40ad9f6aa0f9d88 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 18:12:47 +0200 Subject: [PATCH 10/58] defined constants in ftrack lib --- openpype/modules/ftrack/lib/__init__.py | 13 +++++++++++++ openpype/modules/ftrack/lib/constants.py | 12 ++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 openpype/modules/ftrack/lib/constants.py diff --git a/openpype/modules/ftrack/lib/__init__.py b/openpype/modules/ftrack/lib/__init__.py index 82b6875590..87dadf6480 100644 --- a/openpype/modules/ftrack/lib/__init__.py +++ b/openpype/modules/ftrack/lib/__init__.py @@ -1,3 +1,10 @@ +from .constants import ( + CUST_ATTR_ID_KEY, + CUST_ATTR_AUTO_SYNC, + CUST_ATTR_GROUP, + CUST_ATTR_TOOLS, + CUST_ATTR_APPLICATIONS +) from . settings import ( get_ftrack_url_from_settings, get_ftrack_event_mongo_info @@ -10,6 +17,12 @@ from .ftrack_action_handler import BaseAction, ServerAction, statics_icon __all__ = ( + "CUST_ATTR_ID_KEY", + "CUST_ATTR_AUTO_SYNC", + "CUST_ATTR_GROUP", + "CUST_ATTR_TOOLS", + "CUST_ATTR_APPLICATIONS", + "get_ftrack_url_from_settings", "get_ftrack_event_mongo_info", diff --git a/openpype/modules/ftrack/lib/constants.py b/openpype/modules/ftrack/lib/constants.py new file mode 100644 index 0000000000..73d5112e6d --- /dev/null +++ b/openpype/modules/ftrack/lib/constants.py @@ -0,0 +1,12 @@ +# Group name of custom attributes +CUST_ATTR_GROUP = "openpype" + +# name of Custom attribute that stores mongo_id from avalon db +CUST_ATTR_ID_KEY = "avalon_mongo_id" +# Auto sync of project +CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" + +# Applications custom attribute name +CUST_ATTR_APPLICATIONS = "applications" +# Environment tools custom attribute +CUST_ATTR_TOOLS = "tools_env" From 5fbc62678b168ff1134ccb6e75803ce162e9b428 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 18:13:13 +0200 Subject: [PATCH 11/58] fixed and replaced import of constants --- .../action_create_cust_attrs.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py b/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py index 63025d35b3..8d585dee20 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py @@ -2,9 +2,16 @@ import collections import json import arrow import ftrack_api -from openpype.modules.ftrack.lib import BaseAction, statics_icon +from openpype.modules.ftrack.lib import ( + BaseAction, + statics_icon, + CUST_ATTR_ID_KEY, + CUST_ATTR_GROUP, + CUST_ATTR_TOOLS, + CUST_ATTR_APPLICATIONS +) from openpype.modules.ftrack.lib.avalon_sync import ( - CUST_ATTR_ID_KEY, CUST_ATTR_GROUP, default_custom_attributes_definition + default_custom_attributes_definition ) from openpype.api import get_system_settings from openpype.lib import ApplicationManager @@ -387,7 +394,7 @@ class CustomAttributes(BaseAction): applications_custom_attr_data = { "label": "Applications", - "key": "applications", + "key": CUST_ATTR_APPLICATIONS, "type": "enumerator", "entity_type": "show", "group": CUST_ATTR_GROUP, @@ -411,7 +418,7 @@ class CustomAttributes(BaseAction): tools_custom_attr_data = { "label": "Tools", - "key": "tools_env", + "key": CUST_ATTR_TOOLS, "type": "enumerator", "is_hierarchical": True, "group": CUST_ATTR_GROUP, From 3f63fc7a9b4eb6105f33dd509dd8a4ee155c3f40 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 18:13:27 +0200 Subject: [PATCH 12/58] avalon_sync is using constants defined in constants.py --- openpype/modules/ftrack/lib/avalon_sync.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index 79e1366a0d..5f44181e5f 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -26,6 +26,12 @@ from pymongo import UpdateOne import ftrack_api from openpype.lib import ApplicationManager +from .constants import ( + CUST_ATTR_ID_KEY, + CUST_ATTR_AUTO_SYNC, + CUST_ATTR_GROUP +) + log = Logger.get_logger(__name__) @@ -36,14 +42,6 @@ EntitySchemas = { "config": "openpype:config-2.0" } -# Group name of custom attributes -CUST_ATTR_GROUP = "openpype" - -# name of Custom attribute that stores mongo_id from avalon db -CUST_ATTR_ID_KEY = "avalon_mongo_id" -CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" - - def default_custom_attributes_definition(): json_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), From 10b362937b1ce0887204882e5b9334e74dc7987b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 18:27:42 +0200 Subject: [PATCH 13/58] application manager can be initialized to use different settings --- openpype/lib/applications.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 51c646d494..dc83037378 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -261,14 +261,32 @@ class Application: class ApplicationManager: - def __init__(self): - self.log = PypeLogger().get_logger(self.__class__.__name__) + """Load applications and tools and store them by their full name. + + Args: + system_settings (dict): Preloaded system settings. When passed manager + will always use these values. Gives ability to create manager + using different settings. + """ + def __init__(self, system_settings=None): + self.log = PypeLogger.get_logger(self.__class__.__name__) self.app_groups = {} self.applications = {} self.tool_groups = {} self.tools = {} + self._system_settings = system_settings + + self.refresh() + + def set_system_settings(self, system_settings): + """Ability to change init system settings. + + This will trigger refresh of manager. + """ + self._system_settings = system_settings + self.refresh() def refresh(self): @@ -278,9 +296,12 @@ class ApplicationManager: self.tool_groups.clear() self.tools.clear() - settings = get_system_settings( - clear_metadata=False, exclude_locals=False - ) + if self._system_settings is not None: + settings = copy.deepcopy(self._system_settings) + else: + settings = get_system_settings( + clear_metadata=False, exclude_locals=False + ) app_defs = settings["applications"] for group_name, variant_defs in app_defs.items(): From d746c4009f487ec34ae75c34a7ae9355b77af903 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 18:49:29 +0200 Subject: [PATCH 14/58] added custom_attributes.py to ftrack lib --- openpype/modules/ftrack/lib/__init__.py | 10 +++++ .../modules/ftrack/lib/custom_attributes.py | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 openpype/modules/ftrack/lib/custom_attributes.py diff --git a/openpype/modules/ftrack/lib/__init__.py b/openpype/modules/ftrack/lib/__init__.py index 87dadf6480..bc0c989c02 100644 --- a/openpype/modules/ftrack/lib/__init__.py +++ b/openpype/modules/ftrack/lib/__init__.py @@ -9,6 +9,12 @@ from . settings import ( get_ftrack_url_from_settings, get_ftrack_event_mongo_info ) +from .custm_attributes import ( + default_custom_attributes_definition, + app_definitions_from_app_manager, + tool_definitions_from_app_manager +) + from . import avalon_sync from . import credentials from .ftrack_base_handler import BaseHandler @@ -26,6 +32,10 @@ __all__ = ( "get_ftrack_url_from_settings", "get_ftrack_event_mongo_info", + "default_custom_attributes_definition", + "app_definitions_from_app_manager", + "tool_definitions_from_app_manager", + "avalon_sync", "credentials", diff --git a/openpype/modules/ftrack/lib/custom_attributes.py b/openpype/modules/ftrack/lib/custom_attributes.py new file mode 100644 index 0000000000..18efd7bcae --- /dev/null +++ b/openpype/modules/ftrack/lib/custom_attributes.py @@ -0,0 +1,38 @@ +import os +import json + + +def default_custom_attributes_definition(): + json_file_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "custom_attributes.json" + ) + with open(json_file_path, "r") as json_stream: + data = json.load(json_stream) + return data + + +def app_definitions_from_app_manager(app_manager): + app_definitions = [] + for app_name, app in app_manager.applications.items(): + if app.enabled and app.is_host: + app_definitions.append({ + app_name: app.full_label + }) + + if not app_definitions: + app_definitions.append({"empty": "< Empty >"}) + return app_definitions + + +def tool_definitions_from_app_manager(app_manager): + tools_data = [] + for tool_name, tool in app_manager.tools.items(): + tools_data.append({ + tool_name: tool.label + }) + + # Make sure there is at least one item + if not tools_data: + tools_data.append({"empty": "< Empty >"}) + return tools_data From d9ef2b59abdbeb7147cf21db47592147cb7f0d0f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 18:49:51 +0200 Subject: [PATCH 15/58] create update custom attributes is using functions from lib --- .../action_create_cust_attrs.py | 35 +++++-------------- openpype/modules/ftrack/lib/avalon_sync.py | 9 ----- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py b/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py index 8d585dee20..63605eda5e 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py @@ -5,14 +5,17 @@ import ftrack_api from openpype.modules.ftrack.lib import ( BaseAction, statics_icon, + CUST_ATTR_ID_KEY, CUST_ATTR_GROUP, CUST_ATTR_TOOLS, - CUST_ATTR_APPLICATIONS -) -from openpype.modules.ftrack.lib.avalon_sync import ( - default_custom_attributes_definition + CUST_ATTR_APPLICATIONS, + + default_custom_attributes_definition, + app_definitions_from_app_manager, + tool_definitions_from_app_manager ) + from openpype.api import get_system_settings from openpype.lib import ApplicationManager @@ -377,20 +380,8 @@ class CustomAttributes(BaseAction): exc_info=True ) - def app_defs_from_app_manager(self): - app_definitions = [] - for app_name, app in self.app_manager.applications.items(): - if app.enabled and app.is_host: - app_definitions.append({ - app_name: app.full_label - }) - - if not app_definitions: - app_definitions.append({"empty": "< Empty >"}) - return app_definitions - def applications_attribute(self, event): - apps_data = self.app_defs_from_app_manager() + apps_data = app_definitions_from_app_manager(self.app_manager) applications_custom_attr_data = { "label": "Applications", @@ -406,15 +397,7 @@ class CustomAttributes(BaseAction): self.process_attr_data(applications_custom_attr_data, event) def tools_attribute(self, event): - tools_data = [] - for tool_name, tool in self.app_manager.tools.items(): - tools_data.append({ - tool_name: tool.label - }) - - # Make sure there is at least one item - if not tools_data: - tools_data.append({"empty": "< Empty >"}) + tools_data = tool_definitions_from_app_manager(self.app_manager) tools_custom_attr_data = { "label": "Tools", diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index 5f44181e5f..6e83be8b64 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -42,15 +42,6 @@ EntitySchemas = { "config": "openpype:config-2.0" } -def default_custom_attributes_definition(): - json_file_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "custom_attributes.json" - ) - with open(json_file_path, "r") as json_stream: - data = json.load(json_stream) - return data - def check_regex(name, entity_type, in_schema=None, schema_patterns=None): schema_name = "asset-3.0" From 02238eef7cbbdcce38d6fe0ccc64f44991907f40 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 19:07:53 +0200 Subject: [PATCH 16/58] added `get_openpype_attr` to custom_attributes --- openpype/modules/ftrack/lib/__init__.py | 6 ++-- .../modules/ftrack/lib/custom_attributes.py | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/lib/__init__.py b/openpype/modules/ftrack/lib/__init__.py index bc0c989c02..ce6d5284b6 100644 --- a/openpype/modules/ftrack/lib/__init__.py +++ b/openpype/modules/ftrack/lib/__init__.py @@ -9,10 +9,11 @@ from . settings import ( get_ftrack_url_from_settings, get_ftrack_event_mongo_info ) -from .custm_attributes import ( +from .custom_attributes import ( default_custom_attributes_definition, app_definitions_from_app_manager, - tool_definitions_from_app_manager + tool_definitions_from_app_manager, + get_openpype_attr ) from . import avalon_sync @@ -35,6 +36,7 @@ __all__ = ( "default_custom_attributes_definition", "app_definitions_from_app_manager", "tool_definitions_from_app_manager", + "get_openpype_attr", "avalon_sync", diff --git a/openpype/modules/ftrack/lib/custom_attributes.py b/openpype/modules/ftrack/lib/custom_attributes.py index 18efd7bcae..33eea32baa 100644 --- a/openpype/modules/ftrack/lib/custom_attributes.py +++ b/openpype/modules/ftrack/lib/custom_attributes.py @@ -1,6 +1,8 @@ import os import json +from .constants import CUST_ATTR_GROUP + def default_custom_attributes_definition(): json_file_path = os.path.join( @@ -36,3 +38,36 @@ def tool_definitions_from_app_manager(app_manager): if not tools_data: tools_data.append({"empty": "< Empty >"}) return tools_data + + +def get_openpype_attr(session, split_hierarchical=True, query_keys=None): + custom_attributes = [] + hier_custom_attributes = [] + if not query_keys: + query_keys = [ + "id", + "entity_type", + "object_type_id", + "is_hierarchical", + "default" + ] + # TODO remove deprecated "pype" group from query + cust_attrs_query = ( + "select {}" + " from CustomAttributeConfiguration" + # Kept `pype` for Backwards Compatiblity + " where group.name in (\"pype\", \"{}\")" + ).format(", ".join(query_keys), CUST_ATTR_GROUP) + all_avalon_attr = session.query(cust_attrs_query).all() + for cust_attr in all_avalon_attr: + if split_hierarchical and cust_attr["is_hierarchical"]: + hier_custom_attributes.append(cust_attr) + continue + + custom_attributes.append(cust_attr) + + if split_hierarchical: + # return tuple + return custom_attributes, hier_custom_attributes + + return custom_attributes From eb1b9556673b63d51ef324fb5a13e2caceb07b43 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 19:09:30 +0200 Subject: [PATCH 17/58] replaced usage of get_pype_attr with get_openpype_attr --- .../action_prepare_project.py | 8 ++-- .../event_sync_to_avalon.py | 9 +++-- .../action_clean_hierarchical_attributes.py | 9 +++-- .../action_prepare_project.py | 8 ++-- openpype/modules/ftrack/ftrack_module.py | 4 +- openpype/modules/ftrack/lib/avalon_sync.py | 38 ++----------------- 6 files changed, 24 insertions(+), 52 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py index 8248bf532e..12d687bbf2 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py @@ -2,9 +2,9 @@ import json from openpype.api import ProjectSettings -from openpype.modules.ftrack.lib import ServerAction -from openpype.modules.ftrack.lib.avalon_sync import ( - get_pype_attr, +from openpype.modules.ftrack.lib import ( + ServerAction, + get_openpype_attr, CUST_ATTR_AUTO_SYNC ) @@ -159,7 +159,7 @@ class PrepareProjectServer(ServerAction): for key, entity in project_anatom_settings["attributes"].items(): attribute_values_by_key[key] = entity.value - cust_attrs, hier_cust_attrs = get_pype_attr(self.session, True) + cust_attrs, hier_cust_attrs = get_openpype_attr(self.session, True) for attr in hier_cust_attrs: key = attr["key"] diff --git a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py index 347b227dd3..3bb01798e4 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -18,12 +18,15 @@ from avalon import schema from avalon.api import AvalonMongoDB from openpype.modules.ftrack.lib import ( + get_openpype_attr, + CUST_ATTR_ID_KEY, + CUST_ATTR_AUTO_SYNC, + avalon_sync, + BaseEvent ) from openpype.modules.ftrack.lib.avalon_sync import ( - CUST_ATTR_ID_KEY, - CUST_ATTR_AUTO_SYNC, EntitySchemas ) @@ -125,7 +128,7 @@ class SyncToAvalonEvent(BaseEvent): @property def avalon_cust_attrs(self): if self._avalon_cust_attrs is None: - self._avalon_cust_attrs = avalon_sync.get_pype_attr( + self._avalon_cust_attrs = get_openpype_attr( self.process_session, query_keys=self.cust_attr_query_keys ) return self._avalon_cust_attrs diff --git a/openpype/modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py b/openpype/modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py index c326c56a7c..45cc9adf55 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py +++ b/openpype/modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py @@ -1,7 +1,10 @@ import collections import ftrack_api -from openpype.modules.ftrack.lib import BaseAction, statics_icon -from openpype.modules.ftrack.lib.avalon_sync import get_pype_attr +from openpype.modules.ftrack.lib import ( + BaseAction, + statics_icon, + get_openpype_attr +) class CleanHierarchicalAttrsAction(BaseAction): @@ -52,7 +55,7 @@ class CleanHierarchicalAttrsAction(BaseAction): ) entity_ids_joined = ", ".join(all_entities_ids) - attrs, hier_attrs = get_pype_attr(session) + attrs, hier_attrs = get_openpype_attr(session) for attr in hier_attrs: configuration_key = attr["key"] diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index bd25f995fe..5298c06371 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -4,10 +4,8 @@ from openpype.api import ProjectSettings from openpype.modules.ftrack.lib import ( BaseAction, - statics_icon -) -from openpype.modules.ftrack.lib.avalon_sync import ( - get_pype_attr, + statics_icon, + get_openpype_attr, CUST_ATTR_AUTO_SYNC ) @@ -162,7 +160,7 @@ class PrepareProjectLocal(BaseAction): for key, entity in project_anatom_settings["attributes"].items(): attribute_values_by_key[key] = entity.value - cust_attrs, hier_cust_attrs = get_pype_attr(self.session, True) + cust_attrs, hier_cust_attrs = get_openpype_attr(self.session, True) for attr in hier_cust_attrs: key = attr["key"] diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index d242268048..b057503007 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -150,7 +150,7 @@ class FtrackModule( return import ftrack_api - from openpype.modules.ftrack.lib import avalon_sync + from openpype.modules.ftrack.lib import get_openpype_attr session = self.create_ftrack_session() project_entity = session.query( @@ -166,7 +166,7 @@ class FtrackModule( project_id = project_entity["id"] - cust_attr, hier_attr = avalon_sync.get_pype_attr(session) + cust_attr, hier_attr = get_openpype_attr(session) cust_attr_by_key = {attr["key"]: attr for attr in cust_attr} hier_attrs_by_key = {attr["key"]: attr for attr in hier_attr} for key, value in attributes_changes.items(): diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index 6e83be8b64..cfe1f011e7 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -31,6 +31,7 @@ from .constants import ( CUST_ATTR_AUTO_SYNC, CUST_ATTR_GROUP ) +from .custom_attributes import get_openpype_attr log = Logger.get_logger(__name__) @@ -80,39 +81,6 @@ def join_query_keys(keys): return ",".join(["\"{}\"".format(key) for key in keys]) -def get_pype_attr(session, split_hierarchical=True, query_keys=None): - custom_attributes = [] - hier_custom_attributes = [] - if not query_keys: - query_keys = [ - "id", - "entity_type", - "object_type_id", - "is_hierarchical", - "default" - ] - # TODO remove deprecated "pype" group from query - cust_attrs_query = ( - "select {}" - " from CustomAttributeConfiguration" - # Kept `pype` for Backwards Compatiblity - " where group.name in (\"pype\", \"{}\")" - ).format(", ".join(query_keys), CUST_ATTR_GROUP) - all_avalon_attr = session.query(cust_attrs_query).all() - for cust_attr in all_avalon_attr: - if split_hierarchical and cust_attr["is_hierarchical"]: - hier_custom_attributes.append(cust_attr) - continue - - custom_attributes.append(cust_attr) - - if split_hierarchical: - # return tuple - return custom_attributes, hier_custom_attributes - - return custom_attributes - - def get_python_type_for_custom_attribute(cust_attr, cust_attr_type_name=None): """Python type that should value of custom attribute have. @@ -910,7 +878,7 @@ class SyncEntitiesFactory: def set_cutom_attributes(self): self.log.debug("* Preparing custom attributes") # Get custom attributes and values - custom_attrs, hier_attrs = get_pype_attr( + custom_attrs, hier_attrs = get_openpype_attr( self.session, query_keys=self.cust_attr_query_keys ) ent_types = self.session.query("select id, name from ObjectType").all() @@ -2497,7 +2465,7 @@ class SyncEntitiesFactory: if new_entity_id not in p_chilren: self.entities_dict[parent_id]["children"].append(new_entity_id) - cust_attr, _ = get_pype_attr(self.session) + cust_attr, _ = get_openpype_attr(self.session) for _attr in cust_attr: key = _attr["key"] if key not in av_entity["data"]: From 4996903f23dbc7174270649d88f775c1a1d7a767 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 19:19:41 +0200 Subject: [PATCH 18/58] settings has defined few api exceptions --- openpype/settings/__init__.py | 7 +++++++ openpype/settings/exceptions.py | 11 +++++++++++ openpype/settings/lib.py | 4 ++++ 3 files changed, 22 insertions(+) create mode 100644 openpype/settings/exceptions.py diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index b4187829fc..3755fabeb2 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -1,3 +1,7 @@ +from .exceptions import ( + SaveWarning, + SaveSettingsValidation +) from .lib import ( get_system_settings, get_project_settings, @@ -12,6 +16,9 @@ from .entities import ( __all__ = ( + "SaveWarning", + "SaveSettingsValidation", + "get_system_settings", "get_project_settings", "get_current_project_settings", diff --git a/openpype/settings/exceptions.py b/openpype/settings/exceptions.py new file mode 100644 index 0000000000..86f04c76b9 --- /dev/null +++ b/openpype/settings/exceptions.py @@ -0,0 +1,11 @@ +class SaveSettingsValidation(Exception): + pass + + +class SaveWarning(SaveSettingsValidation): + def __init__(self, warnings): + if isinstance(warnings, str): + warnings = [warnings] + self.warnings = warnings + msg = ", ".join(warnings) + super(SaveWarning, self).__init__(msg) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 3bf2141808..31c7c902cb 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -4,6 +4,10 @@ import functools import logging import platform import copy +from .exceptions import ( + SaveSettingsValidation, + SaveWarning +) from .constants import ( M_OVERRIDEN_KEY, M_ENVIRONMENT_KEY, From 9ac5427aaf4e79939d586e3b320e07fed8ea8660 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 19:19:55 +0200 Subject: [PATCH 19/58] ftrack updates custom attributes on application save --- openpype/modules/ftrack/ftrack_module.py | 84 +++++++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index b057503007..28281786b3 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -1,4 +1,5 @@ import os +import json import collections from abc import ABCMeta, abstractmethod import six @@ -12,6 +13,7 @@ from openpype.modules import ( ILaunchHookPaths, ISettingsChangeListener ) +from openpype.settings import SaveWarning FTRACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -128,10 +130,86 @@ class FtrackModule( if self.tray_module: self.tray_module.changed_user() - def on_system_settings_save(self, *_args, **_kwargs): + def on_system_settings_save(self, old_value, new_value, changes): """Implementation of ISettingsChangeListener interface.""" - # Ignore - return + try: + session = self.create_ftrack_session() + except Exception: + self.log.warning("Couldn't create ftrack session.", exc_info=True) + raise SaveWarning(( + "Couldn't create Ftrack session." + " You may need to update applications" + " and tools in Ftrack custom attributes using defined action." + )) + + from .lib import ( + get_openpype_attr, + CUST_ATTR_APPLICATIONS, + CUST_ATTR_TOOLS, + app_definitions_from_app_manager, + tool_definitions_from_app_manager + ) + from openpype.api import ApplicationManager + query_keys = [ + "id", + "key", + "config" + ] + custom_attributes = get_openpype_attr( + session, + split_hierarchical=False, + query_keys=query_keys + ) + app_attribute = None + tool_attribute = None + for custom_attribute in custom_attributes: + key = custom_attribute["key"] + if key == CUST_ATTR_APPLICATIONS: + app_attribute = custom_attribute + elif key == CUST_ATTR_TOOLS: + tool_attribute = custom_attribute + + app_manager = ApplicationManager(new_value) + missing_attributes = [] + if not app_attribute: + missing_attributes.append(CUST_ATTR_APPLICATIONS) + else: + config = json.loads(app_attribute["config"]) + new_data = app_definitions_from_app_manager(app_manager) + prepared_data = [] + for item in new_data: + for key, label in item.items(): + prepared_data.append({ + "menu": label, + "value": key + }) + + config["data"] = json.dumps(prepared_data) + app_attribute["config"] = json.dumps(config) + + if not tool_attribute: + missing_attributes.append(CUST_ATTR_TOOLS) + else: + config = json.loads(tool_attribute["config"]) + new_data = tool_definitions_from_app_manager(app_manager) + prepared_data = [] + for item in new_data: + for key, label in item.items(): + prepared_data.append({ + "menu": label, + "value": key + }) + config["data"] = json.dumps(prepared_data) + tool_attribute["config"] = json.dumps(config) + + session.commit() + + if missing_attributes: + raise SaveWarning(( + "Couldn't find custom attribute/s ({}) to update." + " You may need to update applications" + " and tools in Ftrack custom attributes using defined action." + ).format(", ".join(missing_attributes))) def on_project_settings_save(self, *_args, **_kwargs): """Implementation of ISettingsChangeListener interface.""" From cbc0fca2e3114b279470361334036224b269d42f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 19:32:56 +0200 Subject: [PATCH 20/58] removed user module with all settings --- openpype/modules/__init__.py | 7 - openpype/modules/ftrack/ftrack_module.py | 7 - openpype/modules/user/__init__.py | 10 -- openpype/modules/user/rest_api.py | 35 ---- openpype/modules/user/user_module.py | 169 ------------------ openpype/modules/user/widget_user.py | 88 --------- .../defaults/system_settings/modules.json | 3 - .../schemas/system_schema/schema_modules.json | 14 -- 8 files changed, 333 deletions(-) delete mode 100644 openpype/modules/user/__init__.py delete mode 100644 openpype/modules/user/rest_api.py delete mode 100644 openpype/modules/user/user_module.py delete mode 100644 openpype/modules/user/widget_user.py diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index d7c6d99fe6..bae48c540b 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -18,10 +18,6 @@ from .webserver import ( WebServerModule, IWebServerRoutes ) -from .user import ( - UserModule, - IUserModule -) from .idle_manager import ( IdleManager, IIdleManager @@ -60,9 +56,6 @@ __all__ = ( "WebServerModule", "IWebServerRoutes", - "UserModule", - "IUserModule", - "IdleManager", "IIdleManager", diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index d242268048..e639e1a634 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -8,7 +8,6 @@ from openpype.modules import ( ITrayModule, IPluginPaths, ITimersManager, - IUserModule, ILaunchHookPaths, ISettingsChangeListener ) @@ -32,7 +31,6 @@ class FtrackModule( ITrayModule, IPluginPaths, ITimersManager, - IUserModule, ILaunchHookPaths, ISettingsChangeListener ): @@ -123,11 +121,6 @@ class FtrackModule( if self.tray_module: self.tray_module.stop_timer_manager() - def on_pype_user_change(self, username): - """Implementation of IUserModule interface.""" - if self.tray_module: - self.tray_module.changed_user() - def on_system_settings_save(self, *_args, **_kwargs): """Implementation of ISettingsChangeListener interface.""" # Ignore diff --git a/openpype/modules/user/__init__.py b/openpype/modules/user/__init__.py deleted file mode 100644 index a97ac0eef6..0000000000 --- a/openpype/modules/user/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .user_module import ( - UserModule, - IUserModule -) - - -__all__ = ( - "UserModule", - "IUserModule" -) diff --git a/openpype/modules/user/rest_api.py b/openpype/modules/user/rest_api.py deleted file mode 100644 index 566425a19b..0000000000 --- a/openpype/modules/user/rest_api.py +++ /dev/null @@ -1,35 +0,0 @@ -import json -from aiohttp.web_response import Response - - -class UserModuleRestApi: - def __init__(self, user_module, server_manager): - self.module = user_module - self.server_manager = server_manager - - self.prefix = "/user" - - self.register() - - def register(self): - self.server_manager.add_route( - "GET", - self.prefix + "/username", - self.get_username - ) - self.server_manager.add_route( - "GET", - self.prefix + "/show_widget", - self.show_user_widget - ) - - async def get_username(self, request): - return Response( - status=200, - body=json.dumps(self.module.cred, indent=4), - content_type="application/json" - ) - - async def show_user_widget(self, request): - self.module.action_show_widget.trigger() - return Response(status=200) diff --git a/openpype/modules/user/user_module.py b/openpype/modules/user/user_module.py deleted file mode 100644 index 7d257f1781..0000000000 --- a/openpype/modules/user/user_module.py +++ /dev/null @@ -1,169 +0,0 @@ -import os -import json -import getpass - -from abc import ABCMeta, abstractmethod - -import six -import appdirs - -from .. import ( - PypeModule, - ITrayModule, - IWebServerRoutes -) - - -@six.add_metaclass(ABCMeta) -class IUserModule: - """Interface for other modules to use user change callbacks.""" - - @abstractmethod - def on_pype_user_change(self, username): - """What should happen on Pype user change.""" - pass - - -class UserModule(PypeModule, ITrayModule, IWebServerRoutes): - cred_folder_path = os.path.normpath( - appdirs.user_data_dir('pype-app', 'pype') - ) - cred_filename = 'user_info.json' - env_name = "OPENPYPE_USERNAME" - - name = "user" - - def initialize(self, modules_settings): - user_settings = modules_settings[self.name] - self.enabled = user_settings["enabled"] - - self.callbacks_on_user_change = [] - self.cred = {} - self.cred_path = os.path.normpath(os.path.join( - self.cred_folder_path, self.cred_filename - )) - - # Tray attributes - self.widget_login = None - self.action_show_widget = None - - self.rest_api_obj = None - - def tray_init(self): - from .widget_user import UserWidget - self.widget_login = UserWidget(self) - - self.load_credentials() - - def register_callback_on_user_change(self, callback): - self.callbacks_on_user_change.append(callback) - - def tray_start(self): - """Store credentials to env and preset them to widget""" - username = "" - if self.cred: - username = self.cred.get("username") or "" - - os.environ[self.env_name] = username - self.widget_login.set_user(username) - - def tray_exit(self): - """Nothing special for User.""" - return - - def get_user(self): - return self.cred.get("username") or getpass.getuser() - - def webserver_initialization(self, server_manager): - """Implementation of IWebServerRoutes interface.""" - from .rest_api import UserModuleRestApi - - self.rest_api_obj = UserModuleRestApi(self, server_manager) - - def connect_with_modules(self, enabled_modules): - for module in enabled_modules: - if isinstance(module, IUserModule): - self.callbacks_on_user_change.append( - module.on_pype_user_change - ) - - # Definition of Tray menu - def tray_menu(self, parent_menu): - from Qt import QtWidgets - """Add menu or action to Tray(or parent)'s menu""" - action = QtWidgets.QAction("Username", parent_menu) - action.triggered.connect(self.show_widget) - parent_menu.addAction(action) - parent_menu.addSeparator() - - self.action_show_widget = action - - def load_credentials(self): - """Get credentials from JSON file """ - credentials = {} - try: - file = open(self.cred_path, "r") - credentials = json.load(file) - file.close() - - self.cred = credentials - username = credentials.get("username") - if username: - self.log.debug("Loaded Username \"{}\"".format(username)) - else: - self.log.debug("Pype Username is not set") - - return credentials - - except FileNotFoundError: - return self.save_credentials(getpass.getuser()) - - except json.decoder.JSONDecodeError: - self.log.warning(( - "File where users credentials should be stored" - " has invalid json format. Loading system username." - )) - return self.save_credentials(getpass.getuser()) - - def change_credentials(self, username): - self.save_credentials(username) - for callback in self.callbacks_on_user_change: - try: - callback(username) - except Exception: - self.log.warning( - "Failed to execute callback \"{}\".".format( - str(callback) - ), - exc_info=True - ) - - def save_credentials(self, username): - """Save credentials to JSON file, env and widget""" - if username is None: - username = "" - - username = str(username).strip() - - self.cred = {"username": username} - os.environ[self.env_name] = username - if self.widget_login: - self.widget_login.set_user(username) - try: - file = open(self.cred_path, "w") - file.write(json.dumps(self.cred)) - file.close() - self.log.debug("Username \"{}\" stored".format(username)) - except Exception: - self.log.error( - "Could not store username to file \"{}\"".format( - self.cred_path - ), - exc_info=True - ) - - return self.cred - - def show_widget(self): - """Show dialog to enter credentials""" - self.widget_login.show() diff --git a/openpype/modules/user/widget_user.py b/openpype/modules/user/widget_user.py deleted file mode 100644 index f8ecadf56b..0000000000 --- a/openpype/modules/user/widget_user.py +++ /dev/null @@ -1,88 +0,0 @@ -from Qt import QtCore, QtGui, QtWidgets -from avalon import style -from openpype import resources - - -class UserWidget(QtWidgets.QWidget): - - MIN_WIDTH = 300 - - def __init__(self, module): - - super(UserWidget, self).__init__() - - self.module = module - - # Style - icon = QtGui.QIcon(resources.pype_icon_filepath()) - self.setWindowIcon(icon) - self.setWindowTitle("Username Settings") - self.setMinimumWidth(self.MIN_WIDTH) - self.setStyleSheet(style.load_stylesheet()) - - self.setWindowFlags( - QtCore.Qt.WindowCloseButtonHint | - QtCore.Qt.WindowMinimizeButtonHint - ) - - self.setLayout(self._main()) - - def show(self, *args, **kwargs): - super().show(*args, **kwargs) - # Move widget to center of active screen on show - screen = QtWidgets.QApplication.desktop().screen() - screen_center = lambda self: ( - screen.rect().center() - self.rect().center() - ) - self.move(screen_center(self)) - - def _main(self): - main_layout = QtWidgets.QVBoxLayout() - - form_layout = QtWidgets.QFormLayout() - form_layout.setContentsMargins(10, 15, 10, 5) - - label_username = QtWidgets.QLabel("Username:") - label_username.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - label_username.setTextFormat(QtCore.Qt.RichText) - - input_username = QtWidgets.QLineEdit() - input_username.setPlaceholderText( - QtCore.QCoreApplication.translate("main", "e.g. John Smith") - ) - - form_layout.addRow(label_username, input_username) - - btn_save = QtWidgets.QPushButton("Save") - btn_save.clicked.connect(self.click_save) - - btn_cancel = QtWidgets.QPushButton("Cancel") - btn_cancel.clicked.connect(self.close) - - btn_group = QtWidgets.QHBoxLayout() - btn_group.addStretch(1) - btn_group.addWidget(btn_save) - btn_group.addWidget(btn_cancel) - - main_layout.addLayout(form_layout) - main_layout.addLayout(btn_group) - - self.input_username = input_username - - return main_layout - - def set_user(self, username): - self.input_username.setText(username) - - def click_save(self): - # all what should happen - validations and saving into appsdir - username = self.input_username.text() - self.module.change_credentials(username) - self._close_widget() - - def closeEvent(self, event): - event.ignore() - self._close_widget() - - def _close_widget(self): - self.hide() diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index b3065058a1..6e4b493116 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -161,9 +161,6 @@ "log_viewer": { "enabled": true }, - "user": { - "enabled": true - }, "standalonepublish_tool": { "enabled": true } diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index a30cafd0c2..878958b12d 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -154,20 +154,6 @@ } ] }, - { - "type": "dict", - "key": "user", - "label": "User setting", - "collapsible": true, - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - } - ] - }, { "type": "dict", "key": "standalonepublish_tool", From 72ac25e60e949ad3ab8185983f2d011dc9703c5d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 20 Apr 2021 20:18:31 +0200 Subject: [PATCH 21/58] SyncServer GUI - rework of header and menus Reiplemented QHeaderView Refactored accessing models Still wip --- openpype/modules/sync_server/tray/lib.py | 96 ++++ openpype/modules/sync_server/tray/models.py | 102 ++-- openpype/modules/sync_server/tray/widgets.py | 505 ++++++++++++------- 3 files changed, 488 insertions(+), 215 deletions(-) diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 051567ed6c..41b0eb43f9 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -1,5 +1,7 @@ from Qt import QtCore import attr +import abc +import six from openpype.lib import PypeLogger @@ -24,6 +26,100 @@ FailedRole = QtCore.Qt.UserRole + 8 HeaderNameRole = QtCore.Qt.UserRole + 10 +@six.add_metaclass(abc.ABCMeta) +class AbstractColumnFilter: + + def __init__(self, column_name, dbcon=None): + self.column_name = column_name + self.dbcon = dbcon + self._search_variants = [] + + def search_variants(self): + """ + Returns all flavors of search available for this column, + """ + return self._search_variants + + @abc.abstractmethod + def values(self): + """ + Returns dict of available values for filter {'label':'value'} + """ + pass + + @abc.abstractmethod + def prepare_match_part(self, values): + """ + Prepares format valid for $match part from 'values + + Args: + values (dict): {'label': 'value'} + Returns: + (dict): {'COLUMN_NAME': {'$in': ['val1', 'val2']}} + """ + pass + + +class PredefinedSetFilter(AbstractColumnFilter): + + def __init__(self, column_name, values): + super().__init__(column_name) + self._search_variants = ['text', 'checkbox'] + self._values = values + + def values(self): + return {k: v for k, v in self._values.items()} + + def prepare_match_part(self, values): + return {'$in': list(values.keys())} + + +class RegexTextFilter(AbstractColumnFilter): + + def __init__(self, column_name): + super().__init__(column_name) + self._search_variants = ['text'] + + def values(self): + return {} + + def prepare_match_part(self, values): + """ values = {'text1 text2': 'text1 text2'} """ + if not values: + return {} + + regex_strs = set() + text = list(values.keys())[0] # only single key always expected + for word in text.split(): + regex_strs.add('.*{}.*'.format(word)) + + return {"$regex": "|".join(regex_strs), + "$options": 'i'} + + +class MultiSelectFilter(AbstractColumnFilter): + + def __init__(self, column_name, values=None, dbcon=None): + super().__init__(column_name) + self._values = values + self.dbcon = dbcon + self._search_variants = ['checkbox'] + + def values(self): + if self._values: + return {k: v for k, v in self._values.items()} + + recs = self.dbcon.find({'type': self.column_name}, {"name": 1, + "_id": -1}) + values = {} + for item in recs: + values[item["name"]] = item["name"] + return dict(sorted(values.items(), key=lambda it: it[1])) + + def prepare_match_part(self, values): + return {'$in': list(values.keys())} + + @attr.s class FilterDefinition: type = attr.ib() diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 444422c56a..4b70fbae15 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -6,6 +6,7 @@ from Qt import QtCore from Qt.QtCore import Qt from avalon.tools.delegates import pretty_timestamp +from avalon.vendor import qtawesome from openpype.lib import PypeLogger @@ -67,18 +68,24 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): return len(self._header) def headerData(self, section, orientation, role): + name = self.COLUMN_LABELS[section][0] if role == Qt.DisplayRole: if orientation == Qt.Horizontal: - name = self.COLUMN_LABELS[section][0] - txt = "" - if name in self.column_filtering.keys(): - txt = "(F)" - return self.COLUMN_LABELS[section][1] + txt # return label + return self.COLUMN_LABELS[section][1] + + if role == Qt.DecorationRole: + if name in self.column_filtering.keys(): + return qtawesome.icon("fa.filter", color="white") + if self.COLUMN_FILTERS.get(name): + return qtawesome.icon("fa.filter", color="gray") if role == lib.HeaderNameRole: if orientation == Qt.Horizontal: return self.COLUMN_LABELS[section][0] # return name + def get_column(self, index): + return self.COLUMN_LABELS[index] + def get_header_index(self, value): """ Returns index of 'value' in headers @@ -199,9 +206,28 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): Args: word_filter (str): string inputted by user """ - self.word_filter = word_filter + self._word_filter = word_filter self.refresh() + def get_column_filter(self, index): + """ + Returns filter object for column 'index + + Args: + index(int): index of column in header + + Returns: + (AbstractColumnFilter) + """ + column_name = self._header[index] + + filter_rec = self.COLUMN_FILTERS.get(column_name) + if filter_rec: + filter_rec.dbcon = self.dbcon # up-to-date db connection + + return filter_rec + + def get_column_filter_values(self, index): """ Returns list of available values for filtering in the column @@ -215,21 +241,11 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): menu for some columns ('subset') might be 'value' and 'label' same """ - column_name = self._header[index] - - filter_def = self.COLUMN_FILTERS.get(column_name) - if not filter_def: + filter_rec = self.get_column_filter(index) + if not filter_rec: return {} - if filter_def['type'] == 'predefined_set': - return dict(filter_def['values']) - elif filter_def['type'] == 'available_values': - recs = self.dbcon.find({'type': column_name}, {"name": 1, - "_id": -1}) - values = {} - for item in recs: - values[item["name"]] = item["name"] - return dict(sorted(values.items(), key=lambda item: item[1])) + return filter_rec.values() def set_project(self, project): """ @@ -313,11 +329,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): ] COLUMN_FILTERS = { - 'status': {'type': 'predefined_set', - 'values': {k: v for k, v in lib.STATUS.items()}}, - 'subset': {'type': 'available_values'}, - 'asset': {'type': 'available_values'}, - 'representation': {'type': 'available_values'} + 'status': lib.PredefinedSetFilter('status', lib.STATUS), + 'subset': lib.RegexTextFilter('subset'), + 'asset': lib.RegexTextFilter('asset'), + 'representation': lib.MultiSelectFilter('representation') } refresh_started = QtCore.Signal() @@ -356,9 +371,11 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self._project = project self._rec_loaded = 0 self._total_records = 0 # how many documents query actually found - self.word_filter = None + self._word_filter = None self._column_filtering = {} + self._word_filter = None + self._initialized = False if not self._project or self._project == lib.DUMMY_PROJECT: return @@ -383,6 +400,19 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self.timer.timeout.connect(self.tick) self.timer.start(self.REFRESH_SEC) + def get_filters(self): + """ + Returns all available filter editors per column_name keys. + """ + filters = {} + for column_name, _ in self.COLUMN_LABELS: + filter_rec = self.COLUMN_FILTERS.get(column_name) + if filter_rec: + filter_rec.dbcon = self.dbcon + filters[column_name] = filter_rec + + return filters + def data(self, index, role): item = self._data[index.row()] @@ -666,8 +696,12 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self._column_filtering : {'status': {'$in': [1, 2, 3]}} """ filtering = {} - for key, dict_value in checked_values.items(): - filtering[key] = {'$in': list(dict_value.keys())} + for column_name, dict_value in checked_values.items(): + column_f = self.COLUMN_FILTERS.get(column_name) + if not column_f: + continue + column_f.dbcon = self.dbcon + filtering[column_name] = column_f.prepare_match_part(dict_value) self._column_filtering = filtering @@ -690,18 +724,18 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): 'files.sites.name': {'$all': [self.local_site, self.remote_site]} } - if not self.word_filter: + if not self._word_filter: return base_match else: - regex_str = '.*{}.*'.format(self.word_filter) + regex_str = '.*{}.*'.format(self._word_filter) base_match['$or'] = [ {'context.subset': {'$regex': regex_str, '$options': 'i'}}, {'context.asset': {'$regex': regex_str, '$options': 'i'}}, {'context.representation': {'$regex': regex_str, '$options': 'i'}}] - if ObjectId.is_valid(self.word_filter): - base_match['$or'] = [{'_id': ObjectId(self.word_filter)}] + if ObjectId.is_valid(self._word_filter): + base_match['$or'] = [{'_id': ObjectId(self._word_filter)}] return base_match @@ -848,7 +882,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): self._project = project self._rec_loaded = 0 self._total_records = 0 # how many documents query actually found - self.word_filter = None + self._word_filter = None self._id = _id self._initialized = False @@ -1114,13 +1148,13 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): Returns: (dict) """ - if not self.word_filter: + if not self._word_filter: return { "type": "representation", "_id": self._id } else: - regex_str = '.*{}.*'.format(self.word_filter) + regex_str = '.*{}.*'.format(self._word_filter) return { "type": "representation", "_id": self._id, diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 5719d13716..f9f904cd03 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -15,6 +15,7 @@ from openpype.api import get_local_site_id from openpype.lib import PypeLogger from avalon.tools.delegates import pretty_timestamp +from avalon.vendor import qtawesome from openpype.modules.sync_server.tray.models import ( SyncRepresentationSummaryModel, @@ -142,15 +143,15 @@ class SyncRepresentationWidget(QtWidgets.QWidget): message_generated = QtCore.Signal(str) default_widths = ( - ("asset", 220), - ("subset", 190), - ("version", 55), - ("representation", 95), + ("asset", 200), + ("subset", 170), + ("version", 60), + ("representation", 135), ("local_site", 170), ("remote_site", 170), ("files_count", 50), ("files_size", 60), - ("priority", 50), + ("priority", 70), ("status", 110) ) @@ -183,8 +184,6 @@ class SyncRepresentationWidget(QtWidgets.QWidget): QtWidgets.QAbstractItemView.SelectRows) self.table_view.horizontalHeader().setSortIndicator( -1, Qt.AscendingOrder) - self.table_view.setSortingEnabled(True) - self.table_view.horizontalHeader().setSortIndicatorShown(True) self.table_view.setAlternatingRowColors(True) self.table_view.verticalHeader().hide() @@ -196,10 +195,6 @@ class SyncRepresentationWidget(QtWidgets.QWidget): delegate = ImageDelegate(self) self.table_view.setItemDelegateForColumn(column, delegate) - for column_name, width in self.default_widths: - idx = model.get_header_index(column_name) - self.table_view.setColumnWidth(idx, width) - layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addLayout(top_bar_layout) @@ -213,23 +208,26 @@ class SyncRepresentationWidget(QtWidgets.QWidget): model.refresh_started.connect(self._save_scrollbar) model.refresh_finished.connect(self._set_scrollbar) - self.table_view.model().modelReset.connect(self._set_selection) + model.modelReset.connect(self._set_selection) + + self.model = model self.selection_model = self.table_view.selectionModel() self.selection_model.selectionChanged.connect(self._selection_changed) - self.checked_values = {} + self.horizontal_header = HorizontalHeader(self) + self.table_view.setHorizontalHeader(self.horizontal_header) + # self.table_view.setSortingEnabled(True) + # self.table_view.horizontalHeader().setSortIndicatorShown(True) - self.horizontal_header = self.table_view.horizontalHeader() - self.horizontal_header.setContextMenuPolicy( - QtCore.Qt.CustomContextMenu) - self.horizontal_header.customContextMenuRequested.connect( - self._on_section_clicked) + for column_name, width in self.default_widths: + idx = model.get_header_index(column_name) + self.table_view.setColumnWidth(idx, width) def _selection_changed(self, _new_selection): index = self.selection_model.currentIndex() self._selected_id = \ - self.table_view.model().data(index, Qt.UserRole) + self.model.data(index, Qt.UserRole) def _set_selection(self): """ @@ -238,7 +236,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): Keep selection during model refresh. """ if self._selected_id: - index = self.table_view.model().get_index(self._selected_id) + index = self.model.get_index(self._selected_id) if index and index.isValid(): mode = QtCore.QItemSelectionModel.Select | \ QtCore.QItemSelectionModel.Rows @@ -250,141 +248,11 @@ class SyncRepresentationWidget(QtWidgets.QWidget): """ Opens representation dialog with all files after doubleclick """ - _id = self.table_view.model().data(index, Qt.UserRole) + _id = self.model.data(index, Qt.UserRole) detail_window = SyncServerDetailWindow( - self.sync_server, _id, self.table_view.model().project) + self.sync_server, _id, self.model.project) detail_window.exec() - def _on_section_clicked(self, point): - - logical_index = self.horizontal_header.logicalIndexAt(point) - - model = self.table_view.model() - column_name = model.headerData(logical_index, - Qt.Horizontal, lib.HeaderNameRole) - items_dict = model.get_column_filter_values(logical_index) - - if not items_dict: - return - - menu = QtWidgets.QMenu(self) - - # text filtering only if labels same as values, not if codes are used - if list(items_dict.keys())[0] == list(items_dict.values())[0]: - self.line_edit = QtWidgets.QLineEdit(self) - self.line_edit.setPlaceholderText("Type and enter...") - action_le = QtWidgets.QWidgetAction(menu) - action_le.setDefaultWidget(self.line_edit) - self.line_edit.returnPressed.connect( - partial(self._apply_text_filter, column_name, items_dict)) - menu.addAction(action_le) - menu.addSeparator() - - action_all = QtWidgets.QAction("All", self) - state_checked = 2 - # action_all.triggered.connect(partial(self._apply_filter, column_name, - # items_dict, state_checked)) - action_all.triggered.connect(partial(self._reset_filter, column_name)) - menu.addAction(action_all) - - action_none = QtWidgets.QAction("Unselect all", self) - state_unchecked = 0 - action_none.triggered.connect(partial(self._apply_filter, column_name, - items_dict, state_unchecked)) - menu.addAction(action_none) - menu.addSeparator() - - # nothing explicitly >> ALL implicitly >> first time - if self.checked_values.get(column_name) is None: - checked_keys = items_dict.keys() - else: - checked_keys = self.checked_values[column_name] - - for value, label in items_dict.items(): - checkbox = QtWidgets.QCheckBox(str(label), menu) - if value in checked_keys: - checkbox.setChecked(True) - - action = QtWidgets.QWidgetAction(menu) - action.setDefaultWidget(checkbox) - - checkbox.stateChanged.connect(partial(self._apply_filter, - column_name, {value: label})) - menu.addAction(action) - - self.menu = menu - self.menu_items_dict = items_dict # all available items - self.menu.exec_(QtGui.QCursor.pos()) - - def _reset_filter(self, column_name): - """ - Remove whole column from filter >> not in $match at all (faster) - """ - if self.checked_values.get(column_name) is not None: - self.checked_values.pop(column_name) - self._refresh_model_and_menu(column_name, True, True) - - def _apply_filter(self, column_name, values, state): - """ - Sets 'values' to specific 'state' (checked/unchecked), - sends to model. - """ - self._update_checked_values(column_name, values, state) - self._refresh_model_and_menu(column_name, True, False) - - def _apply_text_filter(self, column_name, items): - """ - Resets all checkboxes, prefers inserted text. - """ - self._update_checked_values(column_name, items, 0) # reset other - text_item = {self.line_edit.text(): self.line_edit.text()} - self._update_checked_values(column_name, text_item, 2) - self._refresh_model_and_menu(column_name, True, True) - - def _refresh_model_and_menu(self, column_name, model=True, menu=True): - """ - Refresh model and its content and possibly menu for big changes. - """ - if model: - self.table_view.model().set_column_filtering(self.checked_values) - self.table_view.model().refresh() - if menu: - self._menu_refresh(column_name) - - def _menu_refresh(self, column_name): - """ - Reset boxes after big change - word filtering or reset - """ - for action in self.menu.actions(): - if not isinstance(action, QtWidgets.QWidgetAction): - continue - - widget = action.defaultWidget() - if not isinstance(widget, QtWidgets.QCheckBox): - continue - - if not self.checked_values.get(column_name) or \ - widget.text() in self.checked_values[column_name].values(): - widget.setChecked(True) - else: - widget.setChecked(False) - - def _update_checked_values(self, column_name, values, state): - """ - Modify dictionary of set values in columns for filtering. - - Modifies 'self.checked_values' - """ - checked = self.checked_values.get(column_name, self.menu_items_dict) - set_items = dict(values.items()) # prevent dictionary change during iter - for value, label in set_items.items(): - if state == 2: # checked - checked[value] = label - elif state == 0 and checked.get(value): - checked.pop(value) - - self.checked_values[column_name] = checked - def _on_context_menu(self, point): """ Shows menu with loader actions on Right-click. @@ -393,7 +261,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): if not point_index.isValid(): return - self.item = self.table_view.model()._data[point_index.row()] + self.item = self.model._data[point_index.row()] self.representation_id = self.item._id log.debug("menu representation _id:: {}". format(self.representation_id)) @@ -410,7 +278,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): for site, progress in {local_site: local_progress, remote_site: remote_progress}.items(): - project = self.table_view.model().project + project = self.model.project provider = self.sync_server.get_provider_for_site(project, site) if provider == 'local_drive': @@ -476,10 +344,10 @@ class SyncRepresentationWidget(QtWidgets.QWidget): if to_run: to_run(**to_run_kwargs) - self.table_view.model().refresh() + self.model.refresh() def _pause(self): - self.sync_server.pause_representation(self.table_view.model().project, + self.sync_server.pause_representation(self.model.project, self.representation_id, self.site_name) self.site_name = None @@ -487,7 +355,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): def _unpause(self): self.sync_server.unpause_representation( - self.table_view.model().project, + self.model.project, self.representation_id, self.site_name) self.site_name = None @@ -497,7 +365,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): # temporary here for testing, will be removed TODO def _add_site(self): log.info(self.representation_id) - project_name = self.table_view.model().project + project_name = self.model.project local_site_name = get_local_site_id() try: self.sync_server.add_site( @@ -525,15 +393,15 @@ class SyncRepresentationWidget(QtWidgets.QWidget): try: local_site = get_local_site_id() self.sync_server.remove_site( - self.table_view.model().project, + self.model.project, self.representation_id, local_site, True) self.message_generated.emit("Site {} removed".format(local_site)) except ValueError as exp: self.message_generated.emit("Error {}".format(str(exp))) - self.table_view.model().refresh( - load_records=self.table_view.model()._rec_loaded) + self.model.refresh( + load_records=self.model._rec_loaded) def _reset_local_site(self): """ @@ -541,11 +409,11 @@ class SyncRepresentationWidget(QtWidgets.QWidget): redo of upload/download """ self.sync_server.reset_provider_for_file( - self.table_view.model().project, + self.model.project, self.representation_id, 'local') - self.table_view.model().refresh( - load_records=self.table_view.model()._rec_loaded) + self.model.refresh( + load_records=self.model._rec_loaded) def _reset_remote_site(self): """ @@ -553,18 +421,18 @@ class SyncRepresentationWidget(QtWidgets.QWidget): redo of upload/download """ self.sync_server.reset_provider_for_file( - self.table_view.model().project, + self.model.project, self.representation_id, 'remote') - self.table_view.model().refresh( - load_records=self.table_view.model()._rec_loaded) + self.model.refresh( + load_records=self.model._rec_loaded) def _open_in_explorer(self, site): if not self.item: return fpath = self.item.path - project = self.table_view.model().project + project = self.model.project fpath = self.sync_server.get_local_file_path(project, site, fpath) @@ -647,11 +515,11 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): self.table_view.setAlternatingRowColors(True) self.table_view.verticalHeader().hide() - column = self.table_view.model().get_header_index("local_site") + column = model.get_header_index("local_site") delegate = ImageDelegate(self) self.table_view.setItemDelegateForColumn(column, delegate) - column = self.table_view.model().get_header_index("remote_site") + column = model.get_header_index("remote_site") delegate = ImageDelegate(self) self.table_view.setItemDelegateForColumn(column, delegate) @@ -671,14 +539,15 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): model.refresh_started.connect(self._save_scrollbar) model.refresh_finished.connect(self._set_scrollbar) - self.table_view.model().modelReset.connect(self._set_selection) + model.modelReset.connect(self._set_selection) + self.model = model self.selection_model = self.table_view.selectionModel() self.selection_model.selectionChanged.connect(self._selection_changed) def _selection_changed(self): index = self.selection_model.currentIndex() - self._selected_id = self.table_view.model().data(index, Qt.UserRole) + self._selected_id = self.model.data(index, Qt.UserRole) def _set_selection(self): """ @@ -687,7 +556,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): Keep selection during model refresh. """ if self._selected_id: - index = self.table_view.model().get_index(self._selected_id) + index = self.model.get_index(self._selected_id) if index and index.isValid(): mode = QtCore.QItemSelectionModel.Select | \ QtCore.QItemSelectionModel.Rows @@ -715,7 +584,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): if not point_index.isValid(): return - self.item = self.table_view.model()._data[point_index.row()] + self.item = self.model._data[point_index.row()] menu = QtWidgets.QMenu() #menu.setStyleSheet(style.load_stylesheet()) @@ -729,7 +598,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): for site, progress in {local_site: local_progress, remote_site: remote_progress}.items(): - project = self.table_view.model().project + project = self.model.project provider = self.sync_server.get_provider_for_site(project, site) if provider == 'local_drive': @@ -776,12 +645,12 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): redo of upload/download """ self.sync_server.reset_provider_for_file( - self.table_view.model().project, + self.model.project, self.representation_id, 'local', self.item._id) - self.table_view.model().refresh( - load_records=self.table_view.model()._rec_loaded) + self.model.refresh( + load_records=self.model._rec_loaded) def _reset_remote_site(self): """ @@ -789,12 +658,12 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): redo of upload/download """ self.sync_server.reset_provider_for_file( - self.table_view.model().project, + self.model.project, self.representation_id, 'remote', self.item._id) - self.table_view.model().refresh( - load_records=self.table_view.model()._rec_loaded) + self.model.refresh( + load_records=self.model._rec_loaded) def _open_in_explorer(self, site): if not self.item: @@ -957,3 +826,277 @@ class SyncRepresentationErrorWindow(QtWidgets.QDialog): self.setLayout(body_layout) self.setWindowTitle("Sync Representation Error Detail") + + +class HorizontalHeader(QtWidgets.QHeaderView): + + def __init__(self, parent=None): + super(HorizontalHeader, self).__init__(QtCore.Qt.Horizontal, parent) + self._parent = parent + self.checked_values = {} + + self.setSectionsMovable(True) + self.setSectionsClickable(True) + self.setHighlightSections(True) + + self.menu_items_dict = {} + self.menu = None + self.header_cells = [] + self.filter_buttons = {} + + self.init_layout() + + self.filter_icon = qtawesome.icon("fa.filter", color="gray") + self.filter_set_icon = qtawesome.icon("fa.filter", color="white") + + self._resetting = False + + self.sectionResized.connect(self.handleSectionResized) + self.sectionMoved.connect(self.handleSectionMoved) + #self.sectionPressed.connect(self.model.sort) + + + @property + def model(self): + """Keep model synchronized with parent widget""" + return self._parent.model + + def init_layout(self): + for i in range(self.count()): + cell_content = QtWidgets.QWidget(self) + column_name, column_label = self.model.get_column(i) + + layout = QtWidgets.QHBoxLayout() + layout.setContentsMargins(5, 5, 5, 0) + layout.setAlignment(Qt.AlignVCenter) + layout.addWidget(QtWidgets.QLabel(column_label)) + + filter_rec = self.model.get_filters().get(column_name) + if filter_rec: + icon = self.filter_icon + button = QtWidgets.QPushButton(icon, "") + layout.addWidget(button) + + # button.setMenu(menu) + button.setFixedSize(24, 24) + # button.setAlignment(Qt.AlignRight) + button.setStyleSheet("QPushButton::menu-indicator{width:0px;}" + "QPushButton{border: none}") + button.clicked.connect(partial(self._get_menu, + column_name, i)) + button.setFlat(True) + self.filter_buttons[column_name] = button + + cell_content.setLayout(layout) + + self.header_cells.append(cell_content) + + def showEvent(self, event): + if not self.header_cells: + self.init_layout() + + for i in range(len(self.header_cells)): + cell_content = self.header_cells[i] + cell_content.setGeometry(self.sectionViewportPosition(i), 0, + self.sectionSize(i)-1, self.height()) + + cell_content.show() + + if len(self.model.get_filters()) > self.count(): + for i in range(self.count(), len(self.header_cells)): + self.header_cells[i].deleteLater() + + super(HorizontalHeader, self).showEvent(event) + + def _set_filter_icon(self, column_name): + button = self.filter_buttons.get(column_name) + if button: + if self.checked_values.get(column_name): + button.setIcon(self.filter_set_icon) + else: + button.setIcon(self.filter_icon) + + def _reset_filter(self, column_name): + """ + Remove whole column from filter >> not in $match at all (faster) + """ + self._resetting = True # mark changes to consume them + if self.checked_values.get(column_name) is not None: + self.checked_values.pop(column_name) + self._set_filter_icon(column_name) + self._filter_and_refresh_model_and_menu(column_name, True, True) + self._resetting = False + + def _apply_filter(self, column_name, values, state): + """ + Sets 'values' to specific 'state' (checked/unchecked), + sends to model. + """ + if self._resetting: # event triggered by _resetting, skip it + return + + self._update_checked_values(column_name, values, state) + self._set_filter_icon(column_name) + self._filter_and_refresh_model_and_menu(column_name, True, False) + + def _apply_text_filter(self, column_name, items): + """ + Resets all checkboxes, prefers inserted text. + """ + self._update_checked_values(column_name, items, 0) # reset other + if self.checked_values.get(column_name) is not None or \ + self.line_edit.text() == '': + self.checked_values.pop(column_name) # reset during typing + + text_item = {self.line_edit.text(): self.line_edit.text()} + if self.line_edit.text(): + self._update_checked_values(column_name, text_item, 2) + self._set_filter_icon(column_name) + self._filter_and_refresh_model_and_menu(column_name, True, True) + + def _filter_and_refresh_model_and_menu(self, column_name, + model=True, menu=True): + """ + Refresh model and its content and possibly menu for big changes. + """ + if model: + self.model.set_column_filtering(self.checked_values) + self.model.refresh() + if menu: + self._menu_refresh(column_name) + + def _get_menu(self, column_name, index): + """Prepares content of menu for 'column_name'""" + menu = QtWidgets.QMenu(self) + filter_rec = self.model.get_filters()[column_name] + self.menu_items_dict[column_name] = filter_rec.values() + self.line_edit = None + + # text filtering only if labels same as values, not if codes are used + if 'text' in filter_rec.search_variants(): + self.line_edit = QtWidgets.QLineEdit(self) + self.line_edit.setSizePolicy( + QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum) + txt = "Type..." + if self.checked_values.get(column_name): + txt = list(self.checked_values.get(column_name).keys())[0] + self.line_edit.setPlaceholderText(txt) + + action_le = QtWidgets.QWidgetAction(menu) + action_le.setDefaultWidget(self.line_edit) + self.line_edit.textChanged.connect( + partial(self._apply_text_filter, column_name, + filter_rec.values())) + menu.addAction(action_le) + menu.addSeparator() + + if 'checkbox' in filter_rec.search_variants(): + action_all = QtWidgets.QAction("All", self) + action_all.triggered.connect(partial(self._reset_filter, + column_name)) + menu.addAction(action_all) + + action_none = QtWidgets.QAction("Unselect all", self) + state_unchecked = 0 + action_none.triggered.connect(partial(self._apply_filter, + column_name, + filter_rec.values(), + state_unchecked)) + menu.addAction(action_none) + menu.addSeparator() + + # nothing explicitly >> ALL implicitly >> first time + if self.checked_values.get(column_name) is None: + checked_keys = self.menu_items_dict[column_name].keys() + else: + checked_keys = self.checked_values[column_name] + + for value, label in self.menu_items_dict[column_name].items(): + checkbox = QtWidgets.QCheckBox(str(label), menu) + + # temp + checkbox.setStyleSheet("QCheckBox{spacing: 5px;" + "padding:5px 5px 5px 5px;}") + if value in checked_keys: + checkbox.setChecked(True) + + action = QtWidgets.QWidgetAction(menu) + action.setDefaultWidget(checkbox) + + checkbox.stateChanged.connect(partial(self._apply_filter, + column_name, {value: label})) + menu.addAction(action) + + self.menu = menu + + self._show_menu(index, menu) + + def _show_menu(self, index, menu): + """Shows 'menu' under header column of 'index'""" + global_pos_point = self.mapToGlobal( + QtCore.QPoint(self.sectionViewportPosition(index), 0)) + menu.setMinimumWidth(self.sectionSize(index)) + menu.setMinimumHeight(self.height()) + menu.exec_(QtCore.QPoint(global_pos_point.x(), + global_pos_point.y() + self.height())) + + def _menu_refresh(self, column_name): + """ + Reset boxes after big change - word filtering or reset + """ + for action in self.menu.actions(): + if not isinstance(action, QtWidgets.QWidgetAction): + continue + + widget = action.defaultWidget() + if not isinstance(widget, QtWidgets.QCheckBox): + continue + + if not self.checked_values.get(column_name) or \ + widget.text() in self.checked_values[column_name].values(): + widget.setChecked(True) + else: + widget.setChecked(False) + + def _update_checked_values(self, column_name, values, state): + """ + Modify dictionary of set values in columns for filtering. + + Modifies 'self.checked_values' + """ + checked = self.checked_values.get(column_name, + dict(self.menu_items_dict[column_name])) + set_items = dict(values.items()) # prevent dict change during loop + for value, label in set_items.items(): + if state == 2 and label: # checked + checked[value] = label + elif state == 0 and checked.get(value): + checked.pop(value) + + self.checked_values[column_name] = checked + + def handleSectionResized(self, i): + if not self.header_cells: + self.init_layout() + for i in range(self.count()): + j = self.visualIndex(i) + logical = self.logicalIndex(j) + self.header_cells[i].setGeometry( + self.sectionViewportPosition(logical), 0, + self.sectionSize(logical) - 1, self.height()) + + def handleSectionMoved(self, i, oldVisualIndex, newVisualIndex): + if not self.header_cells: + self.init_layout() + for i in range(min(oldVisualIndex, newVisualIndex), self.count()): + logical = self.logicalIndex(i) + self.header_cells[i].setGeometry( + self.ectionViewportPosition(logical), 0, + self.sectionSize(logical) - 2, self.height()) + + def fixComboPositions(self): + for i in range(self.count()): + self.header_cells[i].setGeometry( + self.sectionViewportPosition(i), 0, + self.sectionSize(i) - 2, self.height()) From a8319ad336d6013d011cfbfbdf2932e4f267918e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 10:25:57 +0200 Subject: [PATCH 22/58] local settings can store open pype username --- .../settings/local_settings/general_widget.py | 22 ++++++++++++++----- .../tools/settings/local_settings/window.py | 13 +++++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/openpype/tools/settings/local_settings/general_widget.py b/openpype/tools/settings/local_settings/general_widget.py index e820d8ab8b..f2147e626a 100644 --- a/openpype/tools/settings/local_settings/general_widget.py +++ b/openpype/tools/settings/local_settings/general_widget.py @@ -5,16 +5,28 @@ class LocalGeneralWidgets(QtWidgets.QWidget): def __init__(self, parent): super(LocalGeneralWidgets, self).__init__(parent) + username_input = QtWidgets.QLineEdit(self) + + layout = QtWidgets.QFormLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + layout.addRow("OpenPype Username", username_input) + + self.username_input = username_input def update_local_settings(self, value): - return - - # RETURNING EARLY TO HIDE WIDGET WITHOUT CONTENT + username = "" + if value: + username = value.get("username", username) + self.username_input.setText(username) def settings_value(self): # Add changed # If these have changed then output = {} - # TEMPORARILY EMPTY AS THERE IS NOTHING TO PUT HERE - + username = self.username_input.text() + if username: + output["username"] = username + # Do not return output yet since we don't have mechanism to save or + # load these data through api calls return output diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index a12a2289b5..b6ca56d348 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -80,7 +80,6 @@ class LocalSettingsWidget(QtWidgets.QWidget): general_widget = LocalGeneralWidgets(general_content) general_layout.addWidget(general_widget) - general_expand_widget.hide() self.main_layout.addWidget(general_expand_widget) @@ -127,9 +126,9 @@ class LocalSettingsWidget(QtWidgets.QWidget): self.system_settings.reset() self.project_settings.reset() - # self.general_widget.update_local_settings( - # value.get(LOCAL_GENERAL_KEY) - # ) + self.general_widget.update_local_settings( + value.get(LOCAL_GENERAL_KEY) + ) self.app_widget.update_local_settings( value.get(LOCAL_APPS_KEY) ) @@ -139,9 +138,9 @@ class LocalSettingsWidget(QtWidgets.QWidget): def settings_value(self): output = {} - # general_value = self.general_widget.settings_value() - # if general_value: - # output[LOCAL_GENERAL_KEY] = general_value + general_value = self.general_widget.settings_value() + if general_value: + output[LOCAL_GENERAL_KEY] = general_value app_value = self.app_widget.settings_value() if app_value: From 7aa222327bffcfca727ed201c4ece88cbec6d103 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 10:26:18 +0200 Subject: [PATCH 23/58] get_local_settings is part of `openpype.settings` --- openpype/settings/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index b4187829fc..c8dd64a41c 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -3,7 +3,8 @@ from .lib import ( get_project_settings, get_current_project_settings, get_anatomy_settings, - get_environments + get_environments, + get_local_settings ) from .entities import ( SystemSettings, @@ -17,6 +18,7 @@ __all__ = ( "get_current_project_settings", "get_anatomy_settings", "get_environments", + "get_local_settings", "SystemSettings", "ProjectSettings" From fee9e9bd563caf5d9e5e99ebde1e04b234efb53e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 10:26:48 +0200 Subject: [PATCH 24/58] implemented function to get openpype username --- openpype/lib/local_settings.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/openpype/lib/local_settings.py b/openpype/lib/local_settings.py index 56bdd047c9..67845c77cf 100644 --- a/openpype/lib/local_settings.py +++ b/openpype/lib/local_settings.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- """Package to deal with saving and retrieving user specific settings.""" import os +import json +import getpass +import platform from datetime import datetime from abc import ABCMeta, abstractmethod -import json # TODO Use pype igniter logic instead of using duplicated code # disable lru cache in Python 2 @@ -24,11 +26,11 @@ try: except ImportError: import ConfigParser as configparser -import platform - import six import appdirs +from openpype.settings import get_local_settings + from .import validate_mongo_connection _PLACEHOLDER = object() @@ -538,3 +540,25 @@ def change_openpype_mongo_url(new_mongo_url): if existing_value is not None: registry.delete_item(key) registry.set_item(key, new_mongo_url) + + +def get_openpype_username(): + """OpenPype username used for templates and publishing. + + May be different than machine's username. + + Always returns "OPENPYPE_USERNAME" environment if is set then tries local + settings and last option is to use `getpass.getuser()` which returns + machine username. + """ + username = os.environ.get("OPENPYPE_USERNAME") + if not username: + local_settings = get_local_settings() + username = ( + local_settings + .get("general", {}) + .get("username") + ) + if not username: + username = getpass.getuser() + return username From 7d9e665e2d7c08f649c5c72eeef325f993e9c8f3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 10:28:05 +0200 Subject: [PATCH 25/58] get_openpype_username replaced usage of OPENPYPE_USERNAME environment in code --- openpype/lib/__init__.py | 34 ++++++++++--------- openpype/lib/applications.py | 3 +- .../publish/collect_current_pype_user.py | 6 ++-- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index f46c81bf7a..895d11601f 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -79,6 +79,16 @@ from .avalon_context import ( change_timer_to_current_context ) +from .local_settings import ( + IniSettingRegistry, + JSONSettingRegistry, + OpenPypeSecureRegistry, + OpenPypeSettingsRegistry, + get_local_site_id, + change_openpype_mongo_url, + get_openpype_username +) + from .applications import ( ApplicationLaunchFailed, ApplictionExecutableNotFound, @@ -112,15 +122,6 @@ from .plugin_tools import ( should_decompress ) -from .local_settings import ( - IniSettingRegistry, - JSONSettingRegistry, - OpenPypeSecureRegistry, - OpenPypeSettingsRegistry, - get_local_site_id, - change_openpype_mongo_url -) - from .path_tools import ( version_up, get_version_from_path, @@ -179,6 +180,14 @@ __all__ = [ "change_timer_to_current_context", + "IniSettingRegistry", + "JSONSettingRegistry", + "OpenPypeSecureRegistry", + "OpenPypeSettingsRegistry", + "get_local_site_id", + "change_openpype_mongo_url", + "get_openpype_username", + "ApplicationLaunchFailed", "ApplictionExecutableNotFound", "ApplicationNotFound", @@ -224,13 +233,6 @@ __all__ = [ "validate_mongo_connection", "OpenPypeMongoConnection", - "IniSettingRegistry", - "JSONSettingRegistry", - "OpenPypeSecureRegistry", - "OpenPypeSettingsRegistry", - "get_local_site_id", - "change_openpype_mongo_url", - "timeit", "is_overlapping_otio_ranges", diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 51c646d494..a0b5569b02 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -25,6 +25,7 @@ from . import ( PypeLogger, Anatomy ) +from .local_settings import get_openpype_username from .avalon_context import ( get_workdir_data, get_workdir_with_workdir_data @@ -1224,7 +1225,7 @@ def _prepare_last_workfile(data, workdir): file_template = anatomy.templates["work"]["file"] workdir_data.update({ "version": 1, - "user": os.environ.get("OPENPYPE_USERNAME") or getpass.getuser(), + "user": get_openpype_username(), "ext": extensions[0] }) diff --git a/openpype/plugins/publish/collect_current_pype_user.py b/openpype/plugins/publish/collect_current_pype_user.py index de4e950d56..003c779836 100644 --- a/openpype/plugins/publish/collect_current_pype_user.py +++ b/openpype/plugins/publish/collect_current_pype_user.py @@ -1,6 +1,7 @@ import os import getpass import pyblish.api +from openpype.lib import get_openpype_username class CollectCurrentUserPype(pyblish.api.ContextPlugin): @@ -11,9 +12,6 @@ class CollectCurrentUserPype(pyblish.api.ContextPlugin): label = "Collect Pype User" def process(self, context): - user = os.getenv("OPENPYPE_USERNAME", "").strip() - if not user: - user = context.data.get("user", getpass.getuser()) - + user = get_openpype_username() context.data["user"] = user self.log.debug("Colected user \"{}\"".format(user)) From e5737db9032c5db26d08559186d090bbf31910b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 10:28:23 +0200 Subject: [PATCH 26/58] render jobs do not care about OPENPYPE_USERNAME --- .../deadline/plugins/publish/submit_aftereffects_deadline.py | 1 - .../modules/deadline/plugins/publish/submit_harmony_deadline.py | 1 - .../modules/deadline/plugins/publish/submit_maya_deadline.py | 1 - 3 files changed, 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py index 38a6b9b246..69159fda1a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py @@ -64,7 +64,6 @@ class AfterEffectsSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline "AVALON_ASSET", "AVALON_TASK", "AVALON_APP_NAME", - "OPENPYPE_USERNAME", "OPENPYPE_DEV", "OPENPYPE_LOG_NO_COLORS" ] diff --git a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py index ba1ffdcf30..37041a84b1 100644 --- a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py @@ -273,7 +273,6 @@ class HarmonySubmitDeadline( "AVALON_ASSET", "AVALON_TASK", "AVALON_APP_NAME", - "OPENPYPE_USERNAME", "OPENPYPE_DEV", "OPENPYPE_LOG_NO_COLORS" ] diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 3aea837bb1..0e92fb38bb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -441,7 +441,6 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "AVALON_ASSET", "AVALON_TASK", "AVALON_APP_NAME", - "OPENPYPE_USERNAME", "OPENPYPE_DEV", "OPENPYPE_LOG_NO_COLORS" ] From f593f3330bc3e4f5483169c982bf29ef2bd4c32f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 10:38:08 +0200 Subject: [PATCH 27/58] added machine user as placeholder --- openpype/tools/settings/local_settings/general_widget.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/settings/local_settings/general_widget.py b/openpype/tools/settings/local_settings/general_widget.py index f2147e626a..78bc53fdd2 100644 --- a/openpype/tools/settings/local_settings/general_widget.py +++ b/openpype/tools/settings/local_settings/general_widget.py @@ -1,3 +1,5 @@ +import getpass + from Qt import QtWidgets @@ -6,6 +8,7 @@ class LocalGeneralWidgets(QtWidgets.QWidget): super(LocalGeneralWidgets, self).__init__(parent) username_input = QtWidgets.QLineEdit(self) + username_input.setPlaceholderText(getpass.getuser()) layout = QtWidgets.QFormLayout(self) layout.setContentsMargins(0, 0, 0, 0) From cc3f483aec706303e68faaa4d7b28a22d8f04b2b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 11:14:39 +0200 Subject: [PATCH 28/58] removed SaveSettingsValidation from settings init --- openpype/settings/__init__.py | 4 +--- openpype/settings/exceptions.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index 3755fabeb2..78a287f07e 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -1,6 +1,5 @@ from .exceptions import ( - SaveWarning, - SaveSettingsValidation + SaveWarning ) from .lib import ( get_system_settings, @@ -17,7 +16,6 @@ from .entities import ( __all__ = ( "SaveWarning", - "SaveSettingsValidation", "get_system_settings", "get_project_settings", diff --git a/openpype/settings/exceptions.py b/openpype/settings/exceptions.py index 86f04c76b9..758a778794 100644 --- a/openpype/settings/exceptions.py +++ b/openpype/settings/exceptions.py @@ -7,5 +7,5 @@ class SaveWarning(SaveSettingsValidation): if isinstance(warnings, str): warnings = [warnings] self.warnings = warnings - msg = ", ".join(warnings) + msg = " | ".join(warnings) super(SaveWarning, self).__init__(msg) From 00c55d0990b25303932ef1453acb2f3435ae37e4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 11:14:57 +0200 Subject: [PATCH 29/58] save warning are raised afterwards --- openpype/settings/lib.py | 42 +++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 31c7c902cb..dd3f79b5b3 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -5,7 +5,6 @@ import logging import platform import copy from .exceptions import ( - SaveSettingsValidation, SaveWarning ) from .constants import ( @@ -118,11 +117,18 @@ def save_studio_settings(data): changes = calculate_changes(old_data, new_data) modules_manager = ModulesManager(_system_settings=new_data) + + warnings = [] for module in modules_manager.get_enabled_modules(): if isinstance(module, ISettingsChangeListener): - module.on_system_settings_save(old_data, new_data, changes) + try: + module.on_system_settings_save(old_data, new_data, changes) + except SaveWarning as exc: + warnings.extend(exc.warnings) - return _SETTINGS_HANDLER.save_studio_settings(data) + _SETTINGS_HANDLER.save_studio_settings(data) + if warnings: + raise SaveWarning(warnings) @require_handler @@ -159,13 +165,20 @@ def save_project_settings(project_name, overrides): changes = calculate_changes(old_data, new_data) modules_manager = ModulesManager() + warnings = [] for module in modules_manager.get_enabled_modules(): if isinstance(module, ISettingsChangeListener): - module.on_project_settings_save( - old_data, new_data, project_name, changes - ) + try: + module.on_project_settings_save( + old_data, new_data, project_name, changes + ) + except SaveWarning as exc: + warnings.extend(exc.warnings) - return _SETTINGS_HANDLER.save_project_settings(project_name, overrides) + _SETTINGS_HANDLER.save_project_settings(project_name, overrides) + + if warnings: + raise SaveWarning(warnings) @require_handler @@ -202,13 +215,20 @@ def save_project_anatomy(project_name, anatomy_data): changes = calculate_changes(old_data, new_data) modules_manager = ModulesManager() + warnings = [] for module in modules_manager.get_enabled_modules(): if isinstance(module, ISettingsChangeListener): - module.on_project_anatomy_save( - old_data, new_data, changes, project_name - ) + try: + module.on_project_anatomy_save( + old_data, new_data, changes, project_name + ) + except SaveWarning as exc: + warnings.extend(exc.warnings) - return _SETTINGS_HANDLER.save_project_anatomy(project_name, anatomy_data) + _SETTINGS_HANDLER.save_project_anatomy(project_name, anatomy_data) + + if warnings: + raise SaveWarning(warnings) @require_handler From 32336eeb63392c397661d2837d62ff48c8819882 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 11:27:01 +0200 Subject: [PATCH 30/58] added use_python_2 to unreal's defaults --- openpype/settings/defaults/system_settings/applications.json | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 2355f39aa1..56d63ecf09 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1165,6 +1165,7 @@ }, "variants": { "4-26": { + "use_python_2": false, "executables": { "windows": [], "darwin": [], From d8f84a750d1b65676b228c5db2bbb44d16ae4705 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 11:41:08 +0200 Subject: [PATCH 31/58] ftrack module raise SaveWarning exception if crashes during changing values --- openpype/modules/ftrack/ftrack_module.py | 62 +++++++++++++++++++++--- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index 28281786b3..46403bbc26 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -230,30 +230,45 @@ class FtrackModule( import ftrack_api from openpype.modules.ftrack.lib import get_openpype_attr - session = self.create_ftrack_session() + try: + session = self.create_ftrack_session() + except Exception: + self.log.warning("Couldn't create ftrack session.", exc_info=True) + raise SaveWarning(( + "Couldn't create Ftrack session." + " You may need to update applications" + " and tools in Ftrack custom attributes using defined action." + )) + project_entity = session.query( "Project where full_name is \"{}\"".format(project_name) ).first() if not project_entity: - self.log.warning(( - "Ftrack project with names \"{}\" was not found." + msg = ( + "Ftrack project with names \"{}\" was not found in Ftrack." " Skipping settings attributes change callback." - )) - return + ).format(project_name) + self.log.warning(msg) + raise SaveWarning(msg) project_id = project_entity["id"] cust_attr, hier_attr = get_openpype_attr(session) cust_attr_by_key = {attr["key"]: attr for attr in cust_attr} hier_attrs_by_key = {attr["key"]: attr for attr in hier_attr} + + failed = {} + missing = {} for key, value in attributes_changes.items(): configuration = hier_attrs_by_key.get(key) if not configuration: configuration = cust_attr_by_key.get(key) if not configuration: + missing[key] = value continue + # TODO add add permissions check # TODO add value validations # - value type and list items entity_key = collections.OrderedDict() @@ -267,10 +282,43 @@ class FtrackModule( "value", ftrack_api.symbol.NOT_SET, value - ) ) - session.commit() + try: + session.commit() + self.log.debug( + "Changed project custom attribute \"{}\" to \"{}\"".format( + key, value + ) + ) + except Exception: + self.log.warning( + "Failed to set \"{}\" to \"{}\"".format(key, value), + exc_info=True + ) + session.rollback() + failed[key] = value + + if not failed and not missing: + return + + error_msg = ( + "Values were not updated on Ftrack which may cause issues." + ) + if missing: + error_msg += " Missing Custom attributes on Ftrack: {}.".format( + ", ".join([ + '"{}"'.format(key) + for key in missing.keys() + ]) + ) + if failed: + joined_failed = ", ".join([ + '"{}": "{}"'.format(key, value) + for key, value in failed.items() + ]) + error_msg += " Failed to set: {}".format(joined_failed) + raise SaveWarning(error_msg) def create_ftrack_session(self, **session_kwargs): import ftrack_api From c5ac2f1f3c361ee44ca5063abc35259a910b3cbc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 11:45:27 +0200 Subject: [PATCH 32/58] AE fix max instead of min 0 is minimum value, not preferred --- openpype/hosts/aftereffects/api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/api/__init__.py b/openpype/hosts/aftereffects/api/__init__.py index 7ad10cde25..99636e8dda 100644 --- a/openpype/hosts/aftereffects/api/__init__.py +++ b/openpype/hosts/aftereffects/api/__init__.py @@ -98,7 +98,7 @@ def get_asset_settings(): handle_end = asset_data.get("handleEnd") resolution_width = asset_data.get("resolutionWidth") resolution_height = asset_data.get("resolutionHeight") - duration = frame_end + handle_end - min(frame_start - handle_start, 0) + duration = frame_end + handle_end - max(frame_start - handle_start, 0) entity_type = asset_data.get("entityType") scene_data = { From ad64ef3ff95e089c9526aacdeb2f4d86437627e5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 11:53:28 +0200 Subject: [PATCH 33/58] added some docstrings --- openpype/settings/lib.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index dd3f79b5b3..c4ed9453f1 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -104,8 +104,14 @@ def save_studio_settings(data): For saving of data cares registered Settings handler. + Warning messages are not logged as module raising them should log it within + it's logger. + Args: data(dict): Overrides data with metadata defying studio overrides. + + Raises: + SaveWarning: If any module raises the exception. """ # Notify Pype modules from openpype.modules import ModulesManager, ISettingsChangeListener @@ -140,10 +146,16 @@ def save_project_settings(project_name, overrides): For saving of data cares registered Settings handler. + Warning messages are not logged as module raising them should log it within + it's logger. + Args: project_name (str): Project name for which overrides are passed. Default project's value is None. overrides(dict): Overrides data with metadata defying studio overrides. + + Raises: + SaveWarning: If any module raises the exception. """ # Notify Pype modules from openpype.modules import ModulesManager, ISettingsChangeListener @@ -190,10 +202,16 @@ def save_project_anatomy(project_name, anatomy_data): For saving of data cares registered Settings handler. + Warning messages are not logged as module raising them should log it within + it's logger. + Args: project_name (str): Project name for which overrides are passed. Default project's value is None. overrides(dict): Overrides data with metadata defying studio overrides. + + Raises: + SaveWarning: If any module raises the exception. """ # Notify Pype modules from openpype.modules import ModulesManager, ISettingsChangeListener From 55dd8f7005785fef10ba7585fa30e2a30af15aed Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 11:53:54 +0200 Subject: [PATCH 34/58] settings gui catch SaveWarning --- .../tools/settings/settings/widgets/categories.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/widgets/categories.py b/openpype/tools/settings/settings/widgets/categories.py index 9d286485a3..d782aaf54f 100644 --- a/openpype/tools/settings/settings/widgets/categories.py +++ b/openpype/tools/settings/settings/widgets/categories.py @@ -27,7 +27,7 @@ from openpype.settings.entities import ( SchemaError ) -from openpype.settings.lib import get_system_settings +from openpype.settings import SaveWarning from .widgets import ProjectListWidget from . import lib @@ -272,6 +272,15 @@ class SettingsCategoryWidget(QtWidgets.QWidget): # not required. self.reset() + except SaveWarning as exc: + msg = "Settings were saved but few issues happened.\n\n" + msg += "\n".join(exc.warnings) + + dialog = QtWidgets.QMessageBox(self) + dialog.setText(msg) + dialog.setIcon(QtWidgets.QMessageBox.Warning) + dialog.exec_() + except Exception as exc: formatted_traceback = traceback.format_exception(*sys.exc_info()) dialog = QtWidgets.QMessageBox(self) From d9424cdf39ec0657919390a57858c117e3125c26 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 12:08:21 +0200 Subject: [PATCH 35/58] replcaed spaces with new line char --- openpype/modules/ftrack/ftrack_module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index 46403bbc26..c7ce7d6d07 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -306,7 +306,7 @@ class FtrackModule( "Values were not updated on Ftrack which may cause issues." ) if missing: - error_msg += " Missing Custom attributes on Ftrack: {}.".format( + error_msg += "\nMissing Custom attributes on Ftrack: {}.".format( ", ".join([ '"{}"'.format(key) for key in missing.keys() @@ -317,7 +317,7 @@ class FtrackModule( '"{}": "{}"'.format(key, value) for key, value in failed.items() ]) - error_msg += " Failed to set: {}".format(joined_failed) + error_msg += "\nFailed to set: {}".format(joined_failed) raise SaveWarning(error_msg) def create_ftrack_session(self, **session_kwargs): From b01d541c59ebb9358aaa40855f4507fbd49ff03a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 12:08:36 +0200 Subject: [PATCH 36/58] fixed save warning catch --- .../tools/settings/settings/widgets/categories.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/widgets/categories.py b/openpype/tools/settings/settings/widgets/categories.py index d782aaf54f..be31a063fe 100644 --- a/openpype/tools/settings/settings/widgets/categories.py +++ b/openpype/tools/settings/settings/widgets/categories.py @@ -273,14 +273,21 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.reset() except SaveWarning as exc: - msg = "Settings were saved but few issues happened.\n\n" - msg += "\n".join(exc.warnings) + warnings = [ + "Settings were saved but few issues happened." + ] + for item in exc.warnings: + warnings.append(item.replace("\n", "
")) + + msg = "

".join(warnings) dialog = QtWidgets.QMessageBox(self) dialog.setText(msg) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.exec_() + self.reset() + except Exception as exc: formatted_traceback = traceback.format_exception(*sys.exc_info()) dialog = QtWidgets.QMessageBox(self) From 4358aace7ef8a8a0f6e5909aa48078405849f1b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 12:09:11 +0200 Subject: [PATCH 37/58] ProjectSettings will try to store project settings if anatomy crashes --- openpype/settings/entities/root_entities.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index eed3d47f46..05c2b61700 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -23,6 +23,7 @@ from openpype.settings.constants import ( PROJECT_ANATOMY_KEY, KEY_REGEX ) +from openpype.settings.exceptions import SaveWarning from openpype.settings.lib import ( DEFAULTS_DIR, @@ -724,8 +725,19 @@ class ProjectSettings(RootEntity): project_settings = settings_value.get(PROJECT_SETTINGS_KEY) or {} project_anatomy = settings_value.get(PROJECT_ANATOMY_KEY) or {} - save_project_settings(self.project_name, project_settings) - save_project_anatomy(self.project_name, project_anatomy) + warnings = [] + try: + save_project_settings(self.project_name, project_settings) + except SaveWarning as exc: + warnings.extend(exc.warnings) + + try: + save_project_anatomy(self.project_name, project_anatomy) + except SaveWarning as exc: + warnings.extend(exc.warnings) + + if warnings: + raise SaveWarning(warnings) def _validate_defaults_to_save(self, value): """Valiations of default values before save.""" From 4ba8eb7610e3b95d352e8430222f65bf9b2f6d71 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 12:09:33 +0200 Subject: [PATCH 38/58] defined max length of exc info marks --- openpype/lib/log.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/lib/log.py b/openpype/lib/log.py index 9745279e28..39b6c67080 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -123,6 +123,8 @@ class PypeFormatter(logging.Formatter): if record.exc_info is not None: line_len = len(str(record.exc_info[1])) + if line_len > 30: + line_len = 30 out = "{}\n{}\n{}\n{}\n{}".format( out, line_len * "=", From 1418d9734ac5029ccc68ebb328a69b918bae295b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 12:15:19 +0200 Subject: [PATCH 39/58] added more messages --- openpype/modules/ftrack/ftrack_module.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index c7ce7d6d07..54e92af42c 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -246,8 +246,8 @@ class FtrackModule( if not project_entity: msg = ( - "Ftrack project with names \"{}\" was not found in Ftrack." - " Skipping settings attributes change callback." + "Ftrack project with name \"{}\" was not found in Ftrack." + " Can't push attribute changes." ).format(project_name) self.log.warning(msg) raise SaveWarning(msg) @@ -265,6 +265,9 @@ class FtrackModule( if not configuration: configuration = cust_attr_by_key.get(key) if not configuration: + self.log.warning( + "Custom attribute \"{}\" was not found.".format(key) + ) missing[key] = value continue @@ -304,6 +307,8 @@ class FtrackModule( error_msg = ( "Values were not updated on Ftrack which may cause issues." + " Try to update OpenPype custom attributes and resave" + " project settings." ) if missing: error_msg += "\nMissing Custom attributes on Ftrack: {}.".format( From bfb0874f0399a2e67f8cb389ddea1404329cd53a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 13:35:04 +0200 Subject: [PATCH 40/58] renamed SaveWarning to SaveWarningExc --- openpype/modules/ftrack/ftrack_module.py | 12 +++++------ openpype/settings/__init__.py | 4 ++-- openpype/settings/entities/root_entities.py | 8 ++++---- openpype/settings/exceptions.py | 4 ++-- openpype/settings/lib.py | 20 +++++++++---------- .../settings/settings/widgets/categories.py | 4 ++-- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index 54e92af42c..60eed5c941 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -13,7 +13,7 @@ from openpype.modules import ( ILaunchHookPaths, ISettingsChangeListener ) -from openpype.settings import SaveWarning +from openpype.settings import SaveWarningExc FTRACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -136,7 +136,7 @@ class FtrackModule( session = self.create_ftrack_session() except Exception: self.log.warning("Couldn't create ftrack session.", exc_info=True) - raise SaveWarning(( + raise SaveWarningExc(( "Couldn't create Ftrack session." " You may need to update applications" " and tools in Ftrack custom attributes using defined action." @@ -205,7 +205,7 @@ class FtrackModule( session.commit() if missing_attributes: - raise SaveWarning(( + raise SaveWarningExc(( "Couldn't find custom attribute/s ({}) to update." " You may need to update applications" " and tools in Ftrack custom attributes using defined action." @@ -234,7 +234,7 @@ class FtrackModule( session = self.create_ftrack_session() except Exception: self.log.warning("Couldn't create ftrack session.", exc_info=True) - raise SaveWarning(( + raise SaveWarningExc(( "Couldn't create Ftrack session." " You may need to update applications" " and tools in Ftrack custom attributes using defined action." @@ -250,7 +250,7 @@ class FtrackModule( " Can't push attribute changes." ).format(project_name) self.log.warning(msg) - raise SaveWarning(msg) + raise SaveWarningExc(msg) project_id = project_entity["id"] @@ -323,7 +323,7 @@ class FtrackModule( for key, value in failed.items() ]) error_msg += "\nFailed to set: {}".format(joined_failed) - raise SaveWarning(error_msg) + raise SaveWarningExc(error_msg) def create_ftrack_session(self, **session_kwargs): import ftrack_api diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index 78a287f07e..e65c89e603 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -1,5 +1,5 @@ from .exceptions import ( - SaveWarning + SaveWarningExc ) from .lib import ( get_system_settings, @@ -15,7 +15,7 @@ from .entities import ( __all__ = ( - "SaveWarning", + "SaveWarningExc", "get_system_settings", "get_project_settings", diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 05c2b61700..b89473d9fb 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -23,7 +23,7 @@ from openpype.settings.constants import ( PROJECT_ANATOMY_KEY, KEY_REGEX ) -from openpype.settings.exceptions import SaveWarning +from openpype.settings.exceptions import SaveWarningExc from openpype.settings.lib import ( DEFAULTS_DIR, @@ -728,16 +728,16 @@ class ProjectSettings(RootEntity): warnings = [] try: save_project_settings(self.project_name, project_settings) - except SaveWarning as exc: + except SaveWarningExc as exc: warnings.extend(exc.warnings) try: save_project_anatomy(self.project_name, project_anatomy) - except SaveWarning as exc: + except SaveWarningExc as exc: warnings.extend(exc.warnings) if warnings: - raise SaveWarning(warnings) + raise SaveWarningExc(warnings) def _validate_defaults_to_save(self, value): """Valiations of default values before save.""" diff --git a/openpype/settings/exceptions.py b/openpype/settings/exceptions.py index 758a778794..a06138eeaf 100644 --- a/openpype/settings/exceptions.py +++ b/openpype/settings/exceptions.py @@ -2,10 +2,10 @@ class SaveSettingsValidation(Exception): pass -class SaveWarning(SaveSettingsValidation): +class SaveWarningExc(SaveSettingsValidation): def __init__(self, warnings): if isinstance(warnings, str): warnings = [warnings] self.warnings = warnings msg = " | ".join(warnings) - super(SaveWarning, self).__init__(msg) + super(SaveWarningExc, self).__init__(msg) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index c4ed9453f1..9c05c8e86c 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -5,7 +5,7 @@ import logging import platform import copy from .exceptions import ( - SaveWarning + SaveWarningExc ) from .constants import ( M_OVERRIDEN_KEY, @@ -111,7 +111,7 @@ def save_studio_settings(data): data(dict): Overrides data with metadata defying studio overrides. Raises: - SaveWarning: If any module raises the exception. + SaveWarningExc: If any module raises the exception. """ # Notify Pype modules from openpype.modules import ModulesManager, ISettingsChangeListener @@ -129,12 +129,12 @@ def save_studio_settings(data): if isinstance(module, ISettingsChangeListener): try: module.on_system_settings_save(old_data, new_data, changes) - except SaveWarning as exc: + except SaveWarningExc as exc: warnings.extend(exc.warnings) _SETTINGS_HANDLER.save_studio_settings(data) if warnings: - raise SaveWarning(warnings) + raise SaveWarningExc(warnings) @require_handler @@ -155,7 +155,7 @@ def save_project_settings(project_name, overrides): overrides(dict): Overrides data with metadata defying studio overrides. Raises: - SaveWarning: If any module raises the exception. + SaveWarningExc: If any module raises the exception. """ # Notify Pype modules from openpype.modules import ModulesManager, ISettingsChangeListener @@ -184,13 +184,13 @@ def save_project_settings(project_name, overrides): module.on_project_settings_save( old_data, new_data, project_name, changes ) - except SaveWarning as exc: + except SaveWarningExc as exc: warnings.extend(exc.warnings) _SETTINGS_HANDLER.save_project_settings(project_name, overrides) if warnings: - raise SaveWarning(warnings) + raise SaveWarningExc(warnings) @require_handler @@ -211,7 +211,7 @@ def save_project_anatomy(project_name, anatomy_data): overrides(dict): Overrides data with metadata defying studio overrides. Raises: - SaveWarning: If any module raises the exception. + SaveWarningExc: If any module raises the exception. """ # Notify Pype modules from openpype.modules import ModulesManager, ISettingsChangeListener @@ -240,13 +240,13 @@ def save_project_anatomy(project_name, anatomy_data): module.on_project_anatomy_save( old_data, new_data, changes, project_name ) - except SaveWarning as exc: + except SaveWarningExc as exc: warnings.extend(exc.warnings) _SETTINGS_HANDLER.save_project_anatomy(project_name, anatomy_data) if warnings: - raise SaveWarning(warnings) + raise SaveWarningExc(warnings) @require_handler diff --git a/openpype/tools/settings/settings/widgets/categories.py b/openpype/tools/settings/settings/widgets/categories.py index be31a063fe..e4832c989a 100644 --- a/openpype/tools/settings/settings/widgets/categories.py +++ b/openpype/tools/settings/settings/widgets/categories.py @@ -27,7 +27,7 @@ from openpype.settings.entities import ( SchemaError ) -from openpype.settings import SaveWarning +from openpype.settings import SaveWarningExc from .widgets import ProjectListWidget from . import lib @@ -272,7 +272,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): # not required. self.reset() - except SaveWarning as exc: + except SaveWarningExc as exc: warnings = [ "Settings were saved but few issues happened." ] From e918acea48755bab9b4744cfe6ecb8dd8e67f08c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 15:13:33 +0200 Subject: [PATCH 41/58] SyncServer GUI - working version for Summary list --- openpype/modules/sync_server/tray/models.py | 14 +- openpype/modules/sync_server/tray/widgets.py | 172 ++++++++++--------- 2 files changed, 95 insertions(+), 91 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 4b70fbae15..266a9289ca 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -64,20 +64,20 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): def rowCount(self, _index): return len(self._data) - def columnCount(self, _index): + def columnCount(self, _index=None): return len(self._header) - def headerData(self, section, orientation, role): + def headerData(self, section, orientation, role=Qt.DisplayRole): name = self.COLUMN_LABELS[section][0] if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self.COLUMN_LABELS[section][1] - if role == Qt.DecorationRole: - if name in self.column_filtering.keys(): - return qtawesome.icon("fa.filter", color="white") - if self.COLUMN_FILTERS.get(name): - return qtawesome.icon("fa.filter", color="gray") + # if role == Qt.DecorationRole: + # if name in self.column_filtering.keys(): + # return qtawesome.icon("fa.filter", color="white") + # if self.COLUMN_FILTERS.get(name): + # return qtawesome.icon("fa.filter", color="gray") if role == lib.HeaderNameRole: if orientation == Qt.Horizontal: diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index f9f904cd03..25abc73a70 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -42,6 +42,8 @@ class SyncProjectListWidget(ProjectListWidget): self.local_site = None self.icons = {} + self.layout().setContentsMargins(0, 0, 0, 0) + def validate_context_change(self): return True @@ -143,12 +145,12 @@ class SyncRepresentationWidget(QtWidgets.QWidget): message_generated = QtCore.Signal(str) default_widths = ( - ("asset", 200), + ("asset", 190), ("subset", 170), ("version", 60), - ("representation", 135), - ("local_site", 170), - ("remote_site", 170), + ("representation", 145), + ("local_site", 160), + ("remote_site", 160), ("files_count", 50), ("files_size", 60), ("priority", 70), @@ -215,10 +217,10 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.selection_model = self.table_view.selectionModel() self.selection_model.selectionChanged.connect(self._selection_changed) - self.horizontal_header = HorizontalHeader(self) - self.table_view.setHorizontalHeader(self.horizontal_header) - # self.table_view.setSortingEnabled(True) - # self.table_view.horizontalHeader().setSortIndicatorShown(True) + 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) @@ -828,6 +830,21 @@ class SyncRepresentationErrorWindow(QtWidgets.QDialog): self.setWindowTitle("Sync Representation Error Detail") +class TransparentWidget(QtWidgets.QWidget): + clicked = QtCore.Signal(str) + + def __init__(self, column_name, *args, **kwargs): + super(TransparentWidget, self).__init__(*args, **kwargs) + self.column_name = column_name + # self.setStyleSheet("background: red;") + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.clicked.emit(self.column_name) + + super(TransparentWidget, self).mouseReleaseEvent(event) + + class HorizontalHeader(QtWidgets.QHeaderView): def __init__(self, parent=None): @@ -835,26 +852,23 @@ class HorizontalHeader(QtWidgets.QHeaderView): self._parent = parent self.checked_values = {} - self.setSectionsMovable(True) + self.setModel(self._parent.model) + self.setSectionsClickable(True) - self.setHighlightSections(True) self.menu_items_dict = {} self.menu = None self.header_cells = [] self.filter_buttons = {} - self.init_layout() - self.filter_icon = qtawesome.icon("fa.filter", color="gray") self.filter_set_icon = qtawesome.icon("fa.filter", color="white") + self.init_layout() + self._resetting = False - self.sectionResized.connect(self.handleSectionResized) - self.sectionMoved.connect(self.handleSectionMoved) - #self.sectionPressed.connect(self.model.sort) - + self.sectionClicked.connect(self.on_section_clicked) @property def model(self): @@ -862,38 +876,30 @@ class HorizontalHeader(QtWidgets.QHeaderView): return self._parent.model def init_layout(self): - for i in range(self.count()): - cell_content = QtWidgets.QWidget(self) - column_name, column_label = self.model.get_column(i) - - layout = QtWidgets.QHBoxLayout() - layout.setContentsMargins(5, 5, 5, 0) - layout.setAlignment(Qt.AlignVCenter) - layout.addWidget(QtWidgets.QLabel(column_label)) - + for column_idx in range(self.model.columnCount()): + column_name, column_label = self.model.get_column(column_idx) filter_rec = self.model.get_filters().get(column_name) - if filter_rec: - icon = self.filter_icon - button = QtWidgets.QPushButton(icon, "") - layout.addWidget(button) + if not filter_rec: + continue - # button.setMenu(menu) - button.setFixedSize(24, 24) - # button.setAlignment(Qt.AlignRight) - button.setStyleSheet("QPushButton::menu-indicator{width:0px;}" - "QPushButton{border: none}") - button.clicked.connect(partial(self._get_menu, - column_name, i)) - button.setFlat(True) - self.filter_buttons[column_name] = button + icon = self.filter_icon + button = QtWidgets.QPushButton(icon, "", self) - cell_content.setLayout(layout) + # button.setMenu(menu) + button.setFixedSize(24, 24) + # button.setAlignment(Qt.AlignRight) + button.setStyleSheet("QPushButton::menu-indicator{width:0px;}" + "QPushButton{border: none;background: transparent;}") + button.clicked.connect(partial(self._get_menu, + column_name, column_idx)) + button.setFlat(True) + self.filter_buttons[column_name] = button - self.header_cells.append(cell_content) + def on_section_clicked(self, column_name): + print("on_section_clicked {}".format(column_name)) def showEvent(self, event): - if not self.header_cells: - self.init_layout() + super(HorizontalHeader, self).showEvent(event) for i in range(len(self.header_cells)): cell_content = self.header_cells[i] @@ -902,12 +908,6 @@ class HorizontalHeader(QtWidgets.QHeaderView): cell_content.show() - if len(self.model.get_filters()) > self.count(): - for i in range(self.count(), len(self.header_cells)): - self.header_cells[i].deleteLater() - - super(HorizontalHeader, self).showEvent(event) - def _set_filter_icon(self, column_name): button = self.filter_buttons.get(column_name) if button: @@ -939,18 +939,18 @@ class HorizontalHeader(QtWidgets.QHeaderView): self._set_filter_icon(column_name) self._filter_and_refresh_model_and_menu(column_name, True, False) - def _apply_text_filter(self, column_name, items): + def _apply_text_filter(self, column_name, items, line_edit): """ Resets all checkboxes, prefers inserted text. """ + le_text = line_edit.text() self._update_checked_values(column_name, items, 0) # reset other if self.checked_values.get(column_name) is not None or \ - self.line_edit.text() == '': + le_text == '': self.checked_values.pop(column_name) # reset during typing - text_item = {self.line_edit.text(): self.line_edit.text()} - if self.line_edit.text(): - self._update_checked_values(column_name, text_item, 2) + if le_text: + self._update_checked_values(column_name, {le_text: le_text}, 2) self._set_filter_icon(column_name) self._filter_and_refresh_model_and_menu(column_name, True, True) @@ -970,24 +970,23 @@ class HorizontalHeader(QtWidgets.QHeaderView): menu = QtWidgets.QMenu(self) filter_rec = self.model.get_filters()[column_name] self.menu_items_dict[column_name] = filter_rec.values() - self.line_edit = None # text filtering only if labels same as values, not if codes are used if 'text' in filter_rec.search_variants(): - self.line_edit = QtWidgets.QLineEdit(self) - self.line_edit.setSizePolicy( - QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) + line_edit = QtWidgets.QLineEdit(menu) + line_edit.setClearButtonEnabled(True) + + line_edit.setFixedHeight(line_edit.height()) txt = "Type..." if self.checked_values.get(column_name): txt = list(self.checked_values.get(column_name).keys())[0] - self.line_edit.setPlaceholderText(txt) + line_edit.setPlaceholderText(txt) action_le = QtWidgets.QWidgetAction(menu) - action_le.setDefaultWidget(self.line_edit) - self.line_edit.textChanged.connect( + action_le.setDefaultWidget(line_edit) + line_edit.textChanged.connect( partial(self._apply_text_filter, column_name, - filter_rec.values())) + filter_rec.values(), line_edit)) menu.addAction(action_le) menu.addSeparator() @@ -1076,27 +1075,32 @@ class HorizontalHeader(QtWidgets.QHeaderView): self.checked_values[column_name] = checked - def handleSectionResized(self, i): - if not self.header_cells: - self.init_layout() - for i in range(self.count()): - j = self.visualIndex(i) - logical = self.logicalIndex(j) - self.header_cells[i].setGeometry( - self.sectionViewportPosition(logical), 0, - self.sectionSize(logical) - 1, self.height()) + def paintEvent(self, event): + self._fix_size() + super(HorizontalHeader, self).paintEvent(event) - def handleSectionMoved(self, i, oldVisualIndex, newVisualIndex): - if not self.header_cells: - self.init_layout() - for i in range(min(oldVisualIndex, newVisualIndex), self.count()): - logical = self.logicalIndex(i) - self.header_cells[i].setGeometry( - self.ectionViewportPosition(logical), 0, - self.sectionSize(logical) - 2, self.height()) + def _fix_size(self): + for column_idx in range(self.count()): + vis_index = self.visualIndex(column_idx) + index = self.logicalIndex(vis_index) + section_width = self.sectionSize(index) + + column_name = self.model.headerData(column_idx, + QtCore.Qt.Horizontal, + lib.HeaderNameRole) + button = self.filter_buttons.get(column_name) + if not button: + continue + + pos_x = self.sectionViewportPosition( + index) + section_width - self.height() + + pos_y = 0 + if button.height() < self.height(): + pos_y = int((self.height() - button.height()) / 2) + button.setGeometry( + pos_x, + pos_y, + self.height(), + self.height()) - def fixComboPositions(self): - for i in range(self.count()): - self.header_cells[i].setGeometry( - self.sectionViewportPosition(i), 0, - self.sectionSize(i) - 2, self.height()) From 981443b5c3a789dba845703ea2a512707753db4e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 15:15:59 +0200 Subject: [PATCH 42/58] removed unused imports --- openpype/modules/ftrack/lib/avalon_sync.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index cfe1f011e7..f58e858a5a 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -14,24 +14,21 @@ else: from avalon.api import AvalonMongoDB import avalon + from openpype.api import ( Logger, Anatomy, get_anatomy_settings ) +from openpype.lib import ApplicationManager + +from .constants import CUST_ATTR_ID_KEY +from .custom_attributes import get_openpype_attr from bson.objectid import ObjectId from bson.errors import InvalidId from pymongo import UpdateOne import ftrack_api -from openpype.lib import ApplicationManager - -from .constants import ( - CUST_ATTR_ID_KEY, - CUST_ATTR_AUTO_SYNC, - CUST_ATTR_GROUP -) -from .custom_attributes import get_openpype_attr log = Logger.get_logger(__name__) From d9cbb6eea6cfc18e8ad7b4a2f2c7357f60a9e94b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 16:04:58 +0200 Subject: [PATCH 43/58] SyncServer GUI - working version for Detail list --- openpype/modules/sync_server/tray/models.py | 101 +++++++++++-------- openpype/modules/sync_server/tray/widgets.py | 63 ++++++------ 2 files changed, 90 insertions(+), 74 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 266a9289ca..d2dc3594c6 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -68,17 +68,14 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): return len(self._header) def headerData(self, section, orientation, role=Qt.DisplayRole): + if section >= len(self.COLUMN_LABELS): + return + name = self.COLUMN_LABELS[section][0] if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self.COLUMN_LABELS[section][1] - # if role == Qt.DecorationRole: - # if name in self.column_filtering.keys(): - # return qtawesome.icon("fa.filter", color="white") - # if self.COLUMN_FILTERS.get(name): - # return qtawesome.icon("fa.filter", color="gray") - if role == lib.HeaderNameRole: if orientation == Qt.Horizontal: return self.COLUMN_LABELS[section][0] # return name @@ -199,7 +196,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): representations = self.dbcon.aggregate(self.query) self.refresh(representations) - def set_filter(self, word_filter): + def set_word_filter(self, word_filter): """ Adds text value filtering @@ -209,6 +206,19 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): self._word_filter = word_filter self.refresh() + def get_filters(self): + """ + Returns all available filter editors per column_name keys. + """ + filters = {} + for column_name, _ in self.COLUMN_LABELS: + filter_rec = self.COLUMN_FILTERS.get(column_name) + if filter_rec: + filter_rec.dbcon = self.dbcon + filters[column_name] = filter_rec + + return filters + def get_column_filter(self, index): """ Returns filter object for column 'index @@ -227,6 +237,25 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): return filter_rec + def set_column_filtering(self, checked_values): + """ + Sets dictionary used in '$match' part of MongoDB aggregate + + Args: + checked_values(dict): key:values ({'status':{1:"Foo",3:"Bar"}} + + Modifies: + self._column_filtering : {'status': {'$in': [1, 2, 3]}} + """ + filtering = {} + for column_name, dict_value in checked_values.items(): + column_f = self.COLUMN_FILTERS.get(column_name) + if not column_f: + continue + column_f.dbcon = self.dbcon + filtering[column_name] = column_f.prepare_match_part(dict_value) + + self._column_filtering = filtering def get_column_filter_values(self, index): """ @@ -400,19 +429,6 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self.timer.timeout.connect(self.tick) self.timer.start(self.REFRESH_SEC) - def get_filters(self): - """ - Returns all available filter editors per column_name keys. - """ - filters = {} - for column_name, _ in self.COLUMN_LABELS: - filter_rec = self.COLUMN_FILTERS.get(column_name) - if filter_rec: - filter_rec.dbcon = self.dbcon - filters[column_name] = filter_rec - - return filters - def data(self, index, role): item = self._data[index.row()] @@ -685,26 +701,6 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): return aggr - def set_column_filtering(self, checked_values): - """ - Sets dictionary used in '$match' part of MongoDB aggregate - - Args: - checked_values(dict): key:values ({'status':{1:"Foo",3:"Bar"}} - - Modifies: - self._column_filtering : {'status': {'$in': [1, 2, 3]}} - """ - filtering = {} - for column_name, dict_value in checked_values.items(): - column_f = self.COLUMN_FILTERS.get(column_name) - if not column_f: - continue - column_f.dbcon = self.dbcon - filtering[column_name] = column_f.prepare_match_part(dict_value) - - self._column_filtering = filtering - def get_match_part(self): """ Extend match part with word_filter if present. @@ -843,10 +839,15 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): "updated_dt_local", # local created_dt "updated_dt_remote", # remote created_dt "size", # remote progress - "context.asset", # priority TODO + "size", # priority TODO "status" # status ] + COLUMN_FILTERS = { + 'status': lib.PredefinedSetFilter('status', lib.STATUS), + 'file': lib.RegexTextFilter('file'), + } + refresh_started = QtCore.Signal() refresh_finished = QtCore.Signal() @@ -885,6 +886,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): self._word_filter = None self._id = _id self._initialized = False + self._column_filtering = {} self.sync_server = sync_server # TODO think about admin mode @@ -1033,7 +1035,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): if limit == 0: limit = SyncRepresentationSummaryModel.PAGE_SIZE - return [ + aggr = [ {"$match": self.get_match_part()}, {"$unwind": "$files"}, {'$addFields': { @@ -1129,7 +1131,16 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): ]} ]}} }}, - {"$project": self.projection}, + {"$project": self.projection} + ] + + if self.column_filtering: + aggr.append( + {"$match": self.column_filtering} + ) + print(self.column_filtering) + + aggr.extend([ {"$sort": self.sort}, { '$facet': { @@ -1138,7 +1149,9 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): 'totalCount': [{'$count': 'count'}] } } - ] + ]) + + return aggr def get_match_part(self): """ diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 25abc73a70..e3d5d0fd12 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -203,7 +203,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): layout.addWidget(self.table_view) self.table_view.doubleClicked.connect(self._double_clicked) - self.filter.textChanged.connect(lambda: model.set_filter( + self.filter.textChanged.connect(lambda: model.set_word_filter( self.filter.text())) self.table_view.customContextMenuRequested.connect( self._on_context_menu) @@ -475,7 +475,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): ("local_site", 185), ("remote_site", 185), ("size", 60), - ("priority", 25), + ("priority", 60), ("status", 110) ) @@ -499,53 +499,58 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): top_bar_layout = QtWidgets.QHBoxLayout() top_bar_layout.addWidget(self.filter) - self.table_view = QtWidgets.QTableView() + table_view = QtWidgets.QTableView() headers = [item[0] for item in self.default_widths] model = SyncRepresentationDetailModel(sync_server, headers, _id, project) - self.table_view.setModel(model) - self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.table_view.setSelectionMode( + table_view.setModel(model) + table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + table_view.setSelectionMode( QtWidgets.QAbstractItemView.SingleSelection) - self.table_view.setSelectionBehavior( + table_view.setSelectionBehavior( QtWidgets.QTableView.SelectRows) - self.table_view.horizontalHeader().setSortIndicator(-1, - Qt.AscendingOrder) - self.table_view.setSortingEnabled(True) - self.table_view.horizontalHeader().setSortIndicatorShown(True) - self.table_view.setAlternatingRowColors(True) - self.table_view.verticalHeader().hide() + table_view.horizontalHeader().setSortIndicator(-1, Qt.AscendingOrder) + table_view.horizontalHeader().setSortIndicatorShown(True) + table_view.setAlternatingRowColors(True) + table_view.verticalHeader().hide() column = model.get_header_index("local_site") delegate = ImageDelegate(self) - self.table_view.setItemDelegateForColumn(column, delegate) + table_view.setItemDelegateForColumn(column, delegate) column = model.get_header_index("remote_site") delegate = ImageDelegate(self) - self.table_view.setItemDelegateForColumn(column, delegate) - - for column_name, width in self.default_widths: - idx = model.get_header_index(column_name) - self.table_view.setColumnWidth(idx, width) + table_view.setItemDelegateForColumn(column, delegate) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addLayout(top_bar_layout) - layout.addWidget(self.table_view) + layout.addWidget(table_view) - self.filter.textChanged.connect(lambda: model.set_filter( + self.model = model + + self.selection_model = table_view.selectionModel() + self.selection_model.selectionChanged.connect(self._selection_changed) + + horizontal_header = HorizontalHeader(self) + + table_view.setHorizontalHeader(horizontal_header) + table_view.setSortingEnabled(True) + + for column_name, width in self.default_widths: + idx = model.get_header_index(column_name) + table_view.setColumnWidth(idx, width) + + self.table_view = table_view + + self.filter.textChanged.connect(lambda: model.set_word_filter( self.filter.text())) - self.table_view.customContextMenuRequested.connect( - self._on_context_menu) + 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) def _selection_changed(self): index = self.selection_model.currentIndex() @@ -885,9 +890,7 @@ class HorizontalHeader(QtWidgets.QHeaderView): icon = self.filter_icon button = QtWidgets.QPushButton(icon, "", self) - # button.setMenu(menu) button.setFixedSize(24, 24) - # button.setAlignment(Qt.AlignRight) button.setStyleSheet("QPushButton::menu-indicator{width:0px;}" "QPushButton{border: none;background: transparent;}") button.clicked.connect(partial(self._get_menu, @@ -1080,7 +1083,7 @@ class HorizontalHeader(QtWidgets.QHeaderView): super(HorizontalHeader, self).paintEvent(event) def _fix_size(self): - for column_idx in range(self.count()): + for column_idx in range(self.model.columnCount()): vis_index = self.visualIndex(column_idx) index = self.logicalIndex(vis_index) section_width = self.sectionSize(index) From 7c53a6587d27f2849c7d40d9bd86c4f80d4c8a47 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 16:11:28 +0200 Subject: [PATCH 44/58] SyncServer GUI - renamed methods --- openpype/modules/sync_server/tray/models.py | 27 +++++++++------------ 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index d2dc3594c6..8a4bc53b25 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -119,7 +119,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): self._rec_loaded = 0 if not representations: - self.query = self.get_default_query(load_records) + self.query = self.get_query(load_records) representations = self.dbcon.aggregate(self.query) self.add_page_records(self.local_site, self.remote_site, @@ -154,7 +154,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): log.debug("fetchMore") items_to_fetch = min(self._total_records - self._rec_loaded, self.PAGE_SIZE) - self.query = self.get_default_query(self._rec_loaded) + self.query = self.get_query(self._rec_loaded) representations = self.dbcon.aggregate(self.query) self.beginInsertRows(index, self._rec_loaded, @@ -187,7 +187,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): order = -1 self.sort = {self.SORT_BY_COLUMN[index]: order, '_id': 1} - self.query = self.get_default_query() + self.query = self.get_query() # import json # log.debug(json.dumps(self.query, indent=4).\ # replace('False', 'false').\ @@ -415,12 +415,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self.local_site = self.sync_server.get_active_site(self.project) self.remote_site = self.sync_server.get_remote_site(self.project) - self.projection = self.get_default_projection() - self.sort = self.DEFAULT_SORT - self.query = self.get_default_query() - self.default_query = list(self.get_default_query()) + self.query = self.get_query() + self.default_query = list(self.get_query()) representations = self.dbcon.aggregate(self.query) self.refresh(representations) @@ -544,7 +542,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self._data.append(item) self._rec_loaded += 1 - def get_default_query(self, limit=0): + def get_query(self, limit=0): """ Returns basic aggregate query for main table. @@ -735,7 +733,8 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): return base_match - def get_default_projection(self): + @property + def projection(self): """ Projection part for aggregate query. @@ -896,10 +895,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): self.sort = self.DEFAULT_SORT - # in case we would like to hide/show some columns - self.projection = self.get_default_projection() - - self.query = self.get_default_query() + self.query = self.get_query() representations = self.dbcon.aggregate(self.query) self.refresh(representations) @@ -1021,7 +1017,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): self._data.append(item) self._rec_loaded += 1 - def get_default_query(self, limit=0): + def get_query(self, limit=0): """ Gets query that gets used when no extra sorting, filtering or projecting is needed. @@ -1174,7 +1170,8 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): '$or': [{'files.path': {'$regex': regex_str, '$options': 'i'}}] } - def get_default_projection(self): + @property + def projection(self): """ Projection part for aggregate query. From ef208e715de6e19e7c611388ba64895cb7835546 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 16:15:59 +0200 Subject: [PATCH 45/58] changed how config actions are registered --- openpype/launcher_actions.py | 30 ------------------------------ openpype/tools/launcher/actions.py | 23 ++++++++--------------- 2 files changed, 8 insertions(+), 45 deletions(-) delete mode 100644 openpype/launcher_actions.py diff --git a/openpype/launcher_actions.py b/openpype/launcher_actions.py deleted file mode 100644 index cf68dfb5c1..0000000000 --- a/openpype/launcher_actions.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -import sys - -from avalon import api, pipeline - -PACKAGE_DIR = os.path.dirname(__file__) -PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins", "launcher") -ACTIONS_DIR = os.path.join(PLUGINS_DIR, "actions") - - -def register_launcher_actions(): - """Register specific actions which should be accessible in the launcher""" - - actions = [] - ext = ".py" - sys.path.append(ACTIONS_DIR) - - for f in os.listdir(ACTIONS_DIR): - file, extention = os.path.splitext(f) - if ext in extention: - module = __import__(file) - klass = getattr(module, file) - actions.append(klass) - - if actions is []: - return - - for action in actions: - print("Using launcher action from config @ '{}'".format(action.name)) - pipeline.register_plugin(api.Action, action) diff --git a/openpype/tools/launcher/actions.py b/openpype/tools/launcher/actions.py index 6261fe91ca..f2308c0150 100644 --- a/openpype/tools/launcher/actions.py +++ b/openpype/tools/launcher/actions.py @@ -2,6 +2,7 @@ import os import importlib from avalon import api, lib, style +from openpype import PLUGINS_DIR from openpype.api import Logger, resources from openpype.lib import ( ApplictionExecutableNotFound, @@ -70,21 +71,6 @@ def register_default_actions(): api.register_plugin(api.Action, LoaderLibrary) -def register_config_actions(): - """Register actions from the configuration for Launcher""" - - module_name = os.environ["AVALON_CONFIG"] - config = importlib.import_module(module_name) - if not hasattr(config, "register_launcher_actions"): - print( - "Current configuration `%s` has no 'register_launcher_actions'" - % config.__name__ - ) - return - - config.register_launcher_actions() - - def register_actions_from_paths(paths): if not paths: return @@ -105,6 +91,13 @@ def register_actions_from_paths(paths): api.register_plugin_path(api.Action, path) + +def register_config_actions(): + """Register actions from the configuration for Launcher""" + + actions_dir = os.path.join(PLUGINS_DIR, "actions") + register_actions_from_paths([actions_dir]) + def register_environment_actions(): """Register actions from AVALON_ACTIONS for Launcher.""" From 9efe5d4f6e675de00b5688c2858e6f7c704efb8c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 16:16:30 +0200 Subject: [PATCH 46/58] SyncServer GUI - added icon, clean up --- openpype/modules/sync_server/tray/models.py | 1 - openpype/modules/sync_server/tray/widgets.py | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 8a4bc53b25..3ee372d27d 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -684,7 +684,6 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): aggr.append( {"$match": self.column_filtering} ) - print(self.column_filtering) aggr.extend( [{"$sort": self.sort}, diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index e3d5d0fd12..9771d656ff 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -873,8 +873,6 @@ class HorizontalHeader(QtWidgets.QHeaderView): self._resetting = False - self.sectionClicked.connect(self.on_section_clicked) - @property def model(self): """Keep model synchronized with parent widget""" @@ -898,9 +896,6 @@ class HorizontalHeader(QtWidgets.QHeaderView): button.setFlat(True) self.filter_buttons[column_name] = button - def on_section_clicked(self, column_name): - print("on_section_clicked {}".format(column_name)) - def showEvent(self, event): super(HorizontalHeader, self).showEvent(event) @@ -978,6 +973,8 @@ class HorizontalHeader(QtWidgets.QHeaderView): if 'text' in filter_rec.search_variants(): line_edit = QtWidgets.QLineEdit(menu) line_edit.setClearButtonEnabled(True) + line_edit.addAction(self.filter_icon, + QtWidgets.QLineEdit.LeadingPosition) line_edit.setFixedHeight(line_edit.height()) txt = "Type..." From db0f6dcefe6d9091b280f5fd422487f541a706ae Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 16:16:33 +0200 Subject: [PATCH 47/58] removed "default actions" and it's registration function --- openpype/modules/launcher_action.py | 1 - openpype/tools/launcher/actions.py | 63 +---------------------------- 2 files changed, 1 insertion(+), 63 deletions(-) diff --git a/openpype/modules/launcher_action.py b/openpype/modules/launcher_action.py index da0468d495..5ed8585b6a 100644 --- a/openpype/modules/launcher_action.py +++ b/openpype/modules/launcher_action.py @@ -22,7 +22,6 @@ class LauncherAction(PypeModule, ITrayAction): # Register actions if self.tray_initialized: from openpype.tools.launcher import actions - # actions.register_default_actions() actions.register_config_actions() actions_paths = self.manager.collect_plugin_paths()["actions"] actions.register_actions_from_paths(actions_paths) diff --git a/openpype/tools/launcher/actions.py b/openpype/tools/launcher/actions.py index f2308c0150..eb89ad3d88 100644 --- a/openpype/tools/launcher/actions.py +++ b/openpype/tools/launcher/actions.py @@ -1,7 +1,6 @@ import os -import importlib -from avalon import api, lib, style +from avalon import api, style from openpype import PLUGINS_DIR from openpype.api import Logger, resources from openpype.lib import ( @@ -11,66 +10,6 @@ from openpype.lib import ( from Qt import QtWidgets, QtGui -class ProjectManagerAction(api.Action): - name = "projectmanager" - label = "Project Manager" - icon = "gear" - order = 999 # at the end - - def is_compatible(self, session): - return "AVALON_PROJECT" in session - - def process(self, session, **kwargs): - return lib.launch( - executable="python", - args=[ - "-u", "-m", "avalon.tools.projectmanager", - session['AVALON_PROJECT'] - ] - ) - - -class LoaderAction(api.Action): - name = "loader" - label = "Loader" - icon = "cloud-download" - order = 998 - - def is_compatible(self, session): - return "AVALON_PROJECT" in session - - def process(self, session, **kwargs): - return lib.launch( - executable="python", - args=[ - "-u", "-m", "avalon.tools.loader", session['AVALON_PROJECT'] - ] - ) - - -class LoaderLibrary(api.Action): - name = "loader_os" - label = "Library Loader" - icon = "book" - order = 997 # at the end - - def is_compatible(self, session): - return True - - def process(self, session, **kwargs): - return lib.launch( - executable="python", - args=["-u", "-m", "avalon.tools.libraryloader"] - ) - - -def register_default_actions(): - """Register default actions for Launcher""" - api.register_plugin(api.Action, ProjectManagerAction) - api.register_plugin(api.Action, LoaderAction) - api.register_plugin(api.Action, LoaderLibrary) - - def register_actions_from_paths(paths): if not paths: return From ac0e2b34a96e53fac9851c3bf3da4b2e0df9fe33 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Apr 2021 16:24:57 +0200 Subject: [PATCH 48/58] removed empty spaces --- openpype/tools/launcher/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/launcher/actions.py b/openpype/tools/launcher/actions.py index eb89ad3d88..72c7aece72 100644 --- a/openpype/tools/launcher/actions.py +++ b/openpype/tools/launcher/actions.py @@ -30,7 +30,7 @@ def register_actions_from_paths(paths): api.register_plugin_path(api.Action, path) - + def register_config_actions(): """Register actions from the configuration for Launcher""" From a5441f153351480d1125317a4697cf7b836b270c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 21 Apr 2021 15:51:21 +0100 Subject: [PATCH 49/58] Added support for point caches --- .../plugins/create/create_pointcache.py | 35 +++ .../hosts/blender/plugins/load/load_abc.py | 247 ++++++++++++++++++ .../hosts/blender/plugins/load/load_model.py | 237 ----------------- .../blender/plugins/publish/extract_abc.py | 2 +- 4 files changed, 283 insertions(+), 238 deletions(-) create mode 100644 openpype/hosts/blender/plugins/create/create_pointcache.py create mode 100644 openpype/hosts/blender/plugins/load/load_abc.py diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py new file mode 100644 index 0000000000..03a468f82e --- /dev/null +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -0,0 +1,35 @@ +"""Create a pointcache asset.""" + +import bpy + +from avalon import api +from avalon.blender import lib +import openpype.hosts.blender.api.plugin + + +class CreatePointcache(openpype.hosts.blender.api.plugin.Creator): + """Polygonal static geometry""" + + name = "pointcacheMain" + label = "Point Cache" + family = "pointcache" + icon = "gears" + + def process(self): + + asset = self.data["asset"] + subset = self.data["subset"] + name = openpype.hosts.blender.api.plugin.asset_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + if (self.options or {}).get("useSelection"): + objects = lib.get_selection() + for obj in objects: + collection.objects.link(obj) + if obj.type == 'EMPTY': + objects.extend(obj.children) + + return collection diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py new file mode 100644 index 0000000000..9e20ccabbc --- /dev/null +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -0,0 +1,247 @@ +"""Load an asset in Blender from an Alembic file.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +from avalon import api, blender +import bpy +import openpype.hosts.blender.api.plugin as plugin + + +class CacheModelLoader(plugin.AssetLoader): + """Load cache models. + + Stores the imported asset in a collection named after the asset. + + Note: + At least for now it only supports Alembic files. + """ + + families = ["model", "pointcache"] + representations = ["abc"] + + label = "Link Alembic" + icon = "code-fork" + color = "orange" + + def _remove(self, objects, container): + for obj in list(objects): + if obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + elif obj.type == 'EMPTY': + bpy.data.objects.remove(obj) + + bpy.data.collections.remove(container) + + def _process(self, libpath, container_name, parent_collection): + bpy.ops.object.select_all(action='DESELECT') + + view_layer = bpy.context.view_layer + view_layer_collection = view_layer.active_layer_collection.collection + + relative = bpy.context.preferences.filepaths.use_relative_paths + bpy.ops.wm.alembic_import( + filepath=libpath, + relative_path=relative + ) + + parent = parent_collection + + if parent is None: + parent = bpy.context.scene.collection + + model_container = bpy.data.collections.new(container_name) + parent.children.link(model_container) + for obj in bpy.context.selected_objects: + model_container.objects.link(obj) + view_layer_collection.objects.unlink(obj) + + name = obj.name + obj.name = f"{name}:{container_name}" + + # Groups are imported as Empty objects in Blender + if obj.type == 'MESH': + data_name = obj.data.name + obj.data.name = f"{data_name}:{container_name}" + + if not obj.get(blender.pipeline.AVALON_PROPERTY): + obj[blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + bpy.ops.object.select_all(action='DESELECT') + + return model_container + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + + lib_container = plugin.asset_name( + asset, subset + ) + unique_number = plugin.get_unique_number( + asset, subset + ) + namespace = namespace or f"{asset}_{unique_number}" + container_name = plugin.asset_name( + asset, subset, unique_number + ) + + container = bpy.data.collections.new(lib_container) + container.name = container_name + blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + obj_container = self._process( + libpath, container_name, None) + + container_metadata["obj_container"] = obj_container + + # Save the list of objects in the metadata container + container_metadata["objects"] = obj_container.all_objects + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + def update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + collection = bpy.data.collections.get( + container["objectName"] + ) + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + self.log.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert not (collection.children), ( + "Nested collections are not supported." + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + collection_metadata = collection.get( + blender.pipeline.AVALON_PROPERTY) + collection_libpath = collection_metadata["libpath"] + + obj_container = plugin.get_local_collection_with_name( + collection_metadata["obj_container"].name + ) + objects = obj_container.all_objects + + container_name = obj_container.name + + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + self.log.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + self.log.info("Library already loaded, not updating...") + return + + parent = plugin.get_parent_collection(obj_container) + + self._remove(objects, obj_container) + + obj_container = self._process( + str(libpath), container_name, parent) + + collection_metadata["obj_container"] = obj_container + collection_metadata["objects"] = obj_container.all_objects + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (openpype:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + + collection_metadata = collection.get( + blender.pipeline.AVALON_PROPERTY) + + obj_container = plugin.get_local_collection_with_name( + collection_metadata["obj_container"].name + ) + objects = obj_container.all_objects + + self._remove(objects, obj_container) + + bpy.data.collections.remove(collection) + + return True \ No newline at end of file diff --git a/openpype/hosts/blender/plugins/load/load_model.py b/openpype/hosts/blender/plugins/load/load_model.py index 168bdf9321..d645bedfcc 100644 --- a/openpype/hosts/blender/plugins/load/load_model.py +++ b/openpype/hosts/blender/plugins/load/load_model.py @@ -242,240 +242,3 @@ class BlendModelLoader(plugin.AssetLoader): bpy.data.collections.remove(collection) return True - - -class CacheModelLoader(plugin.AssetLoader): - """Load cache models. - - Stores the imported asset in a collection named after the asset. - - Note: - At least for now it only supports Alembic files. - """ - - families = ["model"] - representations = ["abc"] - - label = "Link Model" - icon = "code-fork" - color = "orange" - - def _remove(self, objects, container): - for obj in list(objects): - if obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - elif obj.type == 'EMPTY': - bpy.data.objects.remove(obj) - - bpy.data.collections.remove(container) - - def _process(self, libpath, container_name, parent_collection): - bpy.ops.object.select_all(action='DESELECT') - - view_layer = bpy.context.view_layer - view_layer_collection = view_layer.active_layer_collection.collection - - relative = bpy.context.preferences.filepaths.use_relative_paths - bpy.ops.wm.alembic_import( - filepath=libpath, - relative_path=relative - ) - - parent = parent_collection - - if parent is None: - parent = bpy.context.scene.collection - - model_container = bpy.data.collections.new(container_name) - parent.children.link(model_container) - for obj in bpy.context.selected_objects: - model_container.objects.link(obj) - view_layer_collection.objects.unlink(obj) - - name = obj.name - obj.name = f"{name}:{container_name}" - - # Groups are imported as Empty objects in Blender - if obj.type == 'MESH': - data_name = obj.data.name - obj.data.name = f"{data_name}:{container_name}" - - if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": container_name}) - - bpy.ops.object.select_all(action='DESELECT') - - return model_container - - def process_asset( - self, context: dict, name: str, namespace: Optional[str] = None, - options: Optional[Dict] = None - ) -> Optional[List]: - """ - Arguments: - name: Use pre-defined name - namespace: Use pre-defined namespace - context: Full parenthood of representation to load - options: Additional settings dictionary - """ - - libpath = self.fname - asset = context["asset"]["name"] - subset = context["subset"]["name"] - - lib_container = plugin.asset_name( - asset, subset - ) - unique_number = plugin.get_unique_number( - asset, subset - ) - namespace = namespace or f"{asset}_{unique_number}" - container_name = plugin.asset_name( - asset, subset, unique_number - ) - - container = bpy.data.collections.new(lib_container) - container.name = container_name - blender.pipeline.containerise_existing( - container, - name, - namespace, - context, - self.__class__.__name__, - ) - - container_metadata = container.get( - blender.pipeline.AVALON_PROPERTY) - - container_metadata["libpath"] = libpath - container_metadata["lib_container"] = lib_container - - obj_container = self._process( - libpath, container_name, None) - - container_metadata["obj_container"] = obj_container - - # Save the list of objects in the metadata container - container_metadata["objects"] = obj_container.all_objects - - nodes = list(container.objects) - nodes.append(container) - self[:] = nodes - return nodes - - def update(self, container: Dict, representation: Dict): - """Update the loaded asset. - - This will remove all objects of the current collection, load the new - ones and add them to the collection. - If the objects of the collection are used in another collection they - will not be removed, only unlinked. Normally this should not be the - case though. - - Warning: - No nested collections are supported at the moment! - """ - collection = bpy.data.collections.get( - container["objectName"] - ) - libpath = Path(api.get_representation_path(representation)) - extension = libpath.suffix.lower() - - self.log.info( - "Container: %s\nRepresentation: %s", - pformat(container, indent=2), - pformat(representation, indent=2), - ) - - assert collection, ( - f"The asset is not loaded: {container['objectName']}" - ) - assert not (collection.children), ( - "Nested collections are not supported." - ) - assert libpath, ( - "No existing library file found for {container['objectName']}" - ) - assert libpath.is_file(), ( - f"The file doesn't exist: {libpath}" - ) - assert extension in plugin.VALID_EXTENSIONS, ( - f"Unsupported file: {libpath}" - ) - - collection_metadata = collection.get( - blender.pipeline.AVALON_PROPERTY) - collection_libpath = collection_metadata["libpath"] - - obj_container = plugin.get_local_collection_with_name( - collection_metadata["obj_container"].name - ) - objects = obj_container.all_objects - - container_name = obj_container.name - - normalized_collection_libpath = ( - str(Path(bpy.path.abspath(collection_libpath)).resolve()) - ) - normalized_libpath = ( - str(Path(bpy.path.abspath(str(libpath))).resolve()) - ) - self.log.debug( - "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", - normalized_collection_libpath, - normalized_libpath, - ) - if normalized_collection_libpath == normalized_libpath: - self.log.info("Library already loaded, not updating...") - return - - parent = plugin.get_parent_collection(obj_container) - - self._remove(objects, obj_container) - - obj_container = self._process( - str(libpath), container_name, parent) - - collection_metadata["obj_container"] = obj_container - collection_metadata["objects"] = obj_container.all_objects - collection_metadata["libpath"] = str(libpath) - collection_metadata["representation"] = str(representation["_id"]) - - def remove(self, container: Dict) -> bool: - """Remove an existing container from a Blender scene. - - Arguments: - container (openpype:container-1.0): Container to remove, - from `host.ls()`. - - Returns: - bool: Whether the container was deleted. - - Warning: - No nested collections are supported at the moment! - """ - collection = bpy.data.collections.get( - container["objectName"] - ) - if not collection: - return False - assert not (collection.children), ( - "Nested collections are not supported." - ) - - collection_metadata = collection.get( - blender.pipeline.AVALON_PROPERTY) - - obj_container = plugin.get_local_collection_with_name( - collection_metadata["obj_container"].name - ) - objects = obj_container.all_objects - - self._remove(objects, obj_container) - - bpy.data.collections.remove(collection) - - return True diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index a7653d9f5a..a6315908fc 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -11,7 +11,7 @@ class ExtractABC(openpype.api.Extractor): label = "Extract ABC" hosts = ["blender"] - families = ["model"] + families = ["model", "pointcache"] optional = True def process(self, instance): From 5d150a73f65bcbfbef2018a90b8e6d9418a5f71c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Wed, 21 Apr 2021 17:09:49 +0200 Subject: [PATCH 50/58] fix first frame for sequence --- openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py index d0c6c4eb14..7c9e201986 100644 --- a/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py @@ -74,6 +74,8 @@ class ExtractRedshiftProxy(openpype.api.Extractor): 'files': repr_files, "stagingDir": staging_dir, } + if anim_on: + representation["frameStart"] = instance.data["proxyFrameStart"] instance.data["representations"].append(representation) self.log.info("Extracted instance '%s' to: %s" From ee598d1521677d7d87635d6cc952452c3c64ebe1 Mon Sep 17 00:00:00 2001 From: jezscha Date: Wed, 21 Apr 2021 15:16:29 +0000 Subject: [PATCH 51/58] Create draft PR for #1391 From a8ac382c8a219a4941c6baebb3fc59f4cbb84d52 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 21 Apr 2021 17:17:54 +0200 Subject: [PATCH 52/58] Nuke: fixing menu and publish callback --- openpype/hosts/nuke/api/__init__.py | 2 +- openpype/hosts/nuke/api/menu.py | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index c80507e7ea..bd7a95f916 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -106,7 +106,7 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( instance, old_value, new_value)) - from avalon.api.nuke import ( + from avalon.nuke import ( viewer_update_and_undo_stop, add_publish_knob ) diff --git a/openpype/hosts/nuke/api/menu.py b/openpype/hosts/nuke/api/menu.py index 2317066528..8434712138 100644 --- a/openpype/hosts/nuke/api/menu.py +++ b/openpype/hosts/nuke/api/menu.py @@ -26,9 +26,9 @@ def install(): menu.addCommand( name, workfiles.show, - index=(rm_item[0]) + index=2 ) - + menu.addSeparator(index=3) # replace reset resolution from avalon core to pype's name = "Reset Resolution" new_name = "Set Resolution" @@ -63,27 +63,27 @@ def install(): # add colorspace menu item name = "Set Colorspace" menu.addCommand( - name, lambda: WorkfileSettings().set_colorspace(), - index=(rm_item[0] + 2) + name, lambda: WorkfileSettings().set_colorspace() ) log.debug("Adding menu item: {}".format(name)) + # add item that applies all setting above + name = "Apply All Settings" + menu.addCommand( + name, + lambda: WorkfileSettings().set_context_settings() + ) + log.debug("Adding menu item: {}".format(name)) + + menu.addSeparator() + # add workfile builder menu item name = "Build Workfile" menu.addCommand( - name, lambda: BuildWorkfile().process(), - index=(rm_item[0] + 7) + name, lambda: BuildWorkfile().process() ) log.debug("Adding menu item: {}".format(name)) - # add item that applies all setting above - name = "Apply All Settings" - menu.addCommand( - name, - lambda: WorkfileSettings().set_context_settings(), - index=(rm_item[0] + 3) - ) - log.debug("Adding menu item: {}".format(name)) # adding shortcuts add_shortcuts_from_presets() From b40579a861752fb5736884e84038f834367dd145 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 21 Apr 2021 17:19:18 +0200 Subject: [PATCH 53/58] Hound: suggestion --- openpype/hosts/nuke/api/menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/menu.py b/openpype/hosts/nuke/api/menu.py index 8434712138..021ea04159 100644 --- a/openpype/hosts/nuke/api/menu.py +++ b/openpype/hosts/nuke/api/menu.py @@ -67,7 +67,7 @@ def install(): ) log.debug("Adding menu item: {}".format(name)) - # add item that applies all setting above + # add item that applies all setting above name = "Apply All Settings" menu.addCommand( name, From 88feba27243e7681e85bd14dc71990ae801a7655 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 21 Apr 2021 17:48:17 +0100 Subject: [PATCH 54/58] Hound fixes --- openpype/hosts/blender/plugins/load/load_abc.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 9e20ccabbc..4248cffd69 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -1,6 +1,5 @@ """Load an asset in Blender from an Alembic file.""" -import logging from pathlib import Path from pprint import pformat from typing import Dict, List, Optional @@ -244,4 +243,4 @@ class CacheModelLoader(plugin.AssetLoader): bpy.data.collections.remove(collection) - return True \ No newline at end of file + return True From 6742fc89009642df555e20aefdaad736a55da637 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 21:42:10 +0200 Subject: [PATCH 55/58] Hound --- openpype/modules/sync_server/tray/models.py | 14 ++++++++------ openpype/modules/sync_server/tray/widgets.py | 12 +++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 3ee372d27d..8e97f5207d 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -6,7 +6,6 @@ from Qt import QtCore from Qt.QtCore import Qt from avalon.tools.delegates import pretty_timestamp -from avalon.vendor import qtawesome from openpype.lib import PypeLogger @@ -71,7 +70,6 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): if section >= len(self.COLUMN_LABELS): return - name = self.COLUMN_LABELS[section][0] if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self.COLUMN_LABELS[section][1] @@ -453,9 +451,11 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): if role == lib.FailedRole: if header_value == 'local_site': - return item.status == lib.STATUS[2] and item.local_progress < 1 + return item.status == lib.STATUS[2] and \ + item.local_progress < 1 if header_value == 'remote_site': - return item.status == lib.STATUS[2] and item.remote_progress < 1 + return item.status == lib.STATUS[2] and \ + item.remote_progress < 1 if role == Qt.DisplayRole: # because of ImageDelegate @@ -928,9 +928,11 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): if role == lib.FailedRole: if header_value == 'local_site': - return item.status == lib.STATUS[2] and item.local_progress < 1 + return item.status == lib.STATUS[2] and \ + item.local_progress < 1 if header_value == 'remote_site': - return item.status == lib.STATUS[2] and item.remote_progress < 1 + return item.status == lib.STATUS[2] and \ + item.remote_progress <1 if role == Qt.DisplayRole: # because of ImageDelegate diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 9771d656ff..e8b276580a 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -95,7 +95,6 @@ class SyncProjectListWidget(ProjectListWidget): self.project_name = point_index.data(QtCore.Qt.DisplayRole) menu = QtWidgets.QMenu() - #menu.setStyleSheet(style.load_stylesheet()) actions_mapping = {} if self.sync_server.is_project_paused(self.project_name): @@ -269,7 +268,6 @@ class SyncRepresentationWidget(QtWidgets.QWidget): format(self.representation_id)) menu = QtWidgets.QMenu() - menu.setStyleSheet(style.load_stylesheet()) actions_mapping = {} actions_kwargs_mapping = {} @@ -594,7 +592,6 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): self.item = self.model._data[point_index.row()] menu = QtWidgets.QMenu() - #menu.setStyleSheet(style.load_stylesheet()) actions_mapping = {} actions_kwargs_mapping = {} @@ -889,7 +886,8 @@ class HorizontalHeader(QtWidgets.QHeaderView): button = QtWidgets.QPushButton(icon, "", self) button.setFixedSize(24, 24) - button.setStyleSheet("QPushButton::menu-indicator{width:0px;}" + button.setStyleSheet( + "QPushButton::menu-indicator{width:0px;}" "QPushButton{border: none;background: transparent;}") button.clicked.connect(partial(self._get_menu, column_name, column_idx)) @@ -902,7 +900,7 @@ class HorizontalHeader(QtWidgets.QHeaderView): for i in range(len(self.header_cells)): cell_content = self.header_cells[i] cell_content.setGeometry(self.sectionViewportPosition(i), 0, - self.sectionSize(i)-1, self.height()) + self.sectionSize(i) - 1, self.height()) cell_content.show() @@ -1064,8 +1062,8 @@ class HorizontalHeader(QtWidgets.QHeaderView): Modifies 'self.checked_values' """ - checked = self.checked_values.get(column_name, - dict(self.menu_items_dict[column_name])) + copy_menu_items = dict(self.menu_items_dict[column_name]) + checked = self.checked_values.get(column_name, copy_menu_items) set_items = dict(values.items()) # prevent dict change during loop for value, label in set_items.items(): if state == 2 and label: # checked From 498151db82eba4656f669c3c9dc562c5e1483765 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 22:15:50 +0200 Subject: [PATCH 56/58] Hound --- openpype/modules/sync_server/tray/models.py | 8 ++++---- openpype/modules/sync_server/tray/widgets.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 8e97f5207d..dc2094825e 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -452,10 +452,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): if role == lib.FailedRole: if header_value == 'local_site': return item.status == lib.STATUS[2] and \ - item.local_progress < 1 + item.local_progress < 1 if header_value == 'remote_site': return item.status == lib.STATUS[2] and \ - item.remote_progress < 1 + item.remote_progress < 1 if role == Qt.DisplayRole: # because of ImageDelegate @@ -929,10 +929,10 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): if role == lib.FailedRole: if header_value == 'local_site': return item.status == lib.STATUS[2] and \ - item.local_progress < 1 + item.local_progress < 1 if header_value == 'remote_site': return item.status == lib.STATUS[2] and \ - item.remote_progress <1 + item.remote_progress <1 if role == Qt.DisplayRole: # because of ImageDelegate diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index e8b276580a..2cdc99c671 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -1101,4 +1101,3 @@ class HorizontalHeader(QtWidgets.QHeaderView): pos_y, self.height(), self.height()) - From 8b00525f9d3da0efe8512513e75af01ed2ca44f3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 22 Apr 2021 10:20:56 +0200 Subject: [PATCH 57/58] also pass new values with metadata --- openpype/modules/ftrack/ftrack_module.py | 8 +++++--- openpype/modules/settings_action.py | 8 +++++--- openpype/settings/lib.py | 19 ++++++++++++++++--- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index 60eed5c941..848f4cea82 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -130,7 +130,9 @@ class FtrackModule( if self.tray_module: self.tray_module.changed_user() - def on_system_settings_save(self, old_value, new_value, changes): + def on_system_settings_save( + self, old_value, new_value, changes, new_value_metadata + ): """Implementation of ISettingsChangeListener interface.""" try: session = self.create_ftrack_session() @@ -169,7 +171,7 @@ class FtrackModule( elif key == CUST_ATTR_TOOLS: tool_attribute = custom_attribute - app_manager = ApplicationManager(new_value) + app_manager = ApplicationManager(new_value_metadata) missing_attributes = [] if not app_attribute: missing_attributes.append(CUST_ATTR_APPLICATIONS) @@ -217,7 +219,7 @@ class FtrackModule( return def on_project_anatomy_save( - self, old_value, new_value, changes, project_name + self, old_value, new_value, changes, project_name, new_value_metadata ): """Implementation of ISettingsChangeListener interface.""" if not project_name: diff --git a/openpype/modules/settings_action.py b/openpype/modules/settings_action.py index 371e190c12..3f7cb8c3ba 100644 --- a/openpype/modules/settings_action.py +++ b/openpype/modules/settings_action.py @@ -16,18 +16,20 @@ class ISettingsChangeListener: } """ @abstractmethod - def on_system_settings_save(self, old_value, new_value, changes): + def on_system_settings_save( + self, old_value, new_value, changes, new_value_metadata + ): pass @abstractmethod def on_project_settings_save( - self, old_value, new_value, changes, project_name + self, old_value, new_value, changes, project_name, new_value_metadata ): pass @abstractmethod def on_project_anatomy_save( - self, old_value, new_value, changes, project_name + self, old_value, new_value, changes, project_name, new_value_metadata ): pass diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 9c05c8e86c..f61166fa69 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -119,6 +119,7 @@ def save_studio_settings(data): old_data = get_system_settings() default_values = get_default_settings()[SYSTEM_SETTINGS_KEY] new_data = apply_overrides(default_values, copy.deepcopy(data)) + new_data_with_metadata = copy.deepcopy(new_data) clear_metadata_from_settings(new_data) changes = calculate_changes(old_data, new_data) @@ -128,7 +129,9 @@ def save_studio_settings(data): for module in modules_manager.get_enabled_modules(): if isinstance(module, ISettingsChangeListener): try: - module.on_system_settings_save(old_data, new_data, changes) + module.on_system_settings_save( + old_data, new_data, changes, new_data_with_metadata + ) except SaveWarningExc as exc: warnings.extend(exc.warnings) @@ -173,6 +176,7 @@ def save_project_settings(project_name, overrides): old_data = get_default_project_settings(exclude_locals=True) new_data = apply_overrides(default_values, copy.deepcopy(overrides)) + new_data_with_metadata = copy.deepcopy(new_data) clear_metadata_from_settings(new_data) changes = calculate_changes(old_data, new_data) @@ -182,7 +186,11 @@ def save_project_settings(project_name, overrides): if isinstance(module, ISettingsChangeListener): try: module.on_project_settings_save( - old_data, new_data, project_name, changes + old_data, + new_data, + project_name, + changes, + new_data_with_metadata ) except SaveWarningExc as exc: warnings.extend(exc.warnings) @@ -229,6 +237,7 @@ def save_project_anatomy(project_name, anatomy_data): old_data = get_default_anatomy_settings(exclude_locals=True) new_data = apply_overrides(default_values, copy.deepcopy(anatomy_data)) + new_data_with_metadata = copy.deepcopy(new_data) clear_metadata_from_settings(new_data) changes = calculate_changes(old_data, new_data) @@ -238,7 +247,11 @@ def save_project_anatomy(project_name, anatomy_data): if isinstance(module, ISettingsChangeListener): try: module.on_project_anatomy_save( - old_data, new_data, changes, project_name + old_data, + new_data, + changes, + project_name, + new_data_with_metadata ) except SaveWarningExc as exc: warnings.extend(exc.warnings) From 02447101bf4e83b2229279ebb602fdfe1a4a7474 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 22 Apr 2021 10:43:19 +0200 Subject: [PATCH 58/58] SyncServer GUI - small fixes in Status filtering Clear text in column filtering --- openpype/modules/sync_server/tray/lib.py | 5 +++- openpype/modules/sync_server/tray/models.py | 2 +- openpype/modules/sync_server/tray/widgets.py | 30 ++++++++++++-------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 41b0eb43f9..3597213b31 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -64,8 +64,11 @@ class PredefinedSetFilter(AbstractColumnFilter): def __init__(self, column_name, values): super().__init__(column_name) - self._search_variants = ['text', 'checkbox'] + self._search_variants = ['checkbox'] self._values = values + if self._values and \ + list(self._values.keys())[0] == list(self._values.values())[0]: + self._search_variants.append('text') def values(self): return {k: v for k, v in self._values.items()} diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index dc2094825e..981299c6cf 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -932,7 +932,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): item.local_progress < 1 if header_value == 'remote_site': return item.status == lib.STATUS[2] and \ - item.remote_progress <1 + item.remote_progress < 1 if role == Qt.DisplayRole: # because of ImageDelegate diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 2cdc99c671..6d8348becb 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -165,13 +165,16 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.representation_id = None self.site_name = None # to pause/unpause representation - self.filter = QtWidgets.QLineEdit() - self.filter.setPlaceholderText("Filter representations..") + self.txt_filter = QtWidgets.QLineEdit() + self.txt_filter.setPlaceholderText("Quick filter representations..") + self.txt_filter.setClearButtonEnabled(True) + self.txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"), + QtWidgets.QLineEdit.LeadingPosition) self._scrollbar_pos = None top_bar_layout = QtWidgets.QHBoxLayout() - top_bar_layout.addWidget(self.filter) + top_bar_layout.addWidget(self.txt_filter) self.table_view = QtWidgets.QTableView() headers = [item[0] for item in self.default_widths] @@ -202,8 +205,8 @@ class SyncRepresentationWidget(QtWidgets.QWidget): layout.addWidget(self.table_view) self.table_view.doubleClicked.connect(self._double_clicked) - self.filter.textChanged.connect(lambda: model.set_word_filter( - self.filter.text())) + self.txt_filter.textChanged.connect(lambda: model.set_word_filter( + self.txt_filter.text())) self.table_view.customContextMenuRequested.connect( self._on_context_menu) @@ -489,13 +492,16 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): self._selected_id = None - self.filter = QtWidgets.QLineEdit() - self.filter.setPlaceholderText("Filter representation..") + self.txt_filter = QtWidgets.QLineEdit() + self.txt_filter.setPlaceholderText("Quick filter representation..") + self.txt_filter.setClearButtonEnabled(True) + self.txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"), + QtWidgets.QLineEdit.LeadingPosition) self._scrollbar_pos = None top_bar_layout = QtWidgets.QHBoxLayout() - top_bar_layout.addWidget(self.filter) + top_bar_layout.addWidget(self.txt_filter) table_view = QtWidgets.QTableView() headers = [item[0] for item in self.default_widths] @@ -542,8 +548,8 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): self.table_view = table_view - self.filter.textChanged.connect(lambda: model.set_word_filter( - self.filter.text())) + 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) @@ -975,10 +981,10 @@ class HorizontalHeader(QtWidgets.QHeaderView): QtWidgets.QLineEdit.LeadingPosition) line_edit.setFixedHeight(line_edit.height()) - txt = "Type..." + txt = "" if self.checked_values.get(column_name): txt = list(self.checked_values.get(column_name).keys())[0] - line_edit.setPlaceholderText(txt) + line_edit.setText(txt) action_le = QtWidgets.QWidgetAction(menu) action_le.setDefaultWidget(line_edit)