From 4b8dd165d27ff2240f8690a3cb3fde67abfe3df5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Dec 2021 14:31:56 +0100 Subject: [PATCH 01/59] changed "filesequence" target to "farm" --- .../deadline/plugins/publish/submit_publish_job.py | 2 +- openpype/plugins/publish/collect_rendered_files.py | 3 ++- openpype/plugins/publish/validate_filesequences.py | 3 ++- openpype/pype_commands.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py index 516bd755d0..fdcf12f0d7 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py @@ -232,7 +232,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): 'publish', roothless_metadata_path, "--targets", "deadline", - "--targets", "filesequence" + "--targets", "farm" ] # Generate the payload for Deadline submission diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py index 2f55f2bdb5..1005c38b9d 100644 --- a/openpype/plugins/publish/collect_rendered_files.py +++ b/openpype/plugins/publish/collect_rendered_files.py @@ -21,7 +21,8 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): """ order = pyblish.api.CollectorOrder - 0.2 - targets = ["filesequence"] + # Keep "filesequence" for backwards compatibility of older jobs + targets = ["filesequence", "farm"] label = "Collect rendered frames" _context = None diff --git a/openpype/plugins/publish/validate_filesequences.py b/openpype/plugins/publish/validate_filesequences.py index 2f4ac3de4f..8a877d79bb 100644 --- a/openpype/plugins/publish/validate_filesequences.py +++ b/openpype/plugins/publish/validate_filesequences.py @@ -5,7 +5,8 @@ class ValidateFileSequences(pyblish.api.ContextPlugin): """Validates whether any file sequences were collected.""" order = pyblish.api.ValidatorOrder - targets = ["filesequence"] + # Keep "filesequence" for backwards compatibility of older jobs + targets = ["filesequence", "farm"] label = "Validate File Sequences" def process(self, context): diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 519e7c285b..d5fa2fb63a 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -133,7 +133,7 @@ class PypeCommands: print(f"setting target: {target}") pyblish.api.register_target(target) else: - pyblish.api.register_target("filesequence") + pyblish.api.register_target("farm") os.environ["OPENPYPE_PUBLISH_DATA"] = os.pathsep.join(paths) From ff5bed3a13116d71e83efe3e9c2028574e159c01 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Dec 2021 15:15:18 +0100 Subject: [PATCH 02/59] base of farm cleanup plugin --- openpype/plugins/publish/cleanup_farm.py | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 openpype/plugins/publish/cleanup_farm.py diff --git a/openpype/plugins/publish/cleanup_farm.py b/openpype/plugins/publish/cleanup_farm.py new file mode 100644 index 0000000000..5a41dbbd54 --- /dev/null +++ b/openpype/plugins/publish/cleanup_farm.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +"""Cleanup leftover files from publish.""" +import os +import shutil +import pyblish.api +import avalon.api + + +class CleanUpFarm(pyblish.api.ContextPlugin): + """Cleans up the staging directory after a successful publish. + + This will also clean published renders and delete their parent directories. + """ + + order = pyblish.api.IntegratorOrder + 11 + label = "Clean Up Farm" + enabled = True + + # Keep "filesequence" for backwards compatibility of older jobs + targets = ["filesequence", "farm"] + + def process(self, context): + """Plugin entry point.""" + if avalon.api.Session["AVALON_APP"] != "maya": + self.log.info("Not in farm publish of maya renders. Skipping") + return + + dirpaths_to_remove = set() + for instance in context: + staging_dir = instance.data.get("stagingDir") + if staging_dir: + dirpaths_to_remove.add(os.path.normpath(staging_dir)) + + # clean dirs which are empty + for dirpath in dirpaths_to_remove: + if not os.path.exists(dirpath): + continue + + try: + shutil.rmtree(dirpath) + except OSError: + self.log.warning( + "Failed to remove directory \"{}\"".format(dirpath), + exc_info=True + ) From 84e6f5bb65a3c4395dc318d02a5b5fdce9a7072a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Dec 2021 15:23:39 +0100 Subject: [PATCH 03/59] added few logs and comments --- openpype/plugins/publish/cleanup_farm.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/cleanup_farm.py b/openpype/plugins/publish/cleanup_farm.py index 5a41dbbd54..2ca3926098 100644 --- a/openpype/plugins/publish/cleanup_farm.py +++ b/openpype/plugins/publish/cleanup_farm.py @@ -18,13 +18,21 @@ class CleanUpFarm(pyblish.api.ContextPlugin): # Keep "filesequence" for backwards compatibility of older jobs targets = ["filesequence", "farm"] + allowed_hosts = ("maya", ) def process(self, context): - """Plugin entry point.""" - if avalon.api.Session["AVALON_APP"] != "maya": - self.log.info("Not in farm publish of maya renders. Skipping") + # Get source host from which farm publishing was started + src_host_name = avalon.api.Session.get("AVALON_APP") + # Skip process if is not in list of source hosts in which this + # plugin should run + if src_host_name not in self.allowed_hosts: + self.log.info(( + "Source host \"{}\" is not in list of enabled hosts {}." + " Skipping" + ).format(str(src_host_name), str(self.allowed_hosts))) return + # Collect directories to remove dirpaths_to_remove = set() for instance in context: staging_dir = instance.data.get("stagingDir") @@ -34,8 +42,12 @@ class CleanUpFarm(pyblish.api.ContextPlugin): # clean dirs which are empty for dirpath in dirpaths_to_remove: if not os.path.exists(dirpath): + self.log.debug("Skipping not existing directory \"{}\"".format( + dirpath + )) continue + self.log.debug("Removing directory \"{}\"".format(dirpath)) try: shutil.rmtree(dirpath) except OSError: From 005fa46744000b43afc50e885c2197cd62138a41 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 15:47:34 +0100 Subject: [PATCH 04/59] added few more logs --- openpype/plugins/publish/cleanup_farm.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/plugins/publish/cleanup_farm.py b/openpype/plugins/publish/cleanup_farm.py index 2ca3926098..51bc216e73 100644 --- a/openpype/plugins/publish/cleanup_farm.py +++ b/openpype/plugins/publish/cleanup_farm.py @@ -23,6 +23,7 @@ class CleanUpFarm(pyblish.api.ContextPlugin): def process(self, context): # Get source host from which farm publishing was started src_host_name = avalon.api.Session.get("AVALON_APP") + self.log.debug("Host name from session is {}".format(src_host_name)) # Skip process if is not in list of source hosts in which this # plugin should run if src_host_name not in self.allowed_hosts: @@ -32,6 +33,7 @@ class CleanUpFarm(pyblish.api.ContextPlugin): ).format(str(src_host_name), str(self.allowed_hosts))) return + self.log.debug("Preparing filepaths to remove") # Collect directories to remove dirpaths_to_remove = set() for instance in context: @@ -39,6 +41,14 @@ class CleanUpFarm(pyblish.api.ContextPlugin): if staging_dir: dirpaths_to_remove.add(os.path.normpath(staging_dir)) + if not dirpaths_to_remove: + self.log.info("Nothing to remove. Skipping") + return + + self.log.debug("Filepaths to remove are:\n{}".format( + "\n".join(["- {}".format(path) for path in dirpaths_to_remove]) + )) + # clean dirs which are empty for dirpath in dirpaths_to_remove: if not os.path.exists(dirpath): From 8dbc80c2d7643eae7835c908ec770c4c12730439 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Jan 2022 18:11:10 +0100 Subject: [PATCH 05/59] added few more abstract methods for settings hanler --- openpype/settings/handlers.py | 138 ++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 51e390bb6d..628562afb2 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -105,6 +105,144 @@ class SettingsHandler: """ pass + # Getters for specific version overrides + @abstractmethod + def get_studio_system_settings_overrides_for_version(self, version): + """Studio system settings overrides for specific version. + + Args: + version(str): OpenPype version for which settings should be + returned. + + Returns: + None: If the version does not have system settings overrides. + dict: Document with overrides data. + """ + pass + + @abstractmethod + def get_studio_project_anatomy_overrides_for_version(self, version): + """Studio project anatomy overrides for specific version. + + Args: + version(str): OpenPype version for which settings should be + returned. + + Returns: + None: If the version does not have system settings overrides. + dict: Document with overrides data. + """ + pass + + @abstractmethod + def get_studio_project_settings_overrides_for_version(self, version): + """Studio project settings overrides for specific version. + + Args: + version(str): OpenPype version for which settings should be + returned. + + Returns: + None: If the version does not have system settings overrides. + dict: Document with overrides data. + """ + pass + + @abstractmethod + def get_project_settings_overrides_for_version( + self, project_name, version + ): + """Studio project settings overrides for specific project and version. + + Args: + project_name(str): Name of project for which the overrides should + be loaded. + version(str): OpenPype version for which settings should be + returned. + + Returns: + None: If the version does not have system settings overrides. + dict: Document with overrides data. + """ + pass + + # Clear methods - per version + # - clearing may be helpfull when a version settings were created for + # testing purposes + @abstractmethod + def clear_studio_system_settings_overrides_for_version(self, version): + """Remove studio system settings overrides for specific version. + + If version is not available then skip processing. + """ + pass + + @abstractmethod + def clear_studio_project_settings_overrides_for_version(self, version): + """Remove studio project settings overrides for specific version. + + If version is not available then skip processing. + """ + pass + + @abstractmethod + def clear_studio_project_anatomy_overrides_for_version(self, version): + """Remove studio project anatomy overrides for specific version. + + If version is not available then skip processing. + """ + pass + + @abstractmethod + def clear_project_settings_overrides_for_version( + self, version, project_name + ): + """Remove studio project settings overrides for project and version. + + If version is not available then skip processing. + """ + pass + + # Get versions that are available for each type of settings + @abstractmethod + def get_available_studio_system_settings_overrides_versions(self): + """OpenPype versions that have any studio system settings overrides. + + Returns: + list: OpenPype versions strings. + """ + pass + + @abstractmethod + def get_available_studio_project_anatomy_overrides_versions(self): + """OpenPype versions that have any studio project anatomy overrides. + + Returns: + list: OpenPype versions strings. + """ + pass + + @abstractmethod + def get_available_studio_project_settings_overrides_versions(self): + """OpenPype versions that have any studio project settings overrides. + + Returns: + list: OpenPype versions strings. + """ + pass + + @abstractmethod + def get_available_project_settings_overrides_versions(self, project_name): + """OpenPype versions that have any project settings overrides. + + Args: + project_name(str): Name of project. + + Returns: + list: OpenPype versions strings. + """ + pass + @six.add_metaclass(ABCMeta) class LocalSettingsHandler: From c04f253c0fbfd578a96ee59b0a0840f693bef9ed Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Jan 2022 18:11:42 +0100 Subject: [PATCH 06/59] implemented version based settings in mongo settings handler --- openpype/settings/handlers.py | 261 +++++++++++++++++++++++++++++----- 1 file changed, 222 insertions(+), 39 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 628562afb2..2e511a7dde 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -1,12 +1,13 @@ import os import json import copy -import logging import collections import datetime from abc import ABCMeta, abstractmethod import six -import openpype + +import openpype.version + from .constants import ( GLOBAL_SETTINGS_KEY, SYSTEM_SETTINGS_KEY, @@ -15,9 +16,6 @@ from .constants import ( LOCAL_SETTING_KEY, M_OVERRIDEN_KEY ) -from .lib import load_json_file - -JSON_EXC = getattr(json.decoder, "JSONDecodeError", ValueError) @six.add_metaclass(ABCMeta) @@ -313,6 +311,7 @@ class MongoSettingsHandler(SettingsHandler): "production_version", "staging_version" ) + key_suffix = "_versioned" def __init__(self): # Get mongo connection @@ -326,6 +325,11 @@ class MongoSettingsHandler(SettingsHandler): # TODO prepare version of pype # - pype version should define how are settings saved and loaded + self._system_settings_key = SYSTEM_SETTINGS_KEY + self.key_suffix + self._project_settings_key = PROJECT_SETTINGS_KEY + self.key_suffix + self._project_anatomy_key = PROJECT_ANATOMY_KEY + self.key_suffix + self._current_version = openpype.version.__version__ + database_name = os.environ["OPENPYPE_DATABASE_NAME"] # TODO modify to not use hardcoded keys collection_name = "settings" @@ -496,11 +500,13 @@ class MongoSettingsHandler(SettingsHandler): # Store system settings self.collection.replace_one( { - "type": SYSTEM_SETTINGS_KEY + "type": self._system_settings_key, + "version": self._current_version }, { - "type": SYSTEM_SETTINGS_KEY, - "data": system_settings_data + "type": self._system_settings_key, + "data": system_settings_data, + "version": self._current_version }, upsert=True ) @@ -537,7 +543,7 @@ class MongoSettingsHandler(SettingsHandler): data_cache.update_data(overrides) self._save_project_data( - project_name, PROJECT_SETTINGS_KEY, data_cache + project_name, self._project_settings_key, data_cache ) def save_project_anatomy(self, project_name, anatomy_data): @@ -556,7 +562,7 @@ class MongoSettingsHandler(SettingsHandler): else: self._save_project_data( - project_name, PROJECT_ANATOMY_KEY, data_cache + project_name, self._project_anatomy_key, data_cache ) @classmethod @@ -643,12 +649,14 @@ class MongoSettingsHandler(SettingsHandler): is_default = bool(project_name is None) replace_filter = { "type": doc_type, - "is_default": is_default + "is_default": is_default, + "version": self._current_version } replace_data = { "type": doc_type, "data": data_cache.data, - "is_default": is_default + "is_default": is_default, + "version": self._current_version } if not is_default: replace_filter["project_name"] = project_name @@ -660,24 +668,129 @@ class MongoSettingsHandler(SettingsHandler): upsert=True ) + def _get_objected_version(self, version_str): + from openpype.lib.openpype_version import get_OpenPypeVersion + + OpenPypeVersion = get_OpenPypeVersion() + if OpenPypeVersion is None: + return None + return OpenPypeVersion(version=version_str) + + def _find_closest_settings(self, key, legacy_key, additional_filters=None): + doc_filters = { + "type": {"$in": [key, legacy_key]} + } + if additional_filters: + doc_filters.update(additional_filters) + + other_versions = self.collection.find( + doc_filters, + { + "_id": True, + "version": True, + "type": True + } + ) + legacy_settings_doc = None + versioned_settings = [] + for doc in other_versions: + if doc["type"] == legacy_key: + legacy_settings_doc = doc + else: + versioned_settings.append(doc) + + current_version = None + if versioned_settings: + current_version = self._get_objected_version(self._current_version) + + closest_version_doc = legacy_settings_doc + if current_version is not None: + closest_version = None + for version_doc in versioned_settings: + version = self._get_objected_version(version_doc["version"]) + if version > current_version: + continue + if closest_version is None or closest_version < version: + closest_version = version + closest_version_doc = version_doc + + if not closest_version_doc: + return None + + return self.collection.find_one({"_id": closest_version_doc["_id"]}) + + def _find_closest_system_settings(self): + return self._find_closest_settings( + self._system_settings_key, + SYSTEM_SETTINGS_KEY + ) + + def _find_closest_project_settings(self, project_name): + if project_name is None: + additional_filters = {"is_default": True} + else: + additional_filters = {"project_name": project_name} + + return self._find_closest_settings( + self._project_settings_key, + PROJECT_SETTINGS_KEY, + additional_filters + ) + + def _find_closest_project_anatomy(self): + additional_filters = {"is_default": True} + return self._find_closest_settings( + self._project_anatomy_key, + PROJECT_ANATOMY_KEY, + additional_filters + ) + + def _get_studio_system_settings_overrides_for_version(self, version=None): + if version is None: + version = self._current_version + + return self.collection.find_one({ + "type": self._system_settings_key, + "version": version + }) + + def _get_project_settings_overrides_for_version( + self, project_name, version=None + ): + if version is None: + version = self._current_version + + document_filter = { + "type": self._project_settings_key, + "version": version + } + if project_name is None: + document_filter["is_default"] = True + else: + document_filter["project_name"] = project_name + return self.collection.find_one(document_filter) + + def _get_project_anatomy_overrides_for_version(self, version=None): + if version is None: + version = self._current_version + + return self.collection.find_one({ + "type": self._project_settings_key, + "is_default": True, + "version": version + }) + def get_studio_system_settings_overrides(self): """Studio overrides of system settings.""" if self.system_settings_cache.is_outdated: - system_settings_document = None - globals_document = None - docs = self.collection.find({ - # Use `$or` as system settings may have more filters in future - "$or": [ - {"type": GLOBAL_SETTINGS_KEY}, - {"type": SYSTEM_SETTINGS_KEY}, - ] + globals_document = self.collection.find_one({ + "type": GLOBAL_SETTINGS_KEY }) - for doc in docs: - doc_type = doc["type"] - if doc_type == GLOBAL_SETTINGS_KEY: - globals_document = doc - elif doc_type == SYSTEM_SETTINGS_KEY: - system_settings_document = doc + system_settings_document = ( + self._get_studio_system_settings_overrides_for_version() + ) + if system_settings_document is None: + system_settings_document = self._find_closest_system_settings() merged_document = self._apply_global_settings( system_settings_document, globals_document @@ -688,14 +801,11 @@ class MongoSettingsHandler(SettingsHandler): def _get_project_settings_overrides(self, project_name): if self.project_settings_cache[project_name].is_outdated: - document_filter = { - "type": PROJECT_SETTINGS_KEY, - } - if project_name is None: - document_filter["is_default"] = True - else: - document_filter["project_name"] = project_name - document = self.collection.find_one(document_filter) + document = self._get_project_settings_overrides_for_version( + project_name + ) + if document is None: + document = self._find_closest_project_settings(project_name) self.project_settings_cache[project_name].update_from_document( document ) @@ -761,11 +871,9 @@ class MongoSettingsHandler(SettingsHandler): def _get_project_anatomy_overrides(self, project_name): if self.project_anatomy_cache[project_name].is_outdated: if project_name is None: - document_filter = { - "type": PROJECT_ANATOMY_KEY, - "is_default": True - } - document = self.collection.find_one(document_filter) + document = self._get_project_anatomy_overrides_for_version() + if document is None: + document = self._find_closest_project_anatomy() self.project_anatomy_cache[project_name].update_from_document( document ) @@ -795,6 +903,81 @@ class MongoSettingsHandler(SettingsHandler): return {} return self._get_project_anatomy_overrides(project_name) + # Implementations of abstract methods to get overrides for version + def get_studio_system_settings_overrides_for_version(self, version): + return self._get_studio_system_settings_overrides_for_version(version) + + def get_studio_project_anatomy_overrides_for_version(self, version): + return self._get_project_anatomy_overrides_for_version(version) + + def get_studio_project_settings_overrides_for_version(self, version): + return self._get_project_settings_overrides_for_version(None, version) + + def get_project_settings_overrides_for_version( + self, project_name, version + ): + return self._get_project_settings_overrides_for_version( + project_name, version + ) + + # Implementations of abstract methods to clear overrides for version + def clear_studio_system_settings_overrides_for_version(self, version): + self.collection.delete_one({ + "type": self._system_settings_key, + "version": version + }) + + def clear_studio_project_settings_overrides_for_version(self, version): + self.collection.delete_one({ + "type": self._project_settings_key, + "version": version, + "is_default": True + }) + + def clear_studio_project_anatomy_overrides_for_version(self, version): + self.collection.delete_one({ + "type": self._project_anatomy_key, + "version": version + }) + + def clear_project_settings_overrides_for_version( + self, version, project_name + ): + self.collection.delete_one({ + "type": self._project_settings_key, + "version": version, + "project_name": project_name + }) + + # Get available versions for settings type + def get_available_studio_system_settings_overrides_versions(self): + docs = self.collection.find( + {"type": self._system_settings_key}, + {"version": True} + ) + return {doc["version"] for doc in docs} + + def get_available_studio_project_anatomy_overrides_versions(self): + docs = self.collection.find( + {"type": self._project_anatomy_key}, + {"version": True} + ) + return {doc["version"] for doc in docs} + + def get_available_studio_project_settings_overrides_versions(self): + docs = self.collection.find( + {"type": self._project_settings_key, "is_default": True}, + {"version": True} + ) + return {doc["version"] for doc in docs} + + def get_available_project_settings_overrides_versions(self, project_name): + docs = self.collection.find( + {"type": self._project_settings_key, "project_name": project_name}, + {"version": True} + ) + return {doc["version"] for doc in docs} + class MongoLocalSettingsHandler(LocalSettingsHandler): """Settings handler that use mongo for store and load local settings. From 887fd9aca3115bd8993096bfe0e4e829a737f1e7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Jan 2022 18:47:09 +0100 Subject: [PATCH 07/59] be able to handle situations when OpenPypeVersion is not available --- openpype/settings/handlers.py | 154 +++++++++++++++++++++++++++++----- 1 file changed, 131 insertions(+), 23 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 2e511a7dde..d5d73f235d 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -312,6 +312,7 @@ class MongoSettingsHandler(SettingsHandler): "staging_version" ) key_suffix = "_versioned" + _version_order_key = "versions_order" def __init__(self): # Get mongo connection @@ -322,8 +323,8 @@ class MongoSettingsHandler(SettingsHandler): self._anatomy_keys = None self._attribute_keys = None - # TODO prepare version of pype - # - pype version should define how are settings saved and loaded + + self._version_order_checked = False self._system_settings_key = SYSTEM_SETTINGS_KEY + self.key_suffix self._project_settings_key = PROJECT_SETTINGS_KEY + self.key_suffix @@ -668,21 +669,88 @@ class MongoSettingsHandler(SettingsHandler): upsert=True ) - def _get_objected_version(self, version_str): + def _check_version_order(self): + """This method will work only in OpenPype process. + + Will create/update mongo document where OpenPype versions are stored + in semantic version order. + + This document can be then used to find closes version of settings in + processes where 'OpenPypeVersion' is not available. + """ + # Do this step only once + if self._version_order_checked: + return + self._version_order_checked = True + from openpype.lib.openpype_version import get_OpenPypeVersion OpenPypeVersion = get_OpenPypeVersion() + # Skip if 'OpenPypeVersion' is not available if OpenPypeVersion is None: - return None - return OpenPypeVersion(version=version_str) + return + + # Query document holding sorted list of version strings + doc = self.collection.find_one({"type": self._version_order_key}) + if not doc: + # Just create the document if does not exists yet + self.collection.replace_one( + {"type": self._version_order_key}, + { + "type": self._version_order_key, + "versions": [self._current_version] + }, + upsert=True + ) + return + + # Skip if current version is already available + if self._current_version in doc["versions"]: + return + + # Add all versions into list + objected_versions = [ + OpenPypeVersion(version=self._current_version) + ] + for version_str in doc["versions"]: + objected_versions.append(OpenPypeVersion(version=version_str)) + + # Store version string by their order + new_versions = [] + for version in sorted(objected_versions): + new_versions.append(str(version)) + + # Update versions list and push changes to Mongo + doc["versions"] = new_versions + self.collection.replace_one( + {"type": self._version_order_key}, + doc, + upsert=True + ) def _find_closest_settings(self, key, legacy_key, additional_filters=None): + """Try to find closes available versioned settings for settings key. + + This method should be used only if settings for current OpenPype + version are not available. + + Args: + key(str): Settings key under which are settings stored ("type"). + legacy_key(str): Settings key under which were stored not versioned + settings. + additional_filters(dict): Additional filters of document. Used + for project specific settings. + """ + # Trigger check of versions + self._check_version_order() + doc_filters = { "type": {"$in": [key, legacy_key]} } if additional_filters: doc_filters.update(additional_filters) + # Query base data of each settings doc other_versions = self.collection.find( doc_filters, { @@ -691,33 +759,73 @@ class MongoSettingsHandler(SettingsHandler): "type": True } ) + # Query doc with list of sorted versions + versioned_doc = self.collection.find_one( + {"type": self._version_order_key} + ) + # Separate queried docs legacy_settings_doc = None - versioned_settings = [] + versioned_settings_by_version = {} for doc in other_versions: if doc["type"] == legacy_key: legacy_settings_doc = doc + elif doc["type"] == key: + versioned_settings_by_version[doc["version"]] = doc + + # Cases when only legacy settings can be used + if ( + # There are not versioned documents yet + not versioned_settings_by_version + # Versioned document is not available at all + # - this can happen only if old build of OpenPype was used + or not versioned_doc + # Current OpenPype version is not available + # - something went really wrong when this happens + or self._current_version not in versioned_doc["versions"] + ): + if not legacy_settings_doc: + return None + return self.collection.find_one( + {"_id": legacy_settings_doc["_id"]} + ) + + # Separate versions to lower and higher and keep their order + lower_versions = [] + higher_versions = [] + before = True + for version_str in versioned_doc["versions"]: + if version_str == self._current_version: + before = False + elif before: + lower_versions.append(version_str) else: - versioned_settings.append(doc) + higher_versions.append(version_str) - current_version = None - if versioned_settings: - current_version = self._get_objected_version(self._current_version) + # Use legacy settings doc as source document + src_doc_id = None + if legacy_settings_doc: + src_doc_id = legacy_settings_doc["_id"] - closest_version_doc = legacy_settings_doc - if current_version is not None: - closest_version = None - for version_doc in versioned_settings: - version = self._get_objected_version(version_doc["version"]) - if version > current_version: - continue - if closest_version is None or closest_version < version: - closest_version = version - closest_version_doc = version_doc + # Find highest version which has available settings + if lower_versions: + for version_str in reversed(lower_versions): + doc = versioned_settings_by_version.get(version_str) + if doc: + src_doc_id = doc["_id"] + break - if not closest_version_doc: - return None + # Use versions with higher version only if there are not legacy + # settings and there are not any versions before + if src_doc_id is None and higher_versions: + for version_str in higher_versions: + doc = versioned_settings_by_version.get(version_str) + if doc: + src_doc_id = doc["_id"] + break - return self.collection.find_one({"_id": closest_version_doc["_id"]}) + if src_doc_id is None: + return src_doc_id + return self.collection.find_one({"_id": src_doc_id}) def _find_closest_system_settings(self): return self._find_closest_settings( From 6a03efa7e6010416c9475030b696693fc92beaaa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Jan 2022 11:18:22 +0100 Subject: [PATCH 08/59] added warning image to resources and utils function to load it --- openpype/resources/__init__.py | 9 ++++ openpype/resources/images/warning.png | Bin 0 -> 9393 bytes .../project_manager/project_manager/style.py | 46 +----------------- .../project_manager/widgets.py | 8 +-- openpype/tools/utils/__init__.py | 4 +- openpype/tools/utils/delegates.py | 1 - openpype/tools/utils/lib.py | 18 +++++++ openpype/tools/utils/models.py | 3 -- 8 files changed, 36 insertions(+), 53 deletions(-) create mode 100644 openpype/resources/images/warning.png diff --git a/openpype/resources/__init__.py b/openpype/resources/__init__.py index 34a833d080..49eee21002 100644 --- a/openpype/resources/__init__.py +++ b/openpype/resources/__init__.py @@ -14,6 +14,15 @@ def get_resource(*args): return os.path.normpath(os.path.join(RESOURCES_DIR, *args)) +def get_image_path(*args): + """Helper function to get images. + + Args: + *: Filepath part items. + """ + return get_resource("images", *args) + + def get_liberation_font_path(bold=False, italic=False): font_name = "LiberationSans" suffix = "" diff --git a/openpype/resources/images/warning.png b/openpype/resources/images/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..3b4ae861f9f3982195c82d681d4b3f31cdea9df8 GIT binary patch literal 9393 zcmd6Ni96KY8~1lcLk~rzsBA4BT5P2UWf`TELJB1@Bn_gF-84)2Me?9XL=0Jq>|F88So}F}5(4G3GtP@A|#}!F#=~%Z2kj&i9=AzR&0WeC~Tf%}g(D7v3)nLC|)i zOXsgc5FGpphqelW9}E6ntKi399+z%;L6C?z{~HFyzuOBz2Oy*KXD$5SO%39+6PK`U zbM>ayQXR%$3@pW#-Z8&3PuDjkavI9Mioc>4cE{m(DG z)yUS<#F_bby;lO8en)CmGglttrJ1V??P;*f|3_crPqZ6G=Td(C`ZW`_Zo0Pk;k{kh z`t3DlSi59TW#77AdreqUC~wW&#}==Jt5W3%Ku}4tOT3Zv2+>(=UW)=|4?TQAWPX~I z43d)xH}qjQ2tnCWq9H+A$yW`D15;0Go{7}rBju3$VxNdP;=e=rFGmjO?c&~-ZnPsV z*5aeXD7~q6xs$6jbd4yKzhk6LS2|?2CVnXeAJ#<4i!@9{Msq75eN_{MXyiVjY1&0s zddRxTsv+mE|11sq4V9v6OrbU@&$9IxEk{*JkG7#)wzwGn2xk#aFqH~76dKXjy-lgD z9>gop9tWqm!k!d@62*mnW81^%AuB2>uo`j6xENONPUuW(R@O{}z=Bwz*xXSYI1Y(> z43^nK7@?*FC{?_{`d^*f<)MpvEYI;l5FEOFxb0d>?v8T(>SbH1c4Fl@iwMF<{n-`U z$(c8uVyu0(@M>AfcQfB`iLm6oQ#ZT94F2%Yavfgd_bn}lUU35j^ebxZJ=8?Ms7iQf z(I)-i)m^x2sjOZZ=;TzWz#Q_PT;pG{1l|rgP9j}Z4@!F;G^t3y^*g1i%)(-7r*eeDvL9^!shH=e5l*^gyq z^-G2@(&3_vLob@znyy^A!XXhwpk@nc|ErF273q8OzI|gc;gC9MeG04UBv*#;P3Hz~ zrPG8U^dZrTPK0#H1r6Cqyas0lqI6L!dB60qu{HZ_pj=H8^=~*+NX^poYhsI@qjuX{ z7IO<^E-p0i$~lK1P_obypAm3?69A)F)Yr0YU?HxG`y(5eB%&n5vbMAobCS!lJ+1<4 zC?rZjtS=0$HWjqoM;TUncXxLsNlq4GU9#;}6xK44Zc=eI+q5F_@79n7=s( zrtXi$M z2LJIEE=oh~&imC#r-?%kKV@ZcM%w}pbB0;hG$$(b-f?YU!*50?fh7b3(oO2K2^+Q- zf?xs;EkqJ)l3g-m&m#y4IW06(KnR0u)VZ?ZSEXkFq0FSL{jtfJ|!pxW2S;EJ^GOVxrMr zO)HN39cJa>c_2|`*v#hRm(e*I8tDf~SXFWCBI?Gi*-y%xsjfIOxRC65A^Wq`JKh=` zHJjm9M~lY!7+UUrzpvOm+IDELM3Hd~SpG@IluruQmtH5Jf06owX9Wv+>)SKOT7e}8 zZAAN55bYd8-~Y3K;v;*`bl5)>We0?P`8m)&+4uY06b!1Z zWd*>K504D{XA$bIbpG~c>%iJzbJO?$9a+2M!^0iBUpNJ9SltF@R;G2T6)k5SALvxb z6ec{Sp7oTsK0Zw8@V@ODKlX&R0#7#cG}F0Bxs$%{lx!r;i6#jlfFXad3@)qJY>{qC zQimD~HASC?(S=^48CO1i`sC*%CIvnFIJtTr77{&X#*cTJ6ojz4b*o%aH27wrm_o~k zIsdf@$CKrnm}%&eM9Al8dPvk5Ba>AwaG>2oOS=A!M=?RbXntxj!zGwLFQgv@<{QI8 zUhR6}#Pepa!CofwO}@kbFz3K^>y2R74S||(NkcRVkF7@w%AlmCBm-#d2!o4^!!Cb} zlWk182#wvnS~ZWUCInr)=be%m%1YT{|7BzrpG`2hc+W=%qXLUd$;x6=(fCY)z(vHK z*w3-CZ{Dz-Xo`^fE-9`!JtVwQ2|en_ehgO=;77XK0*Lhedj?|oIEXQY#TE7q6ORqQ zyhcxKO)`fvz}rk>|l2 zr3phr}ii(Bhq6hoV`-c*-guC>45lSm7JbcZ8 zefE7TyJKQvVvugIjiS-*gMCjhI4_f%mDNCE?j=0fFuBGoF!>Ij%Kw=>3D)<%uZq~L zr_+^SdXJ_K-Byzp6)JGCQyv~RLk5DtxWl#)@Q~U>nC*)n<1#;xnEMI0KW0s=_hk=# z)l=)LeAwlK)xuQ6`p&y`0i#=e453p0*svc9c!qBGMaO7L*-U$18w~3Mm_cUCI!&t5DOwVS@Jv$b z4>0>viBECm(R44d{5F1|_q_zbzSX)ccbi~P5Pe=OKi}an4g|V>82vYEMab&X{{sC$ z8(f0qw-M-PASh65{m6AfX?yHfhwf_(-y9`nOP>v_xKeVA00 z_a~HlJUWhyf&%aA$Rf=tV=99saorzCy(0Rm7M4DEw=8!@cUFhQCcyXzv;`ET=-sAd zs5~ey%TH=TX+nt-GwX8AjOKJ1wi$aBHlC$BXF9P0*LdWjXzT2xh1m)7Hg!vG5b3MO z^sbhgt)E+4UMX3gR9!7;RG;)`pMj6R=Ewh7Kqg^(l^D4PhYqyuMQZI2pYRV@V*LDj zXkguais!_=YGb1^+&dVeQcTjBSrlly<+(Lc$$oy?du~5Y0r%MP-o0PC16b}rjp91v zVya{zG9tIP^^a1|R{a^i%L+VE-xy_;*3w#b5q^K3yaO+Q05}mL+<2~NS$>19I=XuH@d3k93W0~oNBH~~Ac||Ib;TpelW7xST0oSskGz?{q z{G&}=tb$(bmKrT!wUoP3UD@fk9)BLXQOtA0pRvD4o3iRr7dX<`o_cZSMfe<*6<+RY zX}L(I(^POWk&ZQ@GAyd^7>OvOU$GH6NBweWU#|E$u}Q8u_}Ua!Z>l-<*VG(~Dfsd} zxAav_6#1ZE{>4J#V8X}>HjMxt7poIj^4y^gDtg(Gjs9MXN-$QKX~FrrTk{HQ^Y+YE zc;}eYX`%JRnD5iwaQW}O; zMpc8u%_y)-YD(ZJV(UL3PuxT55XztX>Bu!QytThel88g-kzU_SLhpy@V%I~Q6 z(?B8~q!}|CwgoJ~mFWeudT#23yh6wXDZ0s1%jUTK-Koc52~||X z5b1!-qH|gR$qAaJcD5;roiW3i( z@jb>_fq(DdyB+OQOxR9}1>1GY1LU?D_XSFLVKDp_uF=F8gXfn~Cpq41b-}XCtgPus zlZkLvcp(i;_THq9?_i9-H9k6t^vPoqMJNuEd2{6~=C)bU4Qy}m-1!J*wqV943Ho=+ zCh#7Ezq}8krgiBwspP?Bx#RZcHqULKA_j=z?Mh`EN!1&Gis!*cu(nm{s!ay@RzO5a zoVmsYr|nLBsnDr(8fp z0VLRlV0;c?yGIIGIXIwQz3jC2n8t?dUFE#Y}7Z@w* zIn^RSr^#7C1wR0{$tZnU4mn4cYZ19?Pm(_I*-gvCS6;9-W%Q^OdmF?p*3^g(qe~4Q zV#1SeFKqhC3tBMabNarY5@#f#K>+@;v?PIKrwwwseJqBx{b4&;;=$PHrmK`7-CYSy z(e#7_V48H0c;Rj0d}^Q2*bUbxc}7F%-yYxOJJ5o@8;iC(KhuQ{29u&e_JxfDGfqdg z{0wJpH*tT!Qi1v}Z}J{-{Sd0*A2(RS+aB;X4fHP?wlO5LXCoEVFXWmGBJ>A-rvTbp z;S{-@Tm|0UqEeqqkJgpa52;75Tfq`yHGaaAWxRDT8ia_CS@-SS_QwkMl@%S7gr@A# zD3rBzKT%gsU#LEK?mRmq-9*z1JaBwyilzyfm9;y2AJF)>i7xdmcY<4;Xu@XC`5J)n zy69KS07AbrE)@_Y_|E|;sn1l_cB9*jGF#|WILg}AmaL6I6VCI+fW3N$1Ta?7h9Gzg zi^|@}*R>#DzbZ)*A!KM>$um`m#*mnTmv$sjfXiN%kX)q7GPJhL{x9Yp<=&lmqC^!XA zzD4W%#<~U1a6GkN=J_-1Y0TYr3k@epsHNg~Q0=fi&B58hcED`&%+Q>GNAy z*ZF=6Ujcr5k?~T5Fm}ob9Qdsx2vCuNI2zhrA%5Zu3!WU{qkzdE$h<&2=A+`__SfJ_ zN^gpYa=7mSji2)fw?pv`V9#E}h>I8}wi|4~%(A-<;U*jZNCmnaSALgHLbwewtjZ)w znxZX^>b|hHDvXPd&*C(YAtlMVli~=s{qbj2pvI3I3|FKqoUfrBYOABjcbqlKp(8HS zs6=ChB{XfXhe&}Hv6-QCsx!O~p??cF?u7lubqiYnbn6-VwFqGsW`pX!gK6%Rngy{c zf2z&~daZ+}Inws!pXK{)@dExa~F;u7@LGA?+QGEVGy*Z`|gg~zO#WWE=i8On)#M=+RC}$x84m3I{7q!pr>b& zbiMNP`4TT|_Bu@J$r#wt<)cYUn~r0ON?cc>0C0r0U^{(aW+$lX$hW$F`5ydaW<>z- z2(@7AzyqVrbmk-`aapPAgRs?Fyq5}eEFZ+4kjP;^KB6%>glBoD0dRG^U~X#dwc2yf zFZgL-+K)dVfh9r{za=5Z$yUyMxh+w(3!r(*=6&0CE|01J6caT!4V=S+7;&OZ9V40& zo0SzsR z2(I7PqzmJlj=MvSBIm~t!NFC>hBr}C=bUQ&`@oZ!YoGqLfx2D+m^|#w;O8@k3Tw*K z8?bWd<6$y@c{PkeN~eQ4*8&!H>lq07+}-oD#%$wrPL_ZxE4|k&lq_rnAR{wjXxke? zV(vO~RHBW9P;%G<%v@p7Twl&A|4(Zu-)*X>m{<|QnLYzL=`Y*D26R>eaF3|(!7q2O zvLByQtT$Y>f`#2cfIA)GtAo)?@wc>`3Zp*CBp^P)yi``>fz4k_=N zQQ5~TXlU9v;5%$7M3vsLB478Wq-#fdoKKzEA@b`H`Ex z^5ouaqG;OD-A5(9{)S6r3ag0H^U$>4=r^_~AC9RLc%sO6<`^!AfXMoM+uC|noIYRC zj$ImD%Ax0lHL9}9{;Mxv>JgYdQ-Kce>!wN>CUAe(HW;&`e%x6^e#H1<|~Ve7CyYeHe5C- zi2LC*_D{{r3~Z@H-MS(Bsrk&RgAIIrA)D!UYJc~Mni^Y5!zY~2;jyLJY^LOOpplNG3AZ~9eem~c7jTu>5j3=)T74rsWBxfhP9Q-l|)k#Ya;$HMwfV6nc=(^$y z-FZiL_4Z1Q{o$hZ`gW}-v#73=&8*JRWL@6&#~h)gU^HR4ZcU(oXGa-%m6p5o zyYKo;Go$m)l}%JRY(zf7Fq3Iw^!_M`kkt*mxs63-Ar2eqPHfgvm;B1oe<4>V?$g6A zLZ5{x z=x~W=^2+Duf+ua*)nZfpN~&a#yt2L6y&0ZTiLR$OwwmzlfKgb_GheY*y)9N|CkPE9 zR$Dqsta%dMcF*OILlSuIg`pB_nJItvX$m|Gu&r%F>2L{xcO@!JVEuPRF5ZJ5YSkF` zlO31=UJjR6M7$@1r~QKC20NxIx~8e~4Bjn|0;41)`{i_#uzg(PwdJqi=3q*+ho*+a z@iVFIB6PVB?@#??e+i7v_byO|-5cchH)b|HVUN^E>}cXz$cKMs=DhUwBbiNfx!5A3 zoBg^0*7KsJy9N9z2r01)OX;o}ptw_(OfX{ud9|{elDk78F^Sz=S6ReMI;BCML~N^C zVE6Eg`L}5JH7`B-I!SohKfUz+($7Vj`xAQT9qnjcR!4Au9WR7gEI3iI-dA{q&^Xy# z)dPtfdH8my+-=nSPRnQ_&!p!wUvqZ^_w?%qGF+b^cGme+6As2QQ2mujR$_e3tx6_e zZE$A1Emj!M>|;gjQTyI;n`(@6ks!{NT23Jch@xgL#TcKS_~ekykm(7d2Pu=uSy|PZ zm;w< zv9-%={VP&|@|XuzM|&DB3j6D(fupgS@w$dr>(zAM>Na--Z>nyT)c;iXCHEN%c7Jjm zpC1N~--iV$ku5pl*uvf~hI6i~<*eQd{cYX8Vl3$ZL-C~Y;;o)zv zu5Np*IEl)nUO^dv*+2FsOVI_C9%R?62K4h)jnAux9pS#JG?bjT@wQeHVq0EOK_1nA zx&i+QHKKd1P1G-fO`-t?2k?J>z@8ipA34~ zC$9O0NfNqIaqC0^n0_E3)Na5XU8C2bVEcUMU^LnAh>e>= z4W!`ZOki}EX zlKI6^!Jn7egv+H(assb?L4_xehEmqrOP`%@2(I-=>QvBE=@r>) z{JSJ(Ih`=tyEqS$TTx6uxw?Mn!eOIrR5~l=bxHfZp>3D?Eu9K`w9xqhw0`;@S^eMK3_YXvTh+n#__(PGI(oeuM>e^LxW7_r5{3en%lbCb(<^t_;= zFq9kZY4$(oqx@&(RPC4327NVLw^I>6V*#3{+gkQ6hJuGr*|z3xD7$a6Ixd&FIRSYo z?BiK`W<9S|=IV-^#4CoM4hz8@!;GgL?ZFw&+a>m{hWZ)H-DWE~b>*41xN=`QYe|ca z+_6)(A5L@Wk^QR9OC&7%t&8Fpb3RSc)g~7W3i~KYNA3I4-tL~~Hj$R6nisDwYe#eE zj7V9!QW@ljM?kwNWc2LNSJ_i8Z;J6}4xqVpoz|NF_u8KteOTmFU(~bxS*Qui24jib^H3d`u&Bbc(ZG?XhIquxWgcrms8 zky41R=B-1Re)pHVaBAx{#1}QpN3fcQF4M(M)GC@H+Lc!5cd{b^tGs&7`wm&PJ_V?C zyI6~Js^2~5ubi~qoiK10u`NbEQ%Z)WR~q+}FJtPeEGYU$QD@9JMtK_zKANiUQQ4p3 z^g4N?i5pYBRbTaL`kn#dX-`*FH;O75+Ni&Cr0<4|!1OFBJB*?#Kq1)cUm0g!Sv1Y< zjr>%Rmcd4UPmk7@;qOrsrK=N__OLhUZ#r||zz)q4_ei0oa8~Urd7oM{Gy+xFs*VlIKDlvH5&6P$N<#pejPgBX1 zs&c@p426e|H8&~X+^d7UR=pW;vPLh&wqW_pAufkCAF+KTWo;BL$Tt+Iv?H50W;CbL zgAN)^_)W=a)4+he+Ft2@L*~O-KY&3>r1&K!c0hwkeO*8b0rpCMF(33s>`1iJ8au_9 zGib5r@|i^HzWKYrGYO#QF|gs78qg{_jC+h{pKegD7g$xL2l1Wn30p2^&4&+wZr9!a zjjZ!WtU%pFPHf^hogX9@Ju@B$06M41d3;x7UiB# zQE9D9$eTBAFru-!1>3>u?Qp@1QC(B(*6e^U8acVS44GI0O;Qa+u&9L7SLQ~#1CK;O zgGgo$s!7s44`RD}y_}cfa)U%W;d@Z_C-SSTywUa2`!$7b>=mj?aSc{n?2r1!buv~h z;_W}pS#uBuy7Dsk&_E3#B}#se4d%xAkV2Yr#u3@^cF?EwFp7GS!+rAyf0|R}AUFb_ zwbJ=s3=cMrvOpoDu%;&rs_*5?yWI8ESR=FMLr*^!;zrZ!6pW5#5L@+P<5K;Hi8kx0 z_@QWB|ERQg)jQp{B41%;Ujacs6B@!3LTpQscZr}fcp>ctEU)M1J^xy^-&V)_Dr;!e z@%Vm?@#D@@LrK_mGlJOTVFA2S@CZ?rBMy)1V)l&8@|-pyLPpr!vODO6G@$4&FGpoq zv8HPFaG$Zff}iH#C2MPTlH+yrqta~s%DK^YpczA^Geu+ou`4}~*A8aJzy+ZYJM7Y9nIt2Tk}7OTbrLk zy~D~dDdx)|q18x*oHNBJ^VQD+uz^FQJ9|KS(q{W8z-$sein93KV#_KYr=p3gmJ H|MdR=mHnnz literal 0 HcmV?d00001 diff --git a/openpype/tools/project_manager/project_manager/style.py b/openpype/tools/project_manager/project_manager/style.py index 9fa7a5520b..d24fc7102f 100644 --- a/openpype/tools/project_manager/project_manager/style.py +++ b/openpype/tools/project_manager/project_manager/style.py @@ -1,8 +1,8 @@ import os from Qt import QtCore, QtGui -from openpype.style import get_objected_colors from avalon.vendor import qtawesome +from openpype.tools.utils import paint_image_with_color class ResourceCache: @@ -91,17 +91,6 @@ class ResourceCache: icon.addPixmap(disabled_pix, QtGui.QIcon.Disabled, QtGui.QIcon.Off) return icon - @classmethod - def get_warning_pixmap(cls): - src_image = get_warning_image() - colors = get_objected_colors() - color_value = colors["delete-btn-bg"] - - return paint_image_with_color( - src_image, - color_value.get_qcolor() - ) - def get_remove_image(): image_path = os.path.join( @@ -110,36 +99,3 @@ def get_remove_image(): "bin.png" ) return QtGui.QImage(image_path) - - -def get_warning_image(): - image_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "images", - "warning.png" - ) - return QtGui.QImage(image_path) - - -def paint_image_with_color(image, color): - """TODO: This function should be imported from utils. - - At the moment of creation is not available yet. - """ - width = image.width() - height = image.height() - - alpha_mask = image.createAlphaMask() - alpha_region = QtGui.QRegion(QtGui.QBitmap.fromImage(alpha_mask)) - - pixmap = QtGui.QPixmap(width, height) - pixmap.fill(QtCore.Qt.transparent) - - painter = QtGui.QPainter(pixmap) - painter.setClipRegion(alpha_region) - painter.setPen(QtCore.Qt.NoPen) - painter.setBrush(color) - painter.drawRect(QtCore.QRect(0, 0, width, height)) - painter.end() - - return pixmap diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 4b5aca35ef..ebf344b387 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -4,14 +4,16 @@ from .constants import ( NAME_ALLOWED_SYMBOLS, NAME_REGEX ) -from .style import ResourceCache from openpype.lib import ( create_project, PROJECT_NAME_ALLOWED_SYMBOLS, PROJECT_NAME_REGEX ) from openpype.style import load_stylesheet -from openpype.tools.utils import PlaceholderLineEdit +from openpype.tools.utils import ( + PlaceholderLineEdit, + get_warning_pixmap +) from avalon.api import AvalonMongoDB from Qt import QtWidgets, QtCore, QtGui @@ -338,7 +340,7 @@ class ConfirmProjectDeletion(QtWidgets.QDialog): top_widget = QtWidgets.QWidget(self) - warning_pixmap = ResourceCache.get_warning_pixmap() + warning_pixmap = get_warning_pixmap() warning_icon_label = PixmapLabel(warning_pixmap, top_widget) message_label = QtWidgets.QLabel(top_widget) diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index eb0cb1eef5..9363007532 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -8,7 +8,8 @@ from .widgets import ( from .error_dialog import ErrorMessageBox from .lib import ( WrappedCallbackItem, - paint_image_with_color + paint_image_with_color, + get_warning_pixmap ) @@ -22,4 +23,5 @@ __all__ = ( "WrappedCallbackItem", "paint_image_with_color", + "get_warning_pixmap", ) diff --git a/openpype/tools/utils/delegates.py b/openpype/tools/utils/delegates.py index 1caed732d8..4ec6079bb7 100644 --- a/openpype/tools/utils/delegates.py +++ b/openpype/tools/utils/delegates.py @@ -7,7 +7,6 @@ import Qt from Qt import QtWidgets, QtGui, QtCore from avalon.lib import HeroVersionType -from openpype.style import get_objected_colors from .models import TreeModel from . import lib diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index caaad522ad..76ad944ce5 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -14,6 +14,8 @@ from openpype.api import ( Logger ) from openpype.lib import filter_profiles +from openpype.style import get_objected_colors +from openpype.resources import get_image_path def center_window(window): @@ -670,3 +672,19 @@ class WrappedCallbackItem: finally: self._done = True + + +def get_warning_pixmap(color=None): + """Warning icon as QPixmap. + + Args: + color(QtGui.QColor): Color that will be used to paint warning icon. + """ + src_image_path = get_image_path("warning.png") + src_image = QtGui.QImage(src_image_path) + if color is None: + colors = get_objected_colors() + color_value = colors["delete-btn-bg"] + color = color_value.get_qcolor() + + return paint_image_with_color(src_image, color) diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py index df3eee41a2..74d6c304c2 100644 --- a/openpype/tools/utils/models.py +++ b/openpype/tools/utils/models.py @@ -3,9 +3,6 @@ import logging import Qt from Qt import QtCore, QtGui -from avalon.vendor import qtawesome -from avalon import style, io -from . import lib from .constants import ( PROJECT_IS_ACTIVE_ROLE, PROJECT_NAME_ROLE, From 87ac7686c574d1d02a188411c52473588621170a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Jan 2022 11:24:43 +0100 Subject: [PATCH 09/59] show dialog telling that old build of openpype is used --- openpype/tools/tray/pype_tray.py | 74 +++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 99d431172a..bc1eeaad90 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -33,7 +33,8 @@ from openpype.settings import ( ) from openpype.tools.utils import ( WrappedCallbackItem, - paint_image_with_color + paint_image_with_color, + get_warning_pixmap ) from .pype_info_widget import PypeInfoWidget @@ -76,7 +77,7 @@ class PixmapLabel(QtWidgets.QLabel): super(PixmapLabel, self).resizeEvent(event) -class VersionDialog(QtWidgets.QDialog): +class VersionUpdateDialog(QtWidgets.QDialog): restart_requested = QtCore.Signal() ignore_requested = QtCore.Signal() @@ -84,7 +85,7 @@ class VersionDialog(QtWidgets.QDialog): _min_height = 130 def __init__(self, parent=None): - super(VersionDialog, self).__init__(parent) + super(VersionUpdateDialog, self).__init__(parent) icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) @@ -152,11 +153,11 @@ class VersionDialog(QtWidgets.QDialog): ) def showEvent(self, event): - super().showEvent(event) + super(VersionUpdateDialog, self).showEvent(event) self._restart_accepted = False def closeEvent(self, event): - super().closeEvent(event) + super(VersionUpdateDialog, self).closeEvent(event) if self._restart_accepted or self._current_is_higher: return # Trigger ignore requested only if restart was not clicked and current @@ -202,6 +203,63 @@ class VersionDialog(QtWidgets.QDialog): self.accept() +class BuildVersionDialog(QtWidgets.QDialog): + """Build/Installation version is too low for current OpenPype version. + + This dialog tells to user that it's build OpenPype is too old. + """ + def __init__(self, parent=None): + super(BuildVersionDialog, self).__init__(parent) + + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowTitle("Outdated OpenPype installation") + self.setWindowFlags( + self.windowFlags() + | QtCore.Qt.WindowStaysOnTopHint + ) + + top_widget = QtWidgets.QWidget(self) + + warning_pixmap = get_warning_pixmap() + warning_icon_label = PixmapLabel(warning_pixmap, top_widget) + + message = ( + "Your installation of OpenPype does not match minimum" + " requirements.

Please update OpenPype installation" + " to newer version." + ) + content_label = QtWidgets.QLabel(message, self) + + top_layout = QtWidgets.QHBoxLayout(top_widget) + top_layout.setContentsMargins(0, 0, 0, 0) + top_layout.addWidget( + warning_icon_label, 0, + QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter + ) + top_layout.addWidget(content_label, 1) + + footer_widget = QtWidgets.QWidget(self) + ok_btn = QtWidgets.QPushButton("I understand", footer_widget) + + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addStretch(1) + footer_layout.addWidget(ok_btn) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(top_widget, 0) + main_layout.addStretch(1) + main_layout.addWidget(footer_widget, 0) + + self.setStyleSheet(style.load_stylesheet()) + + ok_btn.clicked.connect(self._on_ok_clicked) + + def _on_ok_clicked(self): + self.close() + + class TrayManager: """Cares about context of application. @@ -272,7 +330,7 @@ class TrayManager: return if self._version_dialog is None: - self._version_dialog = VersionDialog() + self._version_dialog = VersionUpdateDialog() self._version_dialog.restart_requested.connect( self._restart_and_install ) @@ -383,6 +441,10 @@ class TrayManager: self._validate_settings_defaults() + if not op_version_control_available(): + dialog = BuildVersionDialog() + dialog.exec_() + def _validate_settings_defaults(self): valid = True try: From 000e97f874a3492530d1c39fe1388c261a460517 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 28 Jan 2022 18:15:22 +0100 Subject: [PATCH 10/59] enhanced settings to be able define source version of settings in UI and see source version --- openpype/settings/__init__.py | 4 + openpype/settings/constants.py | 2 + openpype/settings/entities/root_entities.py | 175 ++++++- openpype/settings/handlers.py | 483 ++++++++++++++---- openpype/settings/lib.py | 141 ++++- openpype/style/style.css | 13 + .../tools/settings/settings/categories.py | 240 ++++++--- openpype/tools/settings/settings/constants.py | 4 +- openpype/tools/settings/settings/widgets.py | 203 +++++++- openpype/tools/settings/settings/window.py | 24 +- openpype/tools/utils/__init__.py | 4 +- openpype/tools/utils/lib.py | 12 + 12 files changed, 1067 insertions(+), 238 deletions(-) diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index 9d7598a948..14e4678050 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -5,6 +5,8 @@ from .constants import ( PROJECT_ANATOMY_KEY, LOCAL_SETTING_KEY, + LEGACY_SETTINGS_VERSION, + SCHEMA_KEY_SYSTEM_SETTINGS, SCHEMA_KEY_PROJECT_SETTINGS, @@ -37,6 +39,8 @@ __all__ = ( "PROJECT_ANATOMY_KEY", "LOCAL_SETTING_KEY", + "LEGACY_SETTINGS_VERSION", + "SCHEMA_KEY_SYSTEM_SETTINGS", "SCHEMA_KEY_PROJECT_SETTINGS", diff --git a/openpype/settings/constants.py b/openpype/settings/constants.py index 2ea19ead4b..f0e8ba78c8 100644 --- a/openpype/settings/constants.py +++ b/openpype/settings/constants.py @@ -21,6 +21,8 @@ PROJECT_SETTINGS_KEY = "project_settings" PROJECT_ANATOMY_KEY = "project_anatomy" LOCAL_SETTING_KEY = "local_settings" +LEGACY_SETTINGS_VERSION = "legacy" + # Schema hub names SCHEMA_KEY_SYSTEM_SETTINGS = "system_schema" SCHEMA_KEY_PROJECT_SETTINGS = "projects_schema" diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 687784a359..d35a990284 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -19,6 +19,7 @@ from .exceptions import ( SchemaError, InvalidKeySymbols ) +from openpype.lib import get_openpype_version from openpype.settings.constants import ( SYSTEM_SETTINGS_KEY, PROJECT_SETTINGS_KEY, @@ -34,15 +35,24 @@ from openpype.settings.lib import ( reset_default_settings, get_studio_system_settings_overrides, + get_studio_system_settings_overrides_for_version, save_studio_settings, + get_available_studio_system_settings_overrides_versions, get_studio_project_settings_overrides, + get_studio_project_settings_overrides_for_version, get_studio_project_anatomy_overrides, + get_studio_project_anatomy_overrides_for_version, get_project_settings_overrides, + get_project_settings_overrides_for_version, get_project_anatomy_overrides, save_project_settings, save_project_anatomy, + get_available_project_settings_overrides_versions, + get_available_studio_project_settings_overrides_versions, + get_available_studio_project_anatomy_overrides_versions, + find_environments, apply_overrides ) @@ -495,17 +505,27 @@ class SystemSettings(RootEntity): root_key = SYSTEM_SETTINGS_KEY def __init__( - self, set_studio_state=True, reset=True, schema_hub=None + self, + set_studio_state=True, + reset=True, + schema_hub=None, + source_version=None ): if schema_hub is None: # Load system schemas schema_hub = SchemasHub(SCHEMA_KEY_SYSTEM_SETTINGS) + self._source_version = source_version + super(SystemSettings, self).__init__(schema_hub, reset) if set_studio_state: self.set_studio_state() + @property + def source_version(self): + return self._source_version + def get_entity_from_path(self, path): """Return system settings entity.""" path_parts = path.split("/") @@ -524,12 +544,24 @@ class SystemSettings(RootEntity): value = default_value.get(key, NOT_SET) child_obj.update_default_value(value) - studio_overrides = get_studio_system_settings_overrides() + if self._source_version is None: + studio_overrides, version = get_studio_system_settings_overrides( + return_version=True + ) + self._source_version = version + + else: + studio_overrides = ( + get_studio_system_settings_overrides_for_version( + self._source_version + ) + ) + for key, child_obj in self.non_gui_children.items(): value = studio_overrides.get(key, NOT_SET) child_obj.update_studio_value(value) - def reset(self, new_state=None): + def reset(self, new_state=None, source_version=None): """Discard changes and reset entit's values. Reload default values and studio override values and update entities. @@ -547,9 +579,22 @@ class SystemSettings(RootEntity): if new_state is OverrideState.PROJECT: raise ValueError("System settings can't store poject overrides.") + if source_version is not None: + self._source_version = source_version + self._reset_values() self.set_override_state(new_state) + def get_available_source_versions(self, sorted=None): + if self.is_in_studio_state(): + return self.get_available_studio_versions(sorted=sorted) + return [] + + def get_available_studio_versions(self, sorted=None): + return get_available_studio_system_settings_overrides_versions( + sorted=sorted + ) + def defaults_dir(self): """Path to defaults directory. @@ -566,6 +611,8 @@ class SystemSettings(RootEntity): json.dumps(settings_value, indent=4) )) save_studio_settings(settings_value) + # Reset source version after restart + self._source_version = None def _validate_defaults_to_save(self, value): """Valiations of default values before save.""" @@ -622,11 +669,15 @@ class ProjectSettings(RootEntity): project_name=None, change_state=True, reset=True, - schema_hub=None + schema_hub=None, + source_version=None, + anatomy_source_version=None ): self._project_name = project_name self._system_settings_entity = None + self._source_version = source_version + self._anatomy_source_version = anatomy_source_version if schema_hub is None: # Load system schemas @@ -640,6 +691,14 @@ class ProjectSettings(RootEntity): else: self.set_project_state() + @property + def source_version(self): + return self._source_version + + @property + def anatomy_source_version(self): + return self._anatomy_source_version + @property def project_name(self): return self._project_name @@ -682,23 +741,20 @@ class ProjectSettings(RootEntity): output = output[path_part] return output - def change_project(self, project_name): + def change_project(self, project_name, source_version=None): if project_name == self._project_name: - return + if ( + source_version is None + or source_version == self._source_version + ): + if not self.is_in_project_state(): + self.set_project_state() + return - self._project_name = project_name - if project_name is None: - self.set_studio_state() - return - - project_override_value = { - PROJECT_SETTINGS_KEY: get_project_settings_overrides(project_name), - PROJECT_ANATOMY_KEY: get_project_anatomy_overrides(project_name) - } - for key, child_obj in self.non_gui_children.items(): - value = project_override_value.get(key, NOT_SET) - child_obj.update_project_value(value) + self._source_version = source_version + self._anatomy_source_version = None + self._set_values_for_project(project_name) self.set_project_state() def _reset_values(self): @@ -710,27 +766,97 @@ class ProjectSettings(RootEntity): value = default_values.get(key, NOT_SET) child_obj.update_default_value(value) + self._set_values_for_project(self.project_name) + + def _set_values_for_project(self, project_name): + self._project_name = project_name + if project_name: + project_settings_overrides = ( + get_studio_project_settings_overrides() + ) + project_anatomy_overrides = ( + get_studio_project_anatomy_overrides() + ) + else: + if self._source_version is None: + project_settings_overrides, version = ( + get_studio_project_settings_overrides(return_version=True) + ) + self._source_version = version + else: + project_settings_overrides = ( + get_studio_project_settings_overrides_for_version( + self._source_version + ) + ) + + if self._anatomy_source_version is None: + project_anatomy_overrides, anatomy_version = ( + get_studio_project_anatomy_overrides(return_version=True) + ) + self._anatomy_source_version = anatomy_version + else: + project_anatomy_overrides = ( + get_studio_project_anatomy_overrides_for_version( + self._anatomy_source_version + ) + ) + studio_overrides = { - PROJECT_SETTINGS_KEY: get_studio_project_settings_overrides(), - PROJECT_ANATOMY_KEY: get_studio_project_anatomy_overrides() + PROJECT_SETTINGS_KEY: project_settings_overrides, + PROJECT_ANATOMY_KEY: project_anatomy_overrides } for key, child_obj in self.non_gui_children.items(): value = studio_overrides.get(key, NOT_SET) child_obj.update_studio_value(value) - if not self.project_name: + if not project_name: return - project_name = self.project_name + if self._source_version is None: + project_settings_overrides, version = ( + get_project_settings_overrides( + project_name, return_version=True + ) + ) + self._source_version = version + else: + project_settings_overrides = ( + get_project_settings_overrides_for_version( + project_name, self._source_version + ) + ) + project_override_value = { - PROJECT_SETTINGS_KEY: get_project_settings_overrides(project_name), + PROJECT_SETTINGS_KEY: project_settings_overrides, PROJECT_ANATOMY_KEY: get_project_anatomy_overrides(project_name) } for key, child_obj in self.non_gui_children.items(): value = project_override_value.get(key, NOT_SET) child_obj.update_project_value(value) + def get_available_source_versions(self, sorted=None): + if self.is_in_studio_state(): + return self.get_available_studio_versions(sorted=sorted) + elif self.is_in_project_state(): + return get_available_project_settings_overrides_versions( + self.project_name, sorted=sorted + ) + return [] + + def get_available_studio_versions(self, sorted=None): + return get_available_studio_project_settings_overrides_versions( + sorted=sorted + ) + + def get_available_anatomy_source_versions(self, sorted=None): + if self.is_in_studio_state(): + return get_available_studio_project_anatomy_overrides_versions( + sorted=sorted + ) + return [] + def reset(self, new_state=None): """Discard changes and reset entit's values. @@ -763,6 +889,9 @@ class ProjectSettings(RootEntity): self._validate_values_to_save(settings_value) + self._source_version = None + self._anatomy_source_version = None + self.log.debug("Saving project settings: {}".format( json.dumps(settings_value, indent=4) )) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index d5d73f235d..ac477c9f9b 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -14,7 +14,9 @@ from .constants import ( PROJECT_SETTINGS_KEY, PROJECT_ANATOMY_KEY, LOCAL_SETTING_KEY, - M_OVERRIDEN_KEY + M_OVERRIDEN_KEY, + + LEGACY_SETTINGS_VERSION ) @@ -65,26 +67,27 @@ class SettingsHandler: pass @abstractmethod - def get_studio_system_settings_overrides(self): + def get_studio_system_settings_overrides(self, return_version): """Studio overrides of system settings.""" pass @abstractmethod - def get_studio_project_settings_overrides(self): + def get_studio_project_settings_overrides(self, return_version): """Studio overrides of default project settings.""" pass @abstractmethod - def get_studio_project_anatomy_overrides(self): + def get_studio_project_anatomy_overrides(self, return_version): """Studio overrides of default project anatomy data.""" pass @abstractmethod - def get_project_settings_overrides(self, project_name): + def get_project_settings_overrides(self, project_name, return_version): """Studio overrides of project settings for specific project. Args: project_name(str): Name of project for which data should be loaded. + return_version(bool): Version string will be added to output. Returns: dict: Only overrides for entered project, may be empty dictionary. @@ -92,11 +95,12 @@ class SettingsHandler: pass @abstractmethod - def get_project_anatomy_overrides(self, project_name): + def get_project_anatomy_overrides(self, project_name, return_version): """Studio overrides of project anatomy for specific project. Args: project_name(str): Name of project for which data should be loaded. + return_version(bool): Version string will be added to output. Returns: dict: Only overrides for entered project, may be empty dictionary. @@ -203,7 +207,9 @@ class SettingsHandler: # Get versions that are available for each type of settings @abstractmethod - def get_available_studio_system_settings_overrides_versions(self): + def get_available_studio_system_settings_overrides_versions( + self, sorted=None + ): """OpenPype versions that have any studio system settings overrides. Returns: @@ -212,7 +218,9 @@ class SettingsHandler: pass @abstractmethod - def get_available_studio_project_anatomy_overrides_versions(self): + def get_available_studio_project_anatomy_overrides_versions( + self, sorted=None + ): """OpenPype versions that have any studio project anatomy overrides. Returns: @@ -221,7 +229,9 @@ class SettingsHandler: pass @abstractmethod - def get_available_studio_project_settings_overrides_versions(self): + def get_available_studio_project_settings_overrides_versions( + self, sorted=None + ): """OpenPype versions that have any studio project settings overrides. Returns: @@ -230,7 +240,9 @@ class SettingsHandler: pass @abstractmethod - def get_available_project_settings_overrides_versions(self, project_name): + def get_available_project_settings_overrides_versions( + self, project_name, sorted=None + ): """OpenPype versions that have any project settings overrides. Args: @@ -270,17 +282,20 @@ class CacheValues: def __init__(self): self.data = None self.creation_time = None + self.version = None def data_copy(self): if not self.data: return {} return copy.deepcopy(self.data) - def update_data(self, data): + def update_data(self, data, version=None): self.data = data self.creation_time = datetime.datetime.now() + if version is not None: + self.version = version - def update_from_document(self, document): + def update_from_document(self, document, version=None): data = {} if document: if "data" in document: @@ -290,6 +305,8 @@ class CacheValues: if value: data = json.loads(value) self.data = data + if version is not None: + self.version = version def to_json_string(self): return json.dumps(self.data or {}) @@ -313,6 +330,9 @@ class MongoSettingsHandler(SettingsHandler): ) key_suffix = "_versioned" _version_order_key = "versions_order" + _all_versions_keys = "all_versions" + _production_versions_key = "production_versions" + _staging_versions_key = "staging_versions" def __init__(self): # Get mongo connection @@ -488,7 +508,7 @@ class MongoSettingsHandler(SettingsHandler): data(dict): Data of studio overrides with override metadata. """ # Update cache - self.system_settings_cache.update_data(data) + self.system_settings_cache.update_data(data, self._current_version) # Get copy of just updated cache system_settings_data = self.system_settings_cache.data_copy() @@ -541,7 +561,7 @@ class MongoSettingsHandler(SettingsHandler): data(dict): Data of project overrides with override metadata. """ data_cache = self.project_settings_cache[project_name] - data_cache.update_data(overrides) + data_cache.update_data(overrides, self._current_version) self._save_project_data( project_name, self._project_settings_key, data_cache @@ -556,7 +576,7 @@ class MongoSettingsHandler(SettingsHandler): data(dict): Data of project overrides with override metadata. """ data_cache = self.project_anatomy_cache[project_name] - data_cache.update_data(anatomy_data) + data_cache.update_data(anatomy_data, self._current_version) if project_name is not None: self._save_project_anatomy_data(project_name, data_cache) @@ -669,6 +689,13 @@ class MongoSettingsHandler(SettingsHandler): upsert=True ) + def _get_versions_order_doc(self, projection=None): + # TODO cache + return self.collection.find_one( + {"type": self._version_order_key}, + projection + ) + def _check_version_order(self): """This method will work only in OpenPype process. @@ -683,7 +710,10 @@ class MongoSettingsHandler(SettingsHandler): return self._version_order_checked = True - from openpype.lib.openpype_version import get_OpenPypeVersion + from openpype.lib.openpype_version import ( + get_OpenPypeVersion, + is_running_staging + ) OpenPypeVersion = get_OpenPypeVersion() # Skip if 'OpenPypeVersion' is not available @@ -691,44 +721,104 @@ class MongoSettingsHandler(SettingsHandler): return # Query document holding sorted list of version strings - doc = self.collection.find_one({"type": self._version_order_key}) + doc = self._get_versions_order_doc() if not doc: - # Just create the document if does not exists yet - self.collection.replace_one( - {"type": self._version_order_key}, - { - "type": self._version_order_key, - "versions": [self._current_version] - }, - upsert=True - ) - return + doc = {"type": self._version_order_key} + + if self._production_versions_key not in doc: + doc[self._production_versions_key] = [] + + if self._staging_versions_key not in doc: + doc[self._staging_versions_key] = [] + + if self._all_versions_keys not in doc: + doc[self._all_versions_keys] = [] + + if is_running_staging(): + versions_key = self._staging_versions_key + else: + versions_key = self._production_versions_key # Skip if current version is already available - if self._current_version in doc["versions"]: + if ( + self._current_version in doc[self._all_versions_keys] + and self._current_version in doc[versions_key] + ): return - # Add all versions into list - objected_versions = [ - OpenPypeVersion(version=self._current_version) - ] - for version_str in doc["versions"]: - objected_versions.append(OpenPypeVersion(version=version_str)) + if self._current_version not in doc[self._all_versions_keys]: + # Add all versions into list + all_objected_versions = [ + OpenPypeVersion(version=self._current_version) + ] + for version_str in doc[self._all_versions_keys]: + all_objected_versions.append( + OpenPypeVersion(version=version_str) + ) - # Store version string by their order - new_versions = [] - for version in sorted(objected_versions): - new_versions.append(str(version)) + doc[self._all_versions_keys] = [ + str(version) for version in sorted(all_objected_versions) + ] + + if self._current_version not in doc[versions_key]: + objected_versions = [ + OpenPypeVersion(version=self._current_version) + ] + for version_str in doc[versions_key]: + objected_versions.append(OpenPypeVersion(version=version_str)) + + # Update versions list and push changes to Mongo + doc[versions_key] = [ + str(version) for version in sorted(objected_versions) + ] - # Update versions list and push changes to Mongo - doc["versions"] = new_versions self.collection.replace_one( {"type": self._version_order_key}, doc, upsert=True ) - def _find_closest_settings(self, key, legacy_key, additional_filters=None): + def find_closest_version_for_projects(self, project_names): + output = { + project_name: None + for project_name in project_names + } + from openpype.lib.openpype_version import get_OpenPypeVersion + OpenPypeVersion = get_OpenPypeVersion() + if OpenPypeVersion is None: + return output + + versioned_doc = self._get_versions_order_doc() + + settings_ids = [] + for project_name in project_names: + if project_name is None: + doc_filter = {"is_default": True} + else: + doc_filter = {"project_name": project_name} + settings_id = self._find_closest_settings_id( + self._project_settings_key, + PROJECT_SETTINGS_KEY, + doc_filter, + versioned_doc + ) + if settings_id: + settings_ids.append(settings_id) + + if settings_ids: + docs = self.collection.find( + {"_id": {"$in": settings_ids}}, + {"version": True, "project_name": True} + ) + for doc in docs: + project_name = doc.get("project_name") + version = doc.get("version", LEGACY_SETTINGS_VERSION) + output[project_name] = version + return output + + def _find_closest_settings_id( + self, key, legacy_key, additional_filters=None, versioned_doc=None + ): """Try to find closes available versioned settings for settings key. This method should be used only if settings for current OpenPype @@ -741,6 +831,8 @@ class MongoSettingsHandler(SettingsHandler): additional_filters(dict): Additional filters of document. Used for project specific settings. """ + from openpype.lib.openpype_version import is_running_staging + # Trigger check of versions self._check_version_order() @@ -760,9 +852,9 @@ class MongoSettingsHandler(SettingsHandler): } ) # Query doc with list of sorted versions - versioned_doc = self.collection.find_one( - {"type": self._version_order_key} - ) + if versioned_doc is None: + versioned_doc = self._get_versions_order_doc() + # Separate queried docs legacy_settings_doc = None versioned_settings_by_version = {} @@ -770,8 +862,16 @@ class MongoSettingsHandler(SettingsHandler): if doc["type"] == legacy_key: legacy_settings_doc = doc elif doc["type"] == key: + if doc["version"] == self._current_version: + return doc["_id"] versioned_settings_by_version[doc["version"]] = doc + if is_running_staging(): + versions_key = self._staging_versions_key + else: + versions_key = self._production_versions_key + + versions_in_doc = versioned_doc.get(versions_key) or [] # Cases when only legacy settings can be used if ( # There are not versioned documents yet @@ -781,19 +881,17 @@ class MongoSettingsHandler(SettingsHandler): or not versioned_doc # Current OpenPype version is not available # - something went really wrong when this happens - or self._current_version not in versioned_doc["versions"] + or self._current_version not in versions_in_doc ): if not legacy_settings_doc: return None - return self.collection.find_one( - {"_id": legacy_settings_doc["_id"]} - ) + return legacy_settings_doc["_id"] # Separate versions to lower and higher and keep their order lower_versions = [] higher_versions = [] before = True - for version_str in versioned_doc["versions"]: + for version_str in versions_in_doc: if version_str == self._current_version: before = False elif before: @@ -823,9 +921,18 @@ class MongoSettingsHandler(SettingsHandler): src_doc_id = doc["_id"] break - if src_doc_id is None: - return src_doc_id - return self.collection.find_one({"_id": src_doc_id}) + return src_doc_id + + def _find_closest_settings( + self, key, legacy_key, additional_filters=None, versioned_doc=None + ): + doc_id = self._find_closest_settings_id( + key, legacy_key, additional_filters, versioned_doc + ) + if doc_id is None: + return None + return self.collection.find_one({"_id": doc_id}) + def _find_closest_system_settings(self): return self._find_closest_settings( @@ -854,6 +961,12 @@ class MongoSettingsHandler(SettingsHandler): ) def _get_studio_system_settings_overrides_for_version(self, version=None): + # QUESTION cache? + if version == LEGACY_SETTINGS_VERSION: + return self.collection.find_one({ + "type": SYSTEM_SETTINGS_KEY + }) + if version is None: version = self._current_version @@ -865,13 +978,21 @@ class MongoSettingsHandler(SettingsHandler): def _get_project_settings_overrides_for_version( self, project_name, version=None ): - if version is None: - version = self._current_version + # QUESTION cache? + if version == LEGACY_SETTINGS_VERSION: + document_filter = { + "type": PROJECT_SETTINGS_KEY + } + + else: + if version is None: + version = self._current_version + + document_filter = { + "type": self._project_settings_key, + "version": version + } - document_filter = { - "type": self._project_settings_key, - "version": version - } if project_name is None: document_filter["is_default"] = True else: @@ -879,6 +1000,13 @@ class MongoSettingsHandler(SettingsHandler): return self.collection.find_one(document_filter) def _get_project_anatomy_overrides_for_version(self, version=None): + # QUESTION cache? + if version == LEGACY_SETTINGS_VERSION: + return self.collection.find_one({ + "type": PROJECT_SETTINGS_KEY, + "is_default": True + }) + if version is None: version = self._current_version @@ -888,42 +1016,69 @@ class MongoSettingsHandler(SettingsHandler): "version": version }) - def get_studio_system_settings_overrides(self): + def get_studio_system_settings_overrides(self, return_version): """Studio overrides of system settings.""" if self.system_settings_cache.is_outdated: globals_document = self.collection.find_one({ "type": GLOBAL_SETTINGS_KEY }) - system_settings_document = ( + document = ( self._get_studio_system_settings_overrides_for_version() ) - if system_settings_document is None: - system_settings_document = self._find_closest_system_settings() + if document is None: + document = self._find_closest_system_settings() + + version = None + if document: + if document["type"] == self._system_settings_key: + version = document["version"] + else: + version = LEGACY_SETTINGS_VERSION merged_document = self._apply_global_settings( - system_settings_document, globals_document + document, globals_document ) - self.system_settings_cache.update_from_document(merged_document) - return self.system_settings_cache.data_copy() + self.system_settings_cache.update_from_document( + merged_document, version + ) - def _get_project_settings_overrides(self, project_name): + cache = self.system_settings_cache + data = cache.data_copy() + if return_version: + return data, cache.version + return data + + def _get_project_settings_overrides(self, project_name, return_version): if self.project_settings_cache[project_name].is_outdated: document = self._get_project_settings_overrides_for_version( project_name ) if document is None: document = self._find_closest_project_settings(project_name) + + version = None + if document: + if document["type"] == self._project_settings_key: + version = document["version"] + else: + version = LEGACY_SETTINGS_VERSION + self.project_settings_cache[project_name].update_from_document( - document + document, version ) - return self.project_settings_cache[project_name].data_copy() - def get_studio_project_settings_overrides(self): + cache = self.project_settings_cache[project_name] + data = cache.data_copy() + if return_version: + return data, cache.version + return data + + def get_studio_project_settings_overrides(self, return_version): """Studio overrides of default project settings.""" - return self._get_project_settings_overrides(None) + return self._get_project_settings_overrides(None, return_version) - def get_project_settings_overrides(self, project_name): + def get_project_settings_overrides(self, project_name, return_version): """Studio overrides of project settings for specific project. Args: @@ -933,8 +1088,12 @@ class MongoSettingsHandler(SettingsHandler): dict: Only overrides for entered project, may be empty dictionary. """ if not project_name: + if return_version: + return {}, None return {} - return self._get_project_settings_overrides(project_name) + return self._get_project_settings_overrides( + project_name, return_version + ) def project_doc_to_anatomy_data(self, project_doc): """Convert project document to anatomy data. @@ -976,27 +1135,39 @@ class MongoSettingsHandler(SettingsHandler): return output - def _get_project_anatomy_overrides(self, project_name): + def _get_project_anatomy_overrides(self, project_name, return_version): if self.project_anatomy_cache[project_name].is_outdated: if project_name is None: document = self._get_project_anatomy_overrides_for_version() if document is None: document = self._find_closest_project_anatomy() + + version = None + if document: + if document["type"] == self._project_anatomy_key: + version = document["version"] + else: + version = LEGACY_SETTINGS_VERSION self.project_anatomy_cache[project_name].update_from_document( - document + document, version ) else: collection = self.avalon_db.database[project_name] project_doc = collection.find_one({"type": "project"}) self.project_anatomy_cache[project_name].update_data( - self.project_doc_to_anatomy_data(project_doc) + self.project_doc_to_anatomy_data(project_doc), + self._current_version ) - return self.project_anatomy_cache[project_name].data_copy() + cache = self.project_anatomy_cache[project_name] + data = cache.data_copy() + if return_version: + return data, cache.version + return data - def get_studio_project_anatomy_overrides(self): + def get_studio_project_anatomy_overrides(self, return_version): """Studio overrides of default project anatomy data.""" - return self._get_project_anatomy_overrides(None) + return self._get_project_anatomy_overrides(None, return_version) def get_project_anatomy_overrides(self, project_name): """Studio overrides of project anatomy for specific project. @@ -1009,24 +1180,36 @@ class MongoSettingsHandler(SettingsHandler): """ if not project_name: return {} - return self._get_project_anatomy_overrides(project_name) + return self._get_project_anatomy_overrides(project_name, False) # Implementations of abstract methods to get overrides for version def get_studio_system_settings_overrides_for_version(self, version): - return self._get_studio_system_settings_overrides_for_version(version) + doc = self._get_studio_system_settings_overrides_for_version(version) + if not doc: + return doc + return doc["data"] def get_studio_project_anatomy_overrides_for_version(self, version): - return self._get_project_anatomy_overrides_for_version(version) + doc = self._get_project_anatomy_overrides_for_version(version) + if not doc: + return doc + return doc["data"] def get_studio_project_settings_overrides_for_version(self, version): - return self._get_project_settings_overrides_for_version(None, version) + doc = self._get_project_settings_overrides_for_version(None, version) + if not doc: + return doc + return doc["data"] def get_project_settings_overrides_for_version( self, project_name, version ): - return self._get_project_settings_overrides_for_version( + doc = self._get_project_settings_overrides_for_version( project_name, version ) + if not doc: + return doc + return doc["data"] # Implementations of abstract methods to clear overrides for version def clear_studio_system_settings_overrides_for_version(self, version): @@ -1057,34 +1240,136 @@ class MongoSettingsHandler(SettingsHandler): "project_name": project_name }) + def _sort_versions(self, versions): + """Sort versions. + + WARNING: + This method does not handle all possible issues so it should not be + used in logic which determine which settings are used. Is used for + sorting of available versions. + """ + if not versions: + return [] + + set_versions = set(versions) + contain_legacy = LEGACY_SETTINGS_VERSION in set_versions + if contain_legacy: + set_versions.remove(LEGACY_SETTINGS_VERSION) + + from openpype.lib.openpype_version import get_OpenPypeVersion + + OpenPypeVersion = get_OpenPypeVersion() + + # Skip if 'OpenPypeVersion' is not available + if OpenPypeVersion is not None: + obj_versions = sorted( + [OpenPypeVersion(version=version) for version in set_versions] + ) + sorted_versions = [str(version) for version in obj_versions] + if contain_legacy: + sorted_versions.insert(0, LEGACY_SETTINGS_VERSION) + return sorted_versions + + doc = self._get_versions_order_doc() + all_versions = doc.get(self._all_versions_keys) + if not all_versions: + return list(sorted(versions)) + + sorted_versions = [] + for version in all_versions: + if version in set_versions: + set_versions.remove(version) + sorted_versions.append(version) + + for version in sorted(set_versions): + sorted_versions.insert(0, version) + + if contain_legacy: + sorted_versions.insert(0, LEGACY_SETTINGS_VERSION) + return sorted_versions + # Get available versions for settings type - def get_available_studio_system_settings_overrides_versions(self): + def get_available_studio_system_settings_overrides_versions( + self, sorted=None + ): docs = self.collection.find( - {"type": self._system_settings_key}, - {"version": True} + {"type": { + "$in": [self._system_settings_key, SYSTEM_SETTINGS_KEY] + }}, + {"type": True, "version": True} ) - return {doc["version"] for doc in docs} + output = set() + for doc in docs: + if doc["type"] == self._system_settings_key: + output.add(doc["version"]) + else: + output.add(LEGACY_SETTINGS_VERSION) + if not sorted: + return output + return self._sort_versions(output) - def get_available_studio_project_anatomy_overrides_versions(self): + def get_available_studio_project_anatomy_overrides_versions( + self, sorted=None + ): docs = self.collection.find( - {"type": self._project_anatomy_key}, - {"version": True} + {"type": { + "$in": [self._project_anatomy_key, PROJECT_ANATOMY_KEY] + }}, + {"type": True, "version": True} ) - return {doc["version"] for doc in docs} + output = set() + for doc in docs: + if doc["type"] == self._project_anatomy_key: + output.add(doc["version"]) + else: + output.add(LEGACY_SETTINGS_VERSION) + if not sorted: + return output + return self._sort_versions(output) - def get_available_studio_project_settings_overrides_versions(self): + def get_available_studio_project_settings_overrides_versions( + self, sorted=None + ): docs = self.collection.find( - {"type": self._project_settings_key, "is_default": True}, - {"version": True} + { + "is_default": True, + "type": { + "$in": [self._project_settings_key, PROJECT_SETTINGS_KEY] + } + }, + {"type": True, "version": True} ) - return {doc["version"] for doc in docs} + output = set() + for doc in docs: + if doc["type"] == self._project_settings_key: + output.add(doc["version"]) + else: + output.add(LEGACY_SETTINGS_VERSION) + if not sorted: + return output + return self._sort_versions(output) - def get_available_project_settings_overrides_versions(self, project_name): + def get_available_project_settings_overrides_versions( + self, project_name, sorted=None + ): docs = self.collection.find( - {"type": self._project_settings_key, "project_name": project_name}, - {"version": True} + { + "project_name": project_name, + "type": { + "$in": [self._project_settings_key, PROJECT_SETTINGS_KEY] + } + }, + {"type": True, "version": True} ) - return {doc["version"] for doc in docs} + output = set() + for doc in docs: + if doc["type"] == self._project_settings_key: + output.add(doc["version"]) + else: + output.add(LEGACY_SETTINGS_VERSION) + if not sorted: + return output + return self._sort_versions(output) class MongoLocalSettingsHandler(LocalSettingsHandler): diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 43489aecfd..b8e7ef7284 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -266,23 +266,31 @@ def save_project_anatomy(project_name, anatomy_data): @require_handler -def get_studio_system_settings_overrides(): - return _SETTINGS_HANDLER.get_studio_system_settings_overrides() +def get_studio_system_settings_overrides(return_version=False): + return _SETTINGS_HANDLER.get_studio_system_settings_overrides( + return_version + ) @require_handler -def get_studio_project_settings_overrides(): - return _SETTINGS_HANDLER.get_studio_project_settings_overrides() +def get_studio_project_settings_overrides(return_version=False): + return _SETTINGS_HANDLER.get_studio_project_settings_overrides( + return_version + ) @require_handler -def get_studio_project_anatomy_overrides(): - return _SETTINGS_HANDLER.get_studio_project_anatomy_overrides() +def get_studio_project_anatomy_overrides(return_version=False): + return _SETTINGS_HANDLER.get_studio_project_anatomy_overrides( + return_version + ) @require_handler -def get_project_settings_overrides(project_name): - return _SETTINGS_HANDLER.get_project_settings_overrides(project_name) +def get_project_settings_overrides(project_name, return_version=False): + return _SETTINGS_HANDLER.get_project_settings_overrides( + project_name, return_version + ) @require_handler @@ -290,6 +298,123 @@ def get_project_anatomy_overrides(project_name): return _SETTINGS_HANDLER.get_project_anatomy_overrides(project_name) +@require_handler +def get_studio_system_settings_overrides_for_version(version): + return ( + _SETTINGS_HANDLER + .get_studio_system_settings_overrides_for_version(version) + ) + + +@require_handler +def get_studio_project_anatomy_overrides_for_version(version): + return ( + _SETTINGS_HANDLER + .get_studio_project_anatomy_overrides_for_version(version) + ) + + +@require_handler +def get_studio_project_settings_overrides_for_version(version): + return ( + _SETTINGS_HANDLER + .get_studio_project_settings_overrides_for_version(version) + ) + + +@require_handler +def get_project_settings_overrides_for_version( + project_name, version +): + return ( + _SETTINGS_HANDLER + .get_project_settings_overrides_for_version(project_name, version) + ) + + +@require_handler +def get_available_studio_system_settings_overrides_versions(sorted=None): + return ( + _SETTINGS_HANDLER + .get_available_studio_system_settings_overrides_versions( + sorted=sorted + ) + ) + + +@require_handler +def get_available_studio_project_anatomy_overrides_versions(sorted=None): + return ( + _SETTINGS_HANDLER + .get_available_studio_project_anatomy_overrides_versions( + sorted=sorted + ) + ) + + +@require_handler +def get_available_studio_project_settings_overrides_versions(sorted=None): + return ( + _SETTINGS_HANDLER + .get_available_studio_project_settings_overrides_versions( + sorted=sorted + ) + ) + + +@require_handler +def get_available_project_settings_overrides_versions( + project_name, sorted=None +): + return ( + _SETTINGS_HANDLER + .get_available_project_settings_overrides_versions( + project_name, sorted=sorted + ) + ) + + +@require_handler +def find_closest_version_for_projects(project_names): + return ( + _SETTINGS_HANDLER + .find_closest_version_for_projects(project_names) + ) + + +@require_handler +def clear_studio_system_settings_overrides_for_version(version): + return ( + _SETTINGS_HANDLER + .clear_studio_system_settings_overrides_for_version(version) + ) + + +@require_handler +def clear_studio_project_settings_overrides_for_version(version): + return ( + _SETTINGS_HANDLER + .clear_studio_project_settings_overrides_for_version(version) + ) + + +@require_handler +def clear_studio_project_anatomy_overrides_for_version(version): + return ( + _SETTINGS_HANDLER + .clear_studio_project_anatomy_overrides_for_version(version) + ) + + +@require_handler +def clear_project_settings_overrides_for_version( + version, project_name +): + return _SETTINGS_HANDLER.clear_project_settings_overrides_for_version( + version, project_name + ) + + @require_local_handler def save_local_settings(data): return _LOCAL_SETTINGS_HANDLER.save_local_settings(data) diff --git a/openpype/style/style.css b/openpype/style/style.css index d9b0ff7421..e35d8fb220 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1111,6 +1111,19 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { #ExpandLabel[state="invalid"]:hover, #SettingsLabel[state="invalid"]:hover { color: {color:settings:invalid-dark}; } +#SourceVersionLabel { + border-radius: 0.48em; + padding-left: 3px; + padding-right: 3px; +} + +#SourceVersionLabel[state="same"] { + border: 1px solid {color:font}; +} +#SourceVersionLabel[state="different"] { + border: 1px solid {color:font-disabled}; + color: {color:font-disabled}; +} /* TODO Replace these with explicit widget types if possible */ #SettingsMainWidget QWidget[input-state="modified"] { diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index adbde00bf1..74f29133f6 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -3,8 +3,10 @@ import sys import traceback import contextlib from enum import Enum -from Qt import QtWidgets, QtCore, QtGui +from Qt import QtWidgets, QtCore +from openpype.lib import get_openpype_version +from openpype.tools.utils import set_style_property from openpype.settings.entities import ( SystemSettings, ProjectSettings, @@ -34,7 +36,10 @@ from openpype.settings.entities.op_version_entity import ( ) from openpype.settings import SaveWarningExc -from .widgets import ProjectListWidget +from .widgets import ( + ProjectListWidget, + VersionAction +) from .breadcrumbs_widget import ( BreadcrumbsAddressBar, SystemSettingsBreadcrumbs, @@ -98,6 +103,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self._state = CategoryState.Idle self._hide_studio_overrides = False + self._updating_root = False + self._use_version = None + self._current_version = get_openpype_version() + self.ignore_input_changes = IgnoreInputChangesObj(self) self.keys = [] @@ -183,76 +192,98 @@ class SettingsCategoryWidget(QtWidgets.QWidget): def initialize_attributes(self): return + @property + def is_modifying_defaults(self): + if self.modify_defaults_checkbox is None: + return False + return self.modify_defaults_checkbox.isChecked() + def create_ui(self): self.modify_defaults_checkbox = None - scroll_widget = QtWidgets.QScrollArea(self) - scroll_widget.setObjectName("GroupWidget") - content_widget = QtWidgets.QWidget(scroll_widget) + conf_wrapper_widget = QtWidgets.QWidget(self) + configurations_widget = QtWidgets.QWidget(conf_wrapper_widget) - breadcrumbs_label = QtWidgets.QLabel("Path:", content_widget) - breadcrumbs_widget = BreadcrumbsAddressBar(content_widget) + breadcrumbs_widget = QtWidgets.QWidget(self) + breadcrumbs_label = QtWidgets.QLabel("Path:", breadcrumbs_widget) + breadcrumbs_bar = BreadcrumbsAddressBar(breadcrumbs_widget) - breadcrumbs_layout = QtWidgets.QHBoxLayout() + refresh_icon = qtawesome.icon("fa.refresh", color="white") + refresh_btn = QtWidgets.QPushButton(breadcrumbs_widget) + refresh_btn.setIcon(refresh_icon) + + breadcrumbs_layout = QtWidgets.QHBoxLayout(breadcrumbs_widget) breadcrumbs_layout.setContentsMargins(5, 5, 5, 5) breadcrumbs_layout.setSpacing(5) - breadcrumbs_layout.addWidget(breadcrumbs_label) - breadcrumbs_layout.addWidget(breadcrumbs_widget) + breadcrumbs_layout.addWidget(breadcrumbs_label, 0) + breadcrumbs_layout.addWidget(breadcrumbs_bar, 1) + breadcrumbs_layout.addWidget(refresh_btn, 0) + + scroll_widget = QtWidgets.QScrollArea(configurations_widget) + scroll_widget.setObjectName("GroupWidget") + content_widget = QtWidgets.QWidget(scroll_widget) + scroll_widget.setWidgetResizable(True) + scroll_widget.setWidget(content_widget) content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.setContentsMargins(3, 3, 3, 3) content_layout.setSpacing(5) content_layout.setAlignment(QtCore.Qt.AlignTop) - scroll_widget.setWidgetResizable(True) - scroll_widget.setWidget(content_widget) + footer_widget = QtWidgets.QWidget(configurations_widget) - refresh_icon = qtawesome.icon("fa.refresh", color="white") - refresh_btn = QtWidgets.QPushButton(self) - refresh_btn.setIcon(refresh_icon) + source_version_label = QtWidgets.QLabel("", footer_widget) + source_version_label.setObjectName("SourceVersionLabel") + set_style_property(source_version_label, "state", "") + source_version_label.setToolTip( + "Version of OpenPype from which are settings loaded." + "\nThe 'legacy' are settings that were not stored per version." + ) - footer_layout = QtWidgets.QHBoxLayout() - footer_layout.setContentsMargins(5, 5, 5, 5) - if self.user_role == "developer": - self._add_developer_ui(footer_layout) - - save_btn = QtWidgets.QPushButton("Save", self) - require_restart_label = QtWidgets.QLabel(self) + save_btn = QtWidgets.QPushButton("Save", footer_widget) + require_restart_label = QtWidgets.QLabel(footer_widget) require_restart_label.setAlignment(QtCore.Qt.AlignCenter) - footer_layout.addWidget(refresh_btn, 0) + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(5, 5, 5, 5) + footer_layout.setSpacing(10) + if self.user_role == "developer": + self._add_developer_ui(footer_layout, footer_widget) + + footer_layout.addWidget(source_version_label, 0) footer_layout.addWidget(require_restart_label, 1) footer_layout.addWidget(save_btn, 0) - configurations_layout = QtWidgets.QVBoxLayout() + configurations_layout = QtWidgets.QVBoxLayout(configurations_widget) configurations_layout.setContentsMargins(0, 0, 0, 0) configurations_layout.setSpacing(0) configurations_layout.addWidget(scroll_widget, 1) - configurations_layout.addLayout(footer_layout, 0) + configurations_layout.addWidget(footer_widget, 0) - conf_wrapper_layout = QtWidgets.QHBoxLayout() + conf_wrapper_layout = QtWidgets.QHBoxLayout(conf_wrapper_widget) conf_wrapper_layout.setContentsMargins(0, 0, 0, 0) conf_wrapper_layout.setSpacing(0) - conf_wrapper_layout.addLayout(configurations_layout, 1) + conf_wrapper_layout.addWidget(configurations_widget, 1) main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) - main_layout.addLayout(breadcrumbs_layout, 0) - main_layout.addLayout(conf_wrapper_layout, 1) + main_layout.addWidget(breadcrumbs_widget, 0) + main_layout.addWidget(conf_wrapper_widget, 1) save_btn.clicked.connect(self._save) refresh_btn.clicked.connect(self._on_refresh) - breadcrumbs_widget.path_edited.connect(self._on_path_edit) + breadcrumbs_bar.path_edited.connect(self._on_path_edit) self.save_btn = save_btn + self._source_version_label = source_version_label self.refresh_btn = refresh_btn self.require_restart_label = require_restart_label self.scroll_widget = scroll_widget self.content_layout = content_layout self.content_widget = content_widget - self.breadcrumbs_widget = breadcrumbs_widget + self.breadcrumbs_bar = breadcrumbs_bar self.breadcrumbs_model = None self.conf_wrapper_layout = conf_wrapper_layout self.main_layout = main_layout @@ -308,21 +339,17 @@ class SettingsCategoryWidget(QtWidgets.QWidget): pass def set_path(self, path): - self.breadcrumbs_widget.set_path(path) + self.breadcrumbs_bar.set_path(path) - def _add_developer_ui(self, footer_layout): - modify_defaults_widget = QtWidgets.QWidget() - modify_defaults_checkbox = QtWidgets.QCheckBox(modify_defaults_widget) + def _add_developer_ui(self, footer_layout, footer_widget): + modify_defaults_checkbox = QtWidgets.QCheckBox(footer_widget) modify_defaults_checkbox.setChecked(self._hide_studio_overrides) label_widget = QtWidgets.QLabel( - "Modify defaults", modify_defaults_widget + "Modify defaults", footer_widget ) - modify_defaults_layout = QtWidgets.QHBoxLayout(modify_defaults_widget) - modify_defaults_layout.addWidget(label_widget) - modify_defaults_layout.addWidget(modify_defaults_checkbox) - - footer_layout.addWidget(modify_defaults_widget, 0) + footer_layout.addWidget(label_widget, 0) + footer_layout.addWidget(modify_defaults_checkbox, 0) modify_defaults_checkbox.stateChanged.connect( self._on_modify_defaults @@ -361,6 +388,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): try: self.entity.save() + self._use_version = None # NOTE There are relations to previous entities and C++ callbacks # so it is easier to just use new entity and recreate UI but @@ -444,6 +472,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget): widget.deleteLater() dialog = None + self._updating_root = True + source_version = "" try: self._create_root_entity() @@ -459,6 +489,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): input_field.set_entity_value() self.ignore_input_changes.set_ignore(False) + source_version = self.entity.source_version except DefaultsNotDefined: dialog = QtWidgets.QMessageBox(self) @@ -502,6 +533,18 @@ class SettingsCategoryWidget(QtWidgets.QWidget): spacer, layout.rowCount(), 0, 1, layout.columnCount() ) + self._updating_root = False + + # Update source version label + state_value = "" + if source_version: + if source_version != self._current_version: + state_value = "different" + else: + state_value = "same" + self._source_version_label.setText(source_version) + set_style_property(self._source_version_label, "state", state_value) + self.set_state(CategoryState.Idle) if dialog: @@ -510,6 +553,36 @@ class SettingsCategoryWidget(QtWidgets.QWidget): else: self._on_reset_success() + def _on_source_version_change(self, version): + if self._updating_root: + return + + if version == self._current_version: + version = None + + self._use_version = version + QtCore.QTimer.singleShot(20, self.reset) + + def add_context_actions(self, menu): + if not self.entity or self.is_modifying_defaults: + return + + versions = self.entity.get_available_studio_versions(sorted=True) + if not versions: + return + + submenu = QtWidgets.QMenu("Use settings from version", menu) + for version in reversed(versions): + action = VersionAction(version, submenu) + action.version_triggered.connect( + self._on_context_version_trigger + ) + submenu.addAction(action) + menu.addMenu(submenu) + + def _on_context_version_trigger(self, version): + self._on_source_version_change(version) + def _on_reset_crash(self): self.save_btn.setEnabled(False) @@ -521,10 +594,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.save_btn.setEnabled(True) if self.breadcrumbs_model is not None: - path = self.breadcrumbs_widget.path() - self.breadcrumbs_widget.set_path("") + path = self.breadcrumbs_bar.path() + self.breadcrumbs_bar.set_path("") self.breadcrumbs_model.set_entity(self.entity) - self.breadcrumbs_widget.change_path(path) + self.breadcrumbs_bar.change_path(path) def add_children_gui(self): for child_obj in self.entity.children: @@ -565,10 +638,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): def _save(self): # Don't trigger restart if defaults are modified - if ( - self.modify_defaults_checkbox - and self.modify_defaults_checkbox.isChecked() - ): + if self.is_modifying_defaults: require_restart = False else: require_restart = self.entity.require_restart @@ -594,25 +664,29 @@ class SettingsCategoryWidget(QtWidgets.QWidget): class SystemWidget(SettingsCategoryWidget): + def __init__(self, *args, **kwargs): + self._actions = [] + super(SystemWidget, self).__init__(*args, **kwargs) + def contain_category_key(self, category): if category == "system_settings": return True return False def set_category_path(self, category, path): - self.breadcrumbs_widget.change_path(path) + self.breadcrumbs_bar.change_path(path) def _create_root_entity(self): - self.entity = SystemSettings(set_studio_state=False) - self.entity.on_change_callbacks.append(self._on_entity_change) + entity = SystemSettings( + set_studio_state=False, source_version=self._use_version + ) + entity.on_change_callbacks.append(self._on_entity_change) + self.entity = entity try: - if ( - self.modify_defaults_checkbox - and self.modify_defaults_checkbox.isChecked() - ): - self.entity.set_defaults_state() + if self.is_modifying_defaults: + entity.set_defaults_state() else: - self.entity.set_studio_state() + entity.set_studio_state() if self.modify_defaults_checkbox: self.modify_defaults_checkbox.setEnabled(True) @@ -620,16 +694,16 @@ class SystemWidget(SettingsCategoryWidget): if not self.modify_defaults_checkbox: raise - self.entity.set_defaults_state() + entity.set_defaults_state() self.modify_defaults_checkbox.setChecked(True) self.modify_defaults_checkbox.setEnabled(False) def ui_tweaks(self): self.breadcrumbs_model = SystemSettingsBreadcrumbs() - self.breadcrumbs_widget.set_model(self.breadcrumbs_model) + self.breadcrumbs_bar.set_model(self.breadcrumbs_model) def _on_modify_defaults(self): - if self.modify_defaults_checkbox.isChecked(): + if self.is_modifying_defaults: if not self.entity.is_in_defaults_state(): self.reset() else: @@ -638,6 +712,9 @@ class SystemWidget(SettingsCategoryWidget): class ProjectWidget(SettingsCategoryWidget): + def __init__(self, *args, **kwargs): + super(ProjectWidget, self).__init__(*args, **kwargs) + def contain_category_key(self, category): if category in ("project_settings", "project_anatomy"): return True @@ -651,28 +728,28 @@ class ProjectWidget(SettingsCategoryWidget): else: path = category - self.breadcrumbs_widget.change_path(path) + self.breadcrumbs_bar.change_path(path) def initialize_attributes(self): self.project_name = None def ui_tweaks(self): self.breadcrumbs_model = ProjectSettingsBreadcrumbs() - self.breadcrumbs_widget.set_model(self.breadcrumbs_model) + self.breadcrumbs_bar.set_model(self.breadcrumbs_model) project_list_widget = ProjectListWidget(self) self.conf_wrapper_layout.insertWidget(0, project_list_widget, 0) project_list_widget.project_changed.connect(self._on_project_change) + project_list_widget.version_change_requested.connect( + self._on_source_version_change + ) self.project_list_widget = project_list_widget def get_project_names(self): - if ( - self.modify_defaults_checkbox - and self.modify_defaults_checkbox.isChecked() - ): + if self.is_modifying_defaults: return [] return self.project_list_widget.get_project_names() @@ -684,6 +761,10 @@ class ProjectWidget(SettingsCategoryWidget): if self is saved_tab_widget: return + def _on_context_version_trigger(self, version): + self.project_list_widget.select_project(None) + super(ProjectWidget, self)._on_context_version_trigger(version) + def _on_reset_start(self): self.project_list_widget.refresh() @@ -696,32 +777,29 @@ class ProjectWidget(SettingsCategoryWidget): super(ProjectWidget, self)._on_reset_success() def _set_enabled_project_list(self, enabled): - if ( - enabled - and self.modify_defaults_checkbox - and self.modify_defaults_checkbox.isChecked() - ): + if enabled and self.is_modifying_defaults: enabled = False if self.project_list_widget.isEnabled() != enabled: self.project_list_widget.setEnabled(enabled) def _create_root_entity(self): - self.entity = ProjectSettings(change_state=False) - self.entity.on_change_callbacks.append(self._on_entity_change) + entity = ProjectSettings( + change_state=False, source_version=self._use_version + ) + entity.on_change_callbacks.append(self._on_entity_change) + self.project_list_widget.set_entity(entity) + self.entity = entity try: - if ( - self.modify_defaults_checkbox - and self.modify_defaults_checkbox.isChecked() - ): + if self.is_modifying_defaults: self.entity.set_defaults_state() elif self.project_name is None: self.entity.set_studio_state() - elif self.project_name == self.entity.project_name: - self.entity.set_project_state() else: - self.entity.change_project(self.project_name) + self.entity.change_project( + self.project_name, self._use_version + ) if self.modify_defaults_checkbox: self.modify_defaults_checkbox.setEnabled(True) @@ -754,7 +832,7 @@ class ProjectWidget(SettingsCategoryWidget): self.set_state(CategoryState.Idle) def _on_modify_defaults(self): - if self.modify_defaults_checkbox.isChecked(): + if self.is_modifying_defaults: self._set_enabled_project_list(False) if not self.entity.is_in_defaults_state(): self.reset() diff --git a/openpype/tools/settings/settings/constants.py b/openpype/tools/settings/settings/constants.py index 5c20bf1afe..9d6d7904d7 100644 --- a/openpype/tools/settings/settings/constants.py +++ b/openpype/tools/settings/settings/constants.py @@ -5,6 +5,7 @@ DEFAULT_PROJECT_LABEL = "< Default >" PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2 PROJECT_IS_SELECTED_ROLE = QtCore.Qt.UserRole + 3 +PROJECT_VERSION_ROLE = QtCore.Qt.UserRole + 4 __all__ = ( @@ -12,5 +13,6 @@ __all__ = ( "PROJECT_NAME_ROLE", "PROJECT_IS_ACTIVE_ROLE", - "PROJECT_IS_SELECTED_ROLE" + "PROJECT_IS_SELECTED_ROLE", + "PROJECT_VERSION_ROLE", ) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index b5c08ef79b..f136f981cd 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -13,7 +13,7 @@ from openpype.tools.utils.lib import paint_image_with_color from openpype.widgets.nice_checkbox import NiceCheckbox from openpype.tools.utils import PlaceholderLineEdit -from openpype.settings.lib import get_system_settings +from openpype.settings.lib import find_closest_version_for_projects from .images import ( get_pixmap, get_image @@ -21,11 +21,40 @@ from .images import ( from .constants import ( DEFAULT_PROJECT_LABEL, PROJECT_NAME_ROLE, + PROJECT_VERSION_ROLE, PROJECT_IS_ACTIVE_ROLE, PROJECT_IS_SELECTED_ROLE ) +class SettingsTabWidget(QtWidgets.QTabWidget): + context_menu_requested = QtCore.Signal(int) + + def __init__(self, *args, **kwargs): + super(SettingsTabWidget, self).__init__(*args, **kwargs) + self._right_click_tab_idx = None + + def mousePressEvent(self, event): + super(SettingsTabWidget, self).mousePressEvent(event) + if event.button() == QtCore.Qt.RightButton: + tab_bar = self.tabBar() + pos = tab_bar.mapFromGlobal(event.globalPos()) + tab_idx = tab_bar.tabAt(pos) + if tab_idx < 0: + tab_idx = None + self._right_click_tab_idx = tab_idx + + def mouseReleaseEvent(self, event): + super(SettingsTabWidget, self).mouseReleaseEvent(event) + if event.button() == QtCore.Qt.RightButton: + tab_bar = self.tabBar() + pos = tab_bar.mapFromGlobal(event.globalPos()) + tab_idx = tab_bar.tabAt(pos) + if tab_idx == self._right_click_tab_idx: + self.context_menu_requested.emit(tab_idx) + self._right_click_tab = None + + class CompleterFilter(QtCore.QSortFilterProxyModel): def __init__(self, *args, **kwargs): super(CompleterFilter, self).__init__(*args, **kwargs) @@ -603,7 +632,7 @@ class UnsavedChangesDialog(QtWidgets.QDialog): message = "You have unsaved changes. What do you want to do with them?" def __init__(self, parent=None): - super().__init__(parent) + super(UnsavedChangesDialog, self).__init__(parent) message_label = QtWidgets.QLabel(self.message) btns_widget = QtWidgets.QWidget(self) @@ -738,12 +767,19 @@ class ProjectModel(QtGui.QStandardItemModel): def __init__(self, only_active, *args, **kwargs): super(ProjectModel, self).__init__(*args, **kwargs) + self.setColumnCount(2) + self.dbcon = None self._only_active = only_active self._default_item = None self._items_by_name = {} + def flags(self, index): + if index.column() == 1: + index = self.index(index.row(), 0, index.parent()) + return super(ProjectModel, self).flags(index) + def set_dbcon(self, dbcon): self.dbcon = dbcon @@ -757,6 +793,7 @@ class ProjectModel(QtGui.QStandardItemModel): new_items.append(item) self._default_item = item + self._default_item.setData("", PROJECT_VERSION_ROLE) project_names = set() if self.dbcon is not None: for project_doc in self.dbcon.projects( @@ -776,6 +813,7 @@ class ProjectModel(QtGui.QStandardItemModel): is_active = project_doc.get("data", {}).get("active", True) item.setData(project_name, PROJECT_NAME_ROLE) item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) + item.setData("", PROJECT_VERSION_ROLE) item.setData(False, PROJECT_IS_SELECTED_ROLE) if not is_active: @@ -783,6 +821,19 @@ class ProjectModel(QtGui.QStandardItemModel): font.setItalic(True) item.setFont(font) + # TODO this could be threaded + all_project_names = list(project_names) + all_project_names.append(None) + closes_by_project_name = find_closest_version_for_projects( + project_names + ) + for project_name, version in closes_by_project_name.items(): + if project_name is None: + item = self._default_item + else: + item = self._items_by_name.get(project_name) + if item: + item.setData(version, PROJECT_VERSION_ROLE) root_item = self.invisibleRootItem() for project_name in tuple(self._items_by_name.keys()): if project_name not in project_names: @@ -792,15 +843,75 @@ class ProjectModel(QtGui.QStandardItemModel): if new_items: root_item.appendRows(new_items) + def data(self, index, role=QtCore.Qt.DisplayRole): + if index.column() == 1: + if role == QtCore.Qt.TextAlignmentRole: + return QtCore.Qt.AlignRight + index = self.index(index.row(), 0, index.parent()) + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + role = PROJECT_VERSION_ROLE -class ProjectListView(QtWidgets.QListView): + return super(ProjectModel, self).data(index, role) + + def setData(self, index, value, role=QtCore.Qt.EditRole): + if index.column() == 1: + index = self.index(index.row(), 0, index.parent()) + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + role = PROJECT_VERSION_ROLE + return super(ProjectModel, self).setData(index, value, role) + + def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): + if role == QtCore.Qt.DisplayRole: + if section == 0: + return "Project name" + + elif section == 1: + return "Used version" + return "" + return super(ProjectModel, self).headerData( + section, orientation, role + ) + + +class VersionAction(QtWidgets.QAction): + version_triggered = QtCore.Signal(str) + + def __init__(self, version, *args, **kwargs): + super(VersionAction, self).__init__(version, *args, **kwargs) + self._version = version + self.triggered.connect(self._on_trigger) + + def _on_trigger(self): + self.version_triggered.emit(self._version) + + +class ProjectView(QtWidgets.QTreeView): left_mouse_released_at = QtCore.Signal(QtCore.QModelIndex) + right_mouse_released_at = QtCore.Signal(QtCore.QModelIndex) + + def __init__(self, *args, **kwargs): + super(ProjectView, self).__init__(*args, **kwargs) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setIndentation(0) + + # Do not allow editing + self.setEditTriggers( + QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers + ) + # Do not automatically handle selection + self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) def mouseReleaseEvent(self, event): if event.button() == QtCore.Qt.LeftButton: index = self.indexAt(event.pos()) self.left_mouse_released_at.emit(index) - super(ProjectListView, self).mouseReleaseEvent(event) + + elif event.button() == QtCore.Qt.RightButton: + index = self.indexAt(event.pos()) + self.right_mouse_released_at.emit(index) + + super(ProjectView, self).mouseReleaseEvent(event) class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): @@ -846,18 +957,18 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): class ProjectListWidget(QtWidgets.QWidget): project_changed = QtCore.Signal() + version_change_requested = QtCore.Signal(str) def __init__(self, parent, only_active=False): self._parent = parent + self._entity = None self.current_project = None super(ProjectListWidget, self).__init__(parent) self.setObjectName("ProjectListWidget") - label_widget = QtWidgets.QLabel("Projects") - - project_list = ProjectListView(self) + project_list = ProjectView(self) project_model = ProjectModel(only_active) project_proxy = ProjectSortFilterProxy() @@ -865,16 +976,8 @@ class ProjectListWidget(QtWidgets.QWidget): project_proxy.setSourceModel(project_model) project_list.setModel(project_proxy) - # Do not allow editing - project_list.setEditTriggers( - QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers - ) - # Do not automatically handle selection - project_list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) - layout = QtWidgets.QVBoxLayout(self) layout.setSpacing(3) - layout.addWidget(label_widget, 0) layout.addWidget(project_list, 1) if only_active: @@ -890,6 +993,9 @@ class ProjectListWidget(QtWidgets.QWidget): inactive_chk.stateChanged.connect(self.on_inactive_vis_changed) project_list.left_mouse_released_at.connect(self.on_item_clicked) + project_list.right_mouse_released_at.connect( + self._on_item_right_clicked + ) self._default_project_item = None @@ -900,8 +1006,40 @@ class ProjectListWidget(QtWidgets.QWidget): self.dbcon = None + def set_entity(self, entity): + self._entity = entity + + def _on_item_right_clicked(self, index): + project_name = index.data(PROJECT_NAME_ROLE) + if project_name is None: + return + + if self.current_project != project_name: + self.on_item_clicked(index) + + if self.current_project != project_name: + return + + if not self._entity: + return + + versions = self._entity.get_available_source_versions(sorted=True) + if not versions: + return + + menu = QtWidgets.QMenu(self) + submenu = QtWidgets.QMenu("Use settings from version", menu) + for version in reversed(versions): + action = VersionAction(version, submenu) + action.version_triggered.connect( + self.version_change_requested + ) + submenu.addAction(action) + menu.addMenu(submenu) + menu.exec_(QtGui.QCursor.pos()) + def on_item_clicked(self, new_index): - new_project_name = new_index.data(QtCore.Qt.DisplayRole) + new_project_name = new_index.data(PROJECT_NAME_ROLE) if new_project_name is None: return @@ -963,12 +1101,30 @@ class ProjectListWidget(QtWidgets.QWidget): index = model.indexFromItem(found_items[0]) model.setData(index, True, PROJECT_IS_SELECTED_ROLE) - index = proxy.mapFromSource(index) + src_indexes = [] + col_count = model.columnCount() + if col_count > 1: + for col in range(col_count): + src_indexes.append( + model.index(index.row(), col, index.parent()) + ) + dst_indexes = [] + for index in src_indexes: + dst_indexes.append(proxy.mapFromSource(index)) - self.project_list.selectionModel().clear() - self.project_list.selectionModel().setCurrentIndex( - index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent - ) + selection_model = self.project_list.selectionModel() + selection_model.clear() + + first = True + for index in dst_indexes: + if first: + selection_model.setCurrentIndex( + index, + QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent + ) + first = False + continue + selection_model.select(index, QtCore.QItemSelectionModel.Select) def get_project_names(self): output = [] @@ -980,7 +1136,7 @@ class ProjectListWidget(QtWidgets.QWidget): def refresh(self): selected_project = None for index in self.project_list.selectedIndexes(): - selected_project = index.data(QtCore.Qt.DisplayRole) + selected_project = index.data(PROJECT_NAME_ROLE) break mongo_url = os.environ["OPENPYPE_MONGO"] @@ -1008,5 +1164,6 @@ class ProjectListWidget(QtWidgets.QWidget): self.select_project(selected_project) self.current_project = self.project_list.currentIndex().data( - QtCore.Qt.DisplayRole + PROJECT_NAME_ROLE ) + self.project_list.resizeColumnToContents(0) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index c376e5e91e..8297c9e2f5 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -4,7 +4,11 @@ from .categories import ( SystemWidget, ProjectWidget ) -from .widgets import ShadowWidget, RestartDialog +from .widgets import ( + ShadowWidget, + RestartDialog, + SettingsTabWidget +) from openpype import style from openpype.lib import is_admin_password_required @@ -34,7 +38,7 @@ class MainWidget(QtWidgets.QWidget): self.setStyleSheet(stylesheet) self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) - header_tab_widget = QtWidgets.QTabWidget(parent=self) + header_tab_widget = SettingsTabWidget(parent=self) studio_widget = SystemWidget(user_role, header_tab_widget) project_widget = ProjectWidget(user_role, header_tab_widget) @@ -65,6 +69,10 @@ class MainWidget(QtWidgets.QWidget): ) tab_widget.full_path_requested.connect(self._on_full_path_request) + header_tab_widget.context_menu_requested.connect( + self._on_context_menu_request + ) + self._header_tab_widget = header_tab_widget self.tab_widgets = tab_widgets @@ -100,6 +108,18 @@ class MainWidget(QtWidgets.QWidget): tab_widget.set_category_path(category, path) break + def _on_context_menu_request(self, tab_idx): + widget = self._header_tab_widget.widget(tab_idx) + if not widget: + return + + menu = QtWidgets.QMenu(self) + widget.add_context_actions(menu) + if menu.actions(): + result = menu.exec_(QtGui.QCursor.pos()) + if result is not None: + self._header_tab_widget.setCurrentIndex(tab_idx) + def showEvent(self, event): super(MainWidget, self).showEvent(event) if self._reset_on_show: diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 9363007532..2f13eeb9db 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -9,7 +9,8 @@ from .error_dialog import ErrorMessageBox from .lib import ( WrappedCallbackItem, paint_image_with_color, - get_warning_pixmap + get_warning_pixmap, + set_style_property ) @@ -24,4 +25,5 @@ __all__ = ( "WrappedCallbackItem", "paint_image_with_color", "get_warning_pixmap", + "set_style_property", ) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 76ad944ce5..8cc2382a8c 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -30,6 +30,18 @@ def center_window(window): window.move(geo.topLeft()) +def set_style_property(widget, property_name, property_value): + """Set widget's property that may affect style. + + If current property value is different then style of widget is polished. + """ + cur_value = widget.property(property_name) + if cur_value == property_value: + return + widget.setProperty(property_name, property_value) + widget.style().polish(widget) + + def paint_image_with_color(image, color): """Redraw image with single color using it's alpha. From 1587a43a053b55b8a5ba3d9fda1fe598f08e8741 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Jan 2022 11:51:53 +0100 Subject: [PATCH 11/59] changed color of version in projects view --- openpype/tools/settings/settings/widgets.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index f136f981cd..aa3861a56a 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -775,6 +775,11 @@ class ProjectModel(QtGui.QStandardItemModel): self._default_item = None self._items_by_name = {} + colors = get_objected_colors() + font_color = colors["font"].get_qcolor() + font_color.setAlpha(67) + self._version_font_color = font_color + def flags(self, index): if index.column() == 1: index = self.index(index.row(), 0, index.parent()) @@ -847,6 +852,8 @@ class ProjectModel(QtGui.QStandardItemModel): if index.column() == 1: if role == QtCore.Qt.TextAlignmentRole: return QtCore.Qt.AlignRight + if role == QtCore.Qt.ForegroundRole: + return self._version_font_color index = self.index(index.row(), 0, index.parent()) if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): role = PROJECT_VERSION_ROLE From 5a54db2092a02b063ea6e8443e3b2d2e1a0bd8e2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Jan 2022 12:23:02 +0100 Subject: [PATCH 12/59] fix showing of versions in project view --- openpype/tools/settings/settings/widgets.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index aa3861a56a..4bab67dba5 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -14,6 +14,7 @@ from openpype.tools.utils.lib import paint_image_with_color from openpype.widgets.nice_checkbox import NiceCheckbox from openpype.tools.utils import PlaceholderLineEdit from openpype.settings.lib import find_closest_version_for_projects +from openpype.lib import get_openpype_version from .images import ( get_pixmap, get_image @@ -779,6 +780,7 @@ class ProjectModel(QtGui.QStandardItemModel): font_color = colors["font"].get_qcolor() font_color.setAlpha(67) self._version_font_color = font_color + self._current_version = get_openpype_version() def flags(self, index): if index.column() == 1: @@ -830,14 +832,15 @@ class ProjectModel(QtGui.QStandardItemModel): all_project_names = list(project_names) all_project_names.append(None) closes_by_project_name = find_closest_version_for_projects( - project_names + all_project_names ) for project_name, version in closes_by_project_name.items(): if project_name is None: item = self._default_item else: item = self._items_by_name.get(project_name) - if item: + + if item and version != self._current_version: item.setData(version, PROJECT_VERSION_ROLE) root_item = self.invisibleRootItem() for project_name in tuple(self._items_by_name.keys()): @@ -1004,8 +1007,6 @@ class ProjectListWidget(QtWidgets.QWidget): self._on_item_right_clicked ) - self._default_project_item = None - self.project_list = project_list self.project_proxy = project_proxy self.project_model = project_model From 5514b9109509d9e8ead1c4593818f0fb687209d6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Jan 2022 13:57:24 +0100 Subject: [PATCH 13/59] changed colors of version label when is not current version --- openpype/style/data.json | 5 ++++- openpype/style/style.css | 7 ++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index 1db0c732cf..723445cdd2 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -118,7 +118,10 @@ "image-btn-hover": "#189aea", "image-btn-disabled": "#bfccd6", "version-exists": "#458056", - "version-not-found": "#ffc671" + "version-not-found": "#ffc671", + + "source-version": "#D3D8DE", + "source-version-outdated": "#ffc671" } } } diff --git a/openpype/style/style.css b/openpype/style/style.css index e35d8fb220..f5afa0f6cd 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1118,11 +1118,12 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { } #SourceVersionLabel[state="same"] { - border: 1px solid {color:font}; + border: 1px solid {color:settings:source-version}; + color: {color:settings:source-version}; } #SourceVersionLabel[state="different"] { - border: 1px solid {color:font-disabled}; - color: {color:font-disabled}; + border: 1px solid {color:settings:source-version-outdated}; + color: {color:settings:source-version-outdated}; } /* TODO Replace these with explicit widget types if possible */ From 20c9e0fb99122d2519f7bec6a5f012ff691ad68a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Feb 2022 10:59:41 +0100 Subject: [PATCH 14/59] always look into all versions for closes version --- openpype/settings/handlers.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index ac477c9f9b..7d5f1ac152 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -866,12 +866,7 @@ class MongoSettingsHandler(SettingsHandler): return doc["_id"] versioned_settings_by_version[doc["version"]] = doc - if is_running_staging(): - versions_key = self._staging_versions_key - else: - versions_key = self._production_versions_key - - versions_in_doc = versioned_doc.get(versions_key) or [] + versions_in_doc = versioned_doc.get(self._all_versions_keys) or [] # Cases when only legacy settings can be used if ( # There are not versioned documents yet From 33735d487aa38cc2fae4440463deb748ac4739fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Feb 2022 11:02:17 +0100 Subject: [PATCH 15/59] hound fixes --- openpype/settings/entities/root_entities.py | 1 - openpype/settings/handlers.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index d35a990284..edb4407679 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -19,7 +19,6 @@ from .exceptions import ( SchemaError, InvalidKeySymbols ) -from openpype.lib import get_openpype_version from openpype.settings.constants import ( SYSTEM_SETTINGS_KEY, PROJECT_SETTINGS_KEY, diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 7d5f1ac152..4e9689c4c2 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -831,8 +831,6 @@ class MongoSettingsHandler(SettingsHandler): additional_filters(dict): Additional filters of document. Used for project specific settings. """ - from openpype.lib.openpype_version import is_running_staging - # Trigger check of versions self._check_version_order() @@ -928,7 +926,6 @@ class MongoSettingsHandler(SettingsHandler): return None return self.collection.find_one({"_id": doc_id}) - def _find_closest_system_settings(self): return self._find_closest_settings( self._system_settings_key, From ac6238c9385efee5af259c7ff6c54e43a08af4d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Feb 2022 11:39:43 +0100 Subject: [PATCH 16/59] fetch project versions in thread to not stuck UI --- openpype/tools/settings/settings/widgets.py | 63 ++++++++++++++++----- openpype/tools/utils/__init__.py | 4 +- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 4bab67dba5..85610f7c19 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -1,5 +1,6 @@ import os import copy +import uuid from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome from avalon.mongodb import ( @@ -12,7 +13,10 @@ from openpype.tools.utils.widgets import ImageButton from openpype.tools.utils.lib import paint_image_with_color from openpype.widgets.nice_checkbox import NiceCheckbox -from openpype.tools.utils import PlaceholderLineEdit +from openpype.tools.utils import ( + PlaceholderLineEdit, + DynamicQThread +) from openpype.settings.lib import find_closest_version_for_projects from openpype.lib import get_openpype_version from .images import ( @@ -765,6 +769,8 @@ class SettingsNiceCheckbox(NiceCheckbox): class ProjectModel(QtGui.QStandardItemModel): + _update_versions = QtCore.Signal() + def __init__(self, only_active, *args, **kwargs): super(ProjectModel, self).__init__(*args, **kwargs) @@ -775,6 +781,7 @@ class ProjectModel(QtGui.QStandardItemModel): self._only_active = only_active self._default_item = None self._items_by_name = {} + self._versions_by_project = {} colors = get_objected_colors() font_color = colors["font"].get_qcolor() @@ -782,6 +789,33 @@ class ProjectModel(QtGui.QStandardItemModel): self._version_font_color = font_color self._current_version = get_openpype_version() + self._version_refresh_threads = [] + self._version_refresh_id = None + + self._update_versions.connect(self._on_update_versions_signal) + + def _on_update_versions_signal(self): + for project_name, version in self._versions_by_project.items(): + if project_name is None: + item = self._default_item + else: + item = self._items_by_name.get(project_name) + + if item and version != self._current_version: + item.setData(version, PROJECT_VERSION_ROLE) + + def _fetch_settings_versions(self): + """Used versions per project are loaded in thread to not stuck UI.""" + version_refresh_id = self._version_refresh_id + all_project_names = list(self._items_by_name.keys()) + all_project_names.append(None) + closest_by_project_name = find_closest_version_for_projects( + all_project_names + ) + if self._version_refresh_id == version_refresh_id: + self._versions_by_project = closest_by_project_name + self._update_versions.emit() + def flags(self, index): if index.column() == 1: index = self.index(index.row(), 0, index.parent()) @@ -791,6 +825,9 @@ class ProjectModel(QtGui.QStandardItemModel): self.dbcon = dbcon def refresh(self): + # Change id of versions refresh + self._version_refresh_id = uuid.uuid4() + new_items = [] if self._default_item is None: item = QtGui.QStandardItem(DEFAULT_PROJECT_LABEL) @@ -828,20 +865,6 @@ class ProjectModel(QtGui.QStandardItemModel): font.setItalic(True) item.setFont(font) - # TODO this could be threaded - all_project_names = list(project_names) - all_project_names.append(None) - closes_by_project_name = find_closest_version_for_projects( - all_project_names - ) - for project_name, version in closes_by_project_name.items(): - if project_name is None: - item = self._default_item - else: - item = self._items_by_name.get(project_name) - - if item and version != self._current_version: - item.setData(version, PROJECT_VERSION_ROLE) root_item = self.invisibleRootItem() for project_name in tuple(self._items_by_name.keys()): if project_name not in project_names: @@ -851,6 +874,16 @@ class ProjectModel(QtGui.QStandardItemModel): if new_items: root_item.appendRows(new_items) + # Fetch versions per project in thread + thread = DynamicQThread(self._fetch_settings_versions) + self._version_refresh_threads.append(thread) + thread.start() + + # Cleanup done threads + for thread in tuple(self._version_refresh_threads): + if thread.isFinished(): + self._version_refresh_threads.remove(thread) + def data(self, index, role=QtCore.Qt.DisplayRole): if index.column() == 1: if role == QtCore.Qt.TextAlignmentRole: diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 2f13eeb9db..dbe6b27a39 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -10,7 +10,8 @@ from .lib import ( WrappedCallbackItem, paint_image_with_color, get_warning_pixmap, - set_style_property + set_style_property, + DynamicQThread, ) @@ -26,4 +27,5 @@ __all__ = ( "paint_image_with_color", "get_warning_pixmap", "set_style_property", + "DynamicQThread", ) From a646ef49a6094bf818cc926a82b059196351cb93 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Feb 2022 17:41:41 +0100 Subject: [PATCH 17/59] adding stuido name and code to anatomy data --- openpype/lib/avalon_context.py | 28 +++++++++++++++++-- .../publish/collect_anatomy_context_data.py | 16 +++++++++-- openpype/tools/workfiles/app.py | 14 ++++++++-- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 1254580657..b07037bf17 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -9,7 +9,10 @@ import collections import functools import getpass -from openpype.settings import get_project_settings +from openpype.settings import ( + get_project_settings, + get_system_settings +) from .anatomy import Anatomy from .profiles_filtering import filter_profiles @@ -494,6 +497,18 @@ def get_workfile_template_key( return default +def get_system_general_data(): + system_settings = get_system_settings() + studio_name = system_settings["general"]["studio_name"] + studio_code = system_settings["general"]["studio_code"] + return { + "studio": { + "name": studio_name, + "code": studio_code + } + } + + # TODO rename function as is not just "work" specific def get_workdir_data(project_doc, asset_doc, task_name, host_name): """Prepare data for workdir template filling from entered information. @@ -536,6 +551,10 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): "user": getpass.getuser(), "hierarchy": hierarchy, } + + system_general_data = get_system_general_data() + data.update(system_general_data) + return data @@ -1505,7 +1524,7 @@ def _get_task_context_data_for_anatomy( "requested task type: `{}`".format(task_type) ) - return { + data = { "project": { "name": project_doc["name"], "code": project_doc["data"].get("code") @@ -1518,6 +1537,11 @@ def _get_task_context_data_for_anatomy( } } + system_general_data = get_system_general_data() + data.update(system_general_data) + + return data + def get_custom_workfile_template_by_context( template_profiles, project_doc, asset_doc, task_name, anatomy=None diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 07de1b4420..8b6aa3c2a6 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -15,8 +15,10 @@ Provides: import os import json -from openpype.lib import ApplicationManager -from avalon import api, lib +from openpype.settings import ( + get_system_settings +) +from avalon import api import pyblish.api @@ -44,6 +46,10 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): label = "Collect Anatomy Context Data" def process(self, context): + system_settings = get_system_settings() + studio_name = system_settings["general"]["studio_name"] + studio_code = system_settings["general"]["studio_code"] + task_name = api.Session["AVALON_TASK"] project_entity = context.data["projectEntity"] @@ -76,7 +82,11 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): "short": task_code, }, "username": context.data["user"], - "app": context.data["hostName"] + "app": context.data["hostName"], + "studio": { + "name": studio_name, + "code": studio_code + } } datetime_data = context.data.get("datetimeData") or {} diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index b7f9ff8786..884b78f3a3 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -28,7 +28,9 @@ from openpype.lib import ( get_workfile_template_key, create_workdir_extra_folders ) - +from openpype.settings import ( + get_system_settings +) from .model import FilesModel from .view import FilesView @@ -92,6 +94,10 @@ class NameWindow(QtWidgets.QDialog): if asset_parents: parent_name = asset_parents[-1] + system_settings = get_system_settings() + studio_name = system_settings["general"]["studio_name"] + studio_code = system_settings["general"]["studio_code"] + self.data = { "project": { "name": project_doc["name"], @@ -107,7 +113,11 @@ class NameWindow(QtWidgets.QDialog): "version": 1, "user": getpass.getuser(), "comment": "", - "ext": None + "ext": None, + "studio": { + "name": studio_name, + "code": studio_code + } } # Store project anatomy From f386045a0db6f672365e084f7cc2fa860ae5a073 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Feb 2022 20:59:01 +0100 Subject: [PATCH 18/59] use xml format to get information about input file from oiio --- openpype/lib/transcoding.py | 294 +++++++++++++++++++++++++++--------- 1 file changed, 221 insertions(+), 73 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 3d587e2f29..b2db9047e8 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -4,12 +4,35 @@ import logging import collections import tempfile +import xml.etree.ElementTree + from .execute import run_subprocess from .vendor_bin_utils import ( get_oiio_tools_path, is_oiio_supported ) +# Max length of string that is supported by ffmpeg +MAX_FFMPEG_STRING_LEN = 8196 +# OIIO known xml tags +STRING_TAGS = { + "format" +} +INT_TAGS = { + "x", "y", "z", + "width", "height", "depth", + "full_x", "full_y", "full_z", + "full_width", "full_height", "full_depth", + "tile_width", "tile_height", "tile_depth", + "nchannels", + "alpha_channel", + "z_channel", + "deep", + "subimages", +} +# Regex to parse array attributes +ARRAY_TYPE_REGEX = re.compile(r"^(int|float|string)\[\d+\]$") + def get_transcode_temp_directory(): """Creates temporary folder for transcoding. @@ -24,87 +47,211 @@ def get_transcode_temp_directory(): def get_oiio_info_for_input(filepath, logger=None): - """Call oiiotool to get information about input and return stdout.""" - args = [ - get_oiio_tools_path(), "--info", "-v", filepath - ] - return run_subprocess(args, logger=logger) + """Call oiiotool to get information about input and return stdout. - -def parse_oiio_info(oiio_info): - """Create an object based on output from oiiotool. - - Removes quotation marks from compression value. Parse channels into - dictionary - key is channel name value is determined type of channel - (e.g. 'uint', 'float'). - - Args: - oiio_info (str): Output of calling "oiiotool --info -v " - - Returns: - dict: Loaded data from output. + Stdout should contain xml format string. """ - lines = [ - line.strip() - for line in oiio_info.split("\n") + args = [ + get_oiio_tools_path(), "--info", "-v", "-i:infoformat=xml", filepath ] - # Each line should contain information about one key - # key - value are separated with ": " - oiio_sep = ": " - data_map = {} - for line in lines: - parts = line.split(oiio_sep) - if len(parts) < 2: + output = run_subprocess(args, logger=logger) + output = output.replace("\r\n", "\n") + + xml_started = False + lines = [] + for line in output.split("\n"): + if not xml_started: + if not line.startswith("<"): + continue + xml_started = True + if xml_started: + lines.append(line) + + if not xml_started: + raise ValueError( + "Failed to read input file \"{}\".\nOutput:\n{}".format( + filepath, output + ) + ) + + xml_text = "\n".join(lines) + return parse_oiio_xml_output(xml_text, logger=logger) + + +class RationalToInt: + """Rational value stored as division of 2 integers using string.""" + def __init__(self, string_value): + parts = string_value.split("/") + top = float(parts[0]) + bottom = 1.0 + if len(parts) != 1: + bottom = float(parts[1]) + + self._value = top / bottom + self._string_value = string_value + + @property + def value(self): + return self._value + + @property + def string_value(self): + return self._string_value + + def __format__(self, *args, **kwargs): + return self._string_value.__format__(*args, **kwargs) + + def __float__(self): + return self._value + + def __str__(self): + return self._string_value + + def __repr__(self): + return "<{}> {}".format(self.__class__.__name__, self._string_value) + + +def convert_value_by_type_name(value_type, value, logger=None): + """Convert value to proper type based on type name. + + In some cases value types have custom python class. + """ + if logger is None: + logger = logging.getLogger(__name__) + + # Simple types + if value_type == "string": + return value + + if value_type == "int": + return int(value) + + if value_type == "float": + return float(value) + + # Vectors will probably have more types + if value_type == "vec2f": + return [float(item) for item in value.split(",")] + + # Matrix should be always have square size of element 3x3, 4x4 + # - are returned as list of lists + if value_type == "matrix": + output = [] + current_index = -1 + parts = value.split(",") + parts_len = len(parts) + if parts_len == 1: + divisor = 1 + elif parts_len == 4: + divisor = 2 + elif parts_len == 9: + divisor == 3 + elif parts_len == 16: + divisor = 4 + else: + logger.info("Unknown matrix resolution {}. Value: \"{}\"".format( + parts_len, value + )) + for part in parts: + output.append(float(part)) + return output + + for idx, item in enumerate(parts): + list_index = idx % divisor + if list_index > current_index: + current_index = list_index + output.append([]) + output[list_index].append(float(item)) + return output + + if value_type == "rational2i": + return RationalToInt(value) + + # Array of other types is converted to list + re_result = ARRAY_TYPE_REGEX.findall(value_type) + if re_result: + array_type = re_result[0] + output = [] + for item in value.split(","): + output.append(convert_value_by_type_name(array_type, item, logger=logger)) + return output + + logger.info(( + "MISSING IMPLEMENTATION:" + " Unknown attrib type \"{}\". Value: {}" + ).format(value_type, value)) + return value + + +def parse_oiio_xml_output(xml_string, logger=None): + """Parse xml output from OIIO info command.""" + output = {} + if not xml_string: + return output + + if logger is None: + logger = logging.getLogger("OIIO-xml-parse") + + tree = xml.etree.ElementTree.fromstring(xml_string) + attribs = {} + output["attribs"] = attribs + for child in tree: + tag_name = child.tag + if tag_name == "attrib": + attrib_def = child.attrib + value = convert_value_by_type_name(attrib_def["type"], child.text, logger=logger) + + attribs[attrib_def["name"]] = value continue - key = parts.pop(0) - value = oiio_sep.join(parts) - data_map[key] = value - if "compression" in data_map: - value = data_map["compression"] - data_map["compression"] = value.replace("\"", "") + # Channels are stored as tex on each child + if tag_name == "channelnames": + value = [] + for channel in child: + value.append(channel.text) - channels_info = {} - channels_value = data_map.get("channel list") or "" - if channels_value: - channels = channels_value.split(", ") - type_regex = re.compile(r"(?P[^\(]+) \((?P[^\)]+)\)") - for channel in channels: - match = type_regex.search(channel) - if not match: - channel_name = channel - channel_type = "uint" - else: - channel_name = match.group("name") - channel_type = match.group("type") - channels_info[channel_name] = channel_type - data_map["channels_info"] = channels_info - return data_map + # Convert known integer type tags to int + elif tag_name in INT_TAGS: + value = int(child.text) + + # Keep value of known string tags + elif tag_name in STRING_TAGS: + value = child.text + + # Keep value as text for unknown tags + # - feel free to add more tags + else: + value = child.text + logger.info(( + "MISSING IMPLEMENTATION:" + " Unknown tag \"{}\". Value \"{}\"" + ).format(tag_name, value)) + + output[child.tag] = value + + return output -def get_convert_rgb_channels(channels_info): +def get_convert_rgb_channels(channel_names): """Get first available RGB(A) group from channels info. ## Examples ``` # Ideal situation - channels_info: { - "R": ..., - "G": ..., - "B": ..., - "A": ... + channels_info: [ + "R", "G", "B", "A" } ``` Result will be `("R", "G", "B", "A")` ``` # Not ideal situation - channels_info: { - "beauty.red": ..., - "beuaty.green": ..., - "beauty.blue": ..., - "depth.Z": ... - } + channels_info: [ + "beauty.red", + "beuaty.green", + "beauty.blue", + "depth.Z" + ] ``` Result will be `("beauty.red", "beauty.green", "beauty.blue", None)` @@ -116,7 +263,7 @@ def get_convert_rgb_channels(channels_info): """ rgb_by_main_name = collections.defaultdict(dict) main_name_order = [""] - for channel_name in channels_info.keys(): + for channel_name in channel_names: name_parts = channel_name.split(".") rgb_part = name_parts.pop(-1).lower() main_name = ".".join(name_parts) @@ -166,17 +313,18 @@ def should_convert_for_ffmpeg(src_filepath): return None # Load info about info from oiio tool - oiio_info = get_oiio_info_for_input(src_filepath) - input_info = parse_oiio_info(oiio_info) + input_info = get_oiio_info_for_input(src_filepath) + if not input_info: + return None # Check compression - compression = input_info["compression"] + compression = input_info["attribs"].get("compression") if compression in ("dwaa", "dwab"): return True # Check channels - channels_info = input_info["channels_info"] - review_channels = get_convert_rgb_channels(channels_info) + channel_names = input_info["channelnames"] + review_channels = get_convert_rgb_channels(channel_names) if review_channels is None: return None @@ -221,12 +369,11 @@ def convert_for_ffmpeg( if input_frame_start is not None and input_frame_end is not None: is_sequence = int(input_frame_end) != int(input_frame_start) - oiio_info = get_oiio_info_for_input(first_input_path) - input_info = parse_oiio_info(oiio_info) + input_info = get_oiio_info_for_input(first_input_path) # Change compression only if source compression is "dwaa" or "dwab" # - they're not supported in ffmpeg - compression = input_info["compression"] + compression = input_info["attribs"].get("compression") if compression in ("dwaa", "dwab"): compression = "none" @@ -237,8 +384,9 @@ def convert_for_ffmpeg( first_input_path ] - channels_info = input_info["channels_info"] - review_channels = get_convert_rgb_channels(channels_info) + # Collect channels to export + channel_names = input_info["channelnames"] + review_channels = get_convert_rgb_channels(channel_names) if review_channels is None: raise ValueError( "Couldn't find channels that can be used for conversion." From 13238a4eedbca7cc7f5894bfaf3aae36482271dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Feb 2022 20:59:32 +0100 Subject: [PATCH 19/59] add attribute string length check for ffmpeg conversion --- openpype/lib/transcoding.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index b2db9047e8..f5fd254abb 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -328,6 +328,12 @@ def should_convert_for_ffmpeg(src_filepath): if review_channels is None: return None + for attr_value in input_info["attribs"].values(): + if ( + isinstance(attr_value, str) + and len(attr_value) > MAX_FFMPEG_STRING_LEN + ): + return True return False @@ -404,6 +410,25 @@ def convert_for_ffmpeg( oiio_cmd.append("--frames") oiio_cmd.append("{}-{}".format(input_frame_start, input_frame_end)) + ignore_attr_changes_added = False + for attr_name, attr_value in input_info["attribs"].items(): + if not isinstance(attr_value, str): + continue + + # Remove attributes that have string value longer than allowed length + # for ffmpeg + if len(attr_value) > MAX_FFMPEG_STRING_LEN: + if not ignore_attr_changes_added: + # Attrite changes won't be added to attributes itself + ignore_attr_changes_added = True + oiio_cmd.append("--sansattrib") + # Set attribute to empty string + logger.info(( + "Removed attribute \"{}\" from metadata" + " because has too long value ({} chars)." + ).format(attr_name, len(attr_value))) + oiio_cmd.extend(["--eraseattrib", attr_name]) + # Add last argument - path to output base_file_name = os.path.basename(first_input_path) output_path = os.path.join(output_dir, base_file_name) From d4f4027691887eed1c8c17db195b1f9a3aed5f14 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Feb 2022 20:59:56 +0100 Subject: [PATCH 20/59] few improvements of oiio conversions --- openpype/lib/transcoding.py | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index f5fd254abb..17fda182df 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -340,8 +340,8 @@ def should_convert_for_ffmpeg(src_filepath): def convert_for_ffmpeg( first_input_path, output_dir, - input_frame_start, - input_frame_end, + input_frame_start=None, + input_frame_end=None, logger=None ): """Contert source file to format supported in ffmpeg. @@ -384,11 +384,10 @@ def convert_for_ffmpeg( compression = "none" # Prepare subprocess arguments - oiio_cmd = [ - get_oiio_tools_path(), - "--compression", compression, - first_input_path - ] + oiio_cmd = [get_oiio_tools_path()] + # Add input compression if available + if compression: + oiio_cmd.extend(["--compression", compression]) # Collect channels to export channel_names = input_info["channelnames"] @@ -399,16 +398,27 @@ def convert_for_ffmpeg( ) red, green, blue, alpha = review_channels + input_channels = [red, green, blue] channels_arg = "R={},G={},B={}".format(red, green, blue) if alpha is not None: channels_arg += ",A={}".format(alpha) - oiio_cmd.append("--ch") - oiio_cmd.append(channels_arg) + input_channels.append(alpha) + input_channels_str = ",".join(input_channels) + + oiio_cmd.extend([ + # Tell oiiotool which channels should be loaded + # - other channels are not loaded to memory so helps to avoid memory + # leak issues + "-i:ch={}".format(input_channels_str), first_input_path, + # Tell oiiotool which channels should be put to top stack (and output) + "--ch", channels_arg + ]) # Add frame definitions to arguments if is_sequence: - oiio_cmd.append("--frames") - oiio_cmd.append("{}-{}".format(input_frame_start, input_frame_end)) + oiio_cmd.extend([ + "--frames", "{}-{}".format(input_frame_start, input_frame_end) + ]) ignore_attr_changes_added = False for attr_name, attr_value in input_info["attribs"].items(): @@ -432,8 +442,9 @@ def convert_for_ffmpeg( # Add last argument - path to output base_file_name = os.path.basename(first_input_path) output_path = os.path.join(output_dir, base_file_name) - oiio_cmd.append("-o") - oiio_cmd.append(output_path) + oiio_cmd.extend([ + "-o", output_path + ]) logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) From 44647c04dcaa95dba181f60de7be15f0d18ee405 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Feb 2022 21:59:47 +0100 Subject: [PATCH 21/59] hound fixes --- openpype/lib/transcoding.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 17fda182df..36f6858a78 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -173,7 +173,9 @@ def convert_value_by_type_name(value_type, value, logger=None): array_type = re_result[0] output = [] for item in value.split(","): - output.append(convert_value_by_type_name(array_type, item, logger=logger)) + output.append( + convert_value_by_type_name(array_type, item, logger=logger) + ) return output logger.info(( @@ -199,7 +201,9 @@ def parse_oiio_xml_output(xml_string, logger=None): tag_name = child.tag if tag_name == "attrib": attrib_def = child.attrib - value = convert_value_by_type_name(attrib_def["type"], child.text, logger=logger) + value = convert_value_by_type_name( + attrib_def["type"], child.text, logger=logger + ) attribs[attrib_def["name"]] = value continue From ea2c16a378ee24b95510db02618fd0f50c2db311 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 3 Feb 2022 13:46:16 +0100 Subject: [PATCH 22/59] turn public to nonpublic function --- openpype/lib/avalon_context.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index b07037bf17..694aa376b1 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -497,7 +497,7 @@ def get_workfile_template_key( return default -def get_system_general_data(): +def _get_system_general_data(): system_settings = get_system_settings() studio_name = system_settings["general"]["studio_name"] studio_code = system_settings["general"]["studio_code"] @@ -552,7 +552,7 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): "hierarchy": hierarchy, } - system_general_data = get_system_general_data() + system_general_data = _get_system_general_data() data.update(system_general_data) return data @@ -1537,7 +1537,7 @@ def _get_task_context_data_for_anatomy( } } - system_general_data = get_system_general_data() + system_general_data = _get_system_general_data() data.update(system_general_data) return data From c4ef62121b9d6cecc48f8ff64e17b86117baf0ec Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Feb 2022 17:07:54 +0100 Subject: [PATCH 23/59] OP-2579 - updated settings for Webpublisher Changed settings to list instead of dictionary to allow single frame and multiframe sequences into same family --- .../publish/collect_published_files.py | 29 ++++++++++++------- .../schema_project_webpublisher.json | 14 +++++++-- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index c1b1d66cb8..e04f207c01 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -34,7 +34,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): targets = ["filespublish"] # from Settings - task_type_to_family = {} + task_type_to_family = [] def process(self, context): batch_dir = context.data["batchDir"] @@ -160,12 +160,21 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): assert task_obj, "No family configuration for '{}'".format(task_type) found_family = None - for family, content in task_obj.items(): - if is_sequence != content["is_sequence"]: + families_config = [] + # backward compatibility, should be removed pretty soon + if isinstance(task_obj, dict): + for family, config in task_obj: + config["result_family"] = family + families_config.append(config) + else: + families_config = task_obj + + for config in families_config: + if is_sequence != config["is_sequence"]: continue - if extension in content["extensions"] or \ - '' in content["extensions"]: # all extensions setting - found_family = family + if (extension in config["extensions"] or + '' in config["extensions"]): # all extensions setting + found_family = config["result_family"] break msg = "No family found for combination of " +\ @@ -173,10 +182,10 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): task_type, is_sequence, extension) assert found_family, msg - return found_family, \ - content["families"], \ - content["subset_template_name"], \ - content["tags"] + return (found_family, + config["families"], + config["subset_template_name"], + config["tags"]) def _get_last_version(self, asset_name, subset_name): """Returns version number or 0 for 'asset' and 'subset'""" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json index 78f38f111d..d15140db4d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json @@ -24,10 +24,10 @@ "label": "Task type to family mapping", "collapsible_key": true, "object_type": { - "type": "dict-modifiable", - "collapsible": false, + "type": "list", + "collapsible": true, "key": "task_type", - "collapsible_key": false, + "collapsible_key": true, "object_type": { "type": "dict", "children": [ @@ -52,6 +52,14 @@ "type": "schema", "name": "schema_representation_tags" }, + { + "type": "separator" + }, + { + "type": "text", + "key": "result_family", + "label": "Resulting family" + }, { "type": "text", "key": "subset_template_name", From b4fbee9bd2a5fdaf4e44c3ce0f2b01b44ca30b10 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Feb 2022 17:24:53 +0100 Subject: [PATCH 24/59] OP-2579 - updated settings's defaults --- .../project_settings/webpublisher.json | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/openpype/settings/defaults/project_settings/webpublisher.json b/openpype/settings/defaults/project_settings/webpublisher.json index 9db98acd5a..2ab135e59b 100644 --- a/openpype/settings/defaults/project_settings/webpublisher.json +++ b/openpype/settings/defaults/project_settings/webpublisher.json @@ -2,17 +2,18 @@ "publish": { "CollectPublishedFiles": { "task_type_to_family": { - "Animation": { - "workfile": { + "Animation": [ + { "is_sequence": false, "extensions": [ "tvp" ], "families": [], "tags": [], - "subset_template_name": "" + "result_family": "workfile", + "subset_template_name": "{family}{Variant}" }, - "render": { + { "is_sequence": true, "extensions": [ "png", @@ -26,20 +27,22 @@ "tags": [ "review" ], - "subset_template_name": "" + "result_family": "render", + "subset_template_name": "{family}{Variant}" } - }, - "Compositing": { - "workfile": { + ], + "Compositing": [ + { "is_sequence": false, "extensions": [ "aep" ], "families": [], "tags": [], - "subset_template_name": "" + "result_family": "workfile", + "subset_template_name": "{family}{Variant}" }, - "render": { + { "is_sequence": true, "extensions": [ "png", @@ -53,20 +56,22 @@ "tags": [ "review" ], - "subset_template_name": "" + "result_family": "render", + "subset_template_name": "{family}{Variant}" } - }, - "Layout": { - "workfile": { + ], + "Layout": [ + { "is_sequence": false, "extensions": [ "psd" ], "families": [], "tags": [], - "subset_template_name": "" + "result_family": "workfile", + "subset_template_name": "{family}{Variant}" }, - "image": { + { "is_sequence": false, "extensions": [ "png", @@ -81,20 +86,23 @@ "tags": [ "review" ], - "subset_template_name": "" + "result_family": "image", + "subset_template_name": "{family}{Variant}" } - }, - "default_task_type": { - "workfile": { + ], + "default_task_type": [ + { "is_sequence": false, "extensions": [ - "tvp" + "tvp", + "psd" ], "families": [], "tags": [], + "result_family": "workfile", "subset_template_name": "{family}{Variant}" }, - "render": { + { "is_sequence": true, "extensions": [ "png", @@ -108,9 +116,10 @@ "tags": [ "review" ], + "result_family": "render", "subset_template_name": "{family}{Variant}" } - }, + ], "__dynamic_keys_labels__": { "default_task_type": "Default task type" } From 5f30e92e1351d92b501429d91b06bb7d5aa74954 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Feb 2022 16:45:29 +0100 Subject: [PATCH 25/59] do not skip applications without host implementation --- openpype/settings/entities/enum_entity.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 0fcd8f3002..54e09cd823 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -263,8 +263,14 @@ class HostsEnumEntity(BaseEnumEntity): class AppsEnumEntity(BaseEnumEntity): + """Enum of applications for project anatomy attributes.""" schema_types = ["apps-enum"] + _skip_app_groups = [ + # DJV make sense to be launched on representation level + "djvview" + ] + def _item_initialization(self): self.multiselection = True self.value_on_not_set = [] @@ -284,8 +290,7 @@ class AppsEnumEntity(BaseEnumEntity): if enabled_entity and not enabled_entity.value: continue - host_name_entity = app_group.get("host_name") - if not host_name_entity or not host_name_entity.value: + if app_group.key in self._skip_app_groups: continue group_label = app_group["label"].value From b4924e5d406db7d64db2a98454fcd5334c69fe97 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Feb 2022 16:46:54 +0100 Subject: [PATCH 26/59] removed shell application --- .../system_settings/applications.json | 52 ------------------- .../host_settings/schema_shell.json | 35 ------------- .../system_schema/schema_applications.json | 4 -- 3 files changed, 91 deletions(-) delete mode 100644 openpype/settings/entities/schemas/system_schema/host_settings/schema_shell.json diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 4a8b6d82a2..b1f84944c5 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1171,58 +1171,6 @@ } } }, - "shell": { - "enabled": true, - "environment": {}, - "variants": { - "python_3-7": { - "use_python_2": true, - "executables": { - "windows": [], - "darwin": [], - "linux": [] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, - "environment": {} - }, - "python_2-7": { - "use_python_2": true, - "executables": { - "windows": [], - "darwin": [], - "linux": [] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, - "environment": {} - }, - "terminal": { - "use_python_2": true, - "executables": { - "windows": [], - "darwin": [], - "linux": [] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, - "environment": {} - }, - "__dynamic_keys_labels__": { - "python_3-7": "Python 3.7", - "python_2-7": "Python 2.7" - } - } - }, "djvview": { "enabled": true, "label": "DJV View", diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_shell.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_shell.json deleted file mode 100644 index 986f83a9fc..0000000000 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_shell.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "type": "dict", - "key": "shell", - "label": "Shell", - "collapsible": true, - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "key": "environment", - "label": "Environment", - "type": "raw-json" - }, - { - "type": "dict-modifiable", - "key": "variants", - "collapsible_key": true, - "use_label_wrap": false, - "object_type": { - "type": "dict", - "collapsible": true, - "children": [ - { - "type": "schema_template", - "name": "template_host_variant_items" - } - ] - } - } - ] -} diff --git a/openpype/settings/entities/schemas/system_schema/schema_applications.json b/openpype/settings/entities/schemas/system_schema/schema_applications.json index 1767250aae..dcca9a186f 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_applications.json +++ b/openpype/settings/entities/schemas/system_schema/schema_applications.json @@ -85,10 +85,6 @@ "type": "schema", "name": "schema_unreal" }, - { - "type": "schema", - "name": "schema_shell" - }, { "type": "schema", "name": "schema_djv" From b01bf9c91fd7a0b43e8e9dbb84e31d13b15a9f77 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Feb 2022 17:03:38 +0100 Subject: [PATCH 27/59] all applications can be set on project but custom launch applications are defined in applications lib --- openpype/lib/applications.py | 3 +++ .../ftrack/event_handlers_user/action_applications.py | 8 ++++++-- openpype/settings/entities/enum_entity.py | 8 -------- openpype/tools/launcher/models.py | 11 +++++++++-- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index a704c3ae68..aa37abfdd3 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -44,6 +44,9 @@ _logger = None PLATFORM_NAMES = {"windows", "linux", "darwin"} DEFAULT_ENV_SUBGROUP = "standard" +CUSTOM_LAUNCH_APP_GROUPS = { + "djvview" +} def parse_environments(env_data, env_group=None, platform_name=None): diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_applications.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_applications.py index 6d45d43958..48a0dea006 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_applications.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_applications.py @@ -2,10 +2,11 @@ import os from uuid import uuid4 from openpype_modules.ftrack.lib import BaseAction -from openpype.lib import ( +from openpype.lib.applications import ( ApplicationManager, ApplicationLaunchFailed, - ApplictionExecutableNotFound + ApplictionExecutableNotFound, + CUSTOM_LAUNCH_APP_GROUPS ) from avalon.api import AvalonMongoDB @@ -136,6 +137,9 @@ class AppplicationsAction(BaseAction): if not app or not app.enabled: continue + if app.group.name in CUSTOM_LAUNCH_APP_GROUPS: + continue + app_icon = app.icon if app_icon and self.icon_url: try: diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 54e09cd823..010377426a 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -266,11 +266,6 @@ class AppsEnumEntity(BaseEnumEntity): """Enum of applications for project anatomy attributes.""" schema_types = ["apps-enum"] - _skip_app_groups = [ - # DJV make sense to be launched on representation level - "djvview" - ] - def _item_initialization(self): self.multiselection = True self.value_on_not_set = [] @@ -290,9 +285,6 @@ class AppsEnumEntity(BaseEnumEntity): if enabled_entity and not enabled_entity.value: continue - if app_group.key in self._skip_app_groups: - continue - group_label = app_group["label"].value variants_entity = app_group["variants"] for variant_name, variant_entity in variants_entity.items(): diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 6ade9d33ed..02aeb094e3 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -15,8 +15,12 @@ from .constants import ( from .actions import ApplicationAction from Qt import QtCore, QtGui from avalon.vendor import qtawesome -from avalon import style, api -from openpype.lib import ApplicationManager, JSONSettingRegistry +from avalon import api +from openpype.lib import JSONSettingRegistry +from openpype.lib.applications import ( + CUSTOM_LAUNCH_APP_GROUPS, + ApplicationManager +) log = logging.getLogger(__name__) @@ -72,6 +76,9 @@ class ActionModel(QtGui.QStandardItemModel): if not app or not app.enabled: continue + if app.group.name in CUSTOM_LAUNCH_APP_GROUPS: + continue + # Get from app definition, if not there from app in project action = type( "app_{}".format(app_name), From c419b5cea07fb392e894d6c231a19f941cbf5540 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Feb 2022 17:10:59 +0100 Subject: [PATCH 28/59] ftrack will add even applications without host implementation --- .../modules/default_modules/ftrack/lib/custom_attributes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/ftrack/lib/custom_attributes.py b/openpype/modules/default_modules/ftrack/lib/custom_attributes.py index 53facd4ab2..8aab769009 100644 --- a/openpype/modules/default_modules/ftrack/lib/custom_attributes.py +++ b/openpype/modules/default_modules/ftrack/lib/custom_attributes.py @@ -17,7 +17,7 @@ def default_custom_attributes_definition(): 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: + if app.enabled: _app_definitions.append( (app_name, app.full_label) ) From 437bcbe6290ff53c9d13cb628295aabfcafd147d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Feb 2022 19:05:29 +0100 Subject: [PATCH 29/59] Change data only on ApplicationAction classes --- openpype/tools/launcher/models.py | 2 +- openpype/tools/launcher/widgets.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 6ade9d33ed..1bdaf392e1 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -15,7 +15,7 @@ from .constants import ( from .actions import ApplicationAction from Qt import QtCore, QtGui from avalon.vendor import qtawesome -from avalon import style, api +from avalon import api from openpype.lib import ApplicationManager, JSONSettingRegistry log = logging.getLogger(__name__) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index ba0d9dd6b5..5dad41c349 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -6,6 +6,7 @@ from avalon.vendor import qtawesome from .delegates import ActionDelegate from . import lib +from .actions import ApplicationAction from .models import ActionModel from openpype.tools.flickcharm import FlickCharm from .constants import ( @@ -239,10 +240,12 @@ class ActionBar(QtWidgets.QWidget): is_variant_group = index.data(VARIANT_GROUP_ROLE) if not is_group and not is_variant_group: action = index.data(ACTION_ROLE) - if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE): - action.data["start_last_workfile"] = False - else: - action.data.pop("start_last_workfile", None) + # Change data of application action + if issubclass(action, ApplicationAction): + if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE): + action.data["start_last_workfile"] = False + else: + action.data.pop("start_last_workfile", None) self._start_animation(index) self.action_clicked.emit(action) return From d94a61682b4ed2be7c5ee1d0a68d1556072a8319 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 4 Feb 2022 19:25:06 +0100 Subject: [PATCH 30/59] Fix typos, tweak docstring --- .../maya/plugins/publish/validate_frame_range.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_frame_range.py b/openpype/hosts/maya/plugins/publish/validate_frame_range.py index d5009701f2..98b5b4d79b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/maya/plugins/publish/validate_frame_range.py @@ -5,15 +5,16 @@ from maya import cmds class ValidateFrameRange(pyblish.api.InstancePlugin): - """Valides the frame ranges. + """Validates the frame ranges. - This is optional validator checking if the frame range on instance - matches the one of asset. It also validate render frame range of render - layers + This is an optional validator checking if the frame range on instance + matches the frame range specified for the asset. - Repair action will change everything to match asset. + It also validates render frame ranges of render layers. - This can be turned off by artist to allow custom ranges. + Repair action will change everything to match the asset frame range. + + This can be turned off by the artist to allow custom ranges. """ label = "Validate Frame Range" From 42c6d58fe5b2c39ab66b8f5c678c3dab40d75d82 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 4 Feb 2022 19:30:17 +0100 Subject: [PATCH 31/59] Expose Maya Validate Frame Range in settings --- openpype/settings/defaults/project_settings/maya.json | 5 +++++ .../projects_schema/schemas/schema_maya_publish.json | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 52b8db058c..fbe5d255a7 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -170,6 +170,11 @@ "optional": true, "active": true }, + "ValidateFrameRange": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateShaderName": { "enabled": false, "regex": "(?P.*)_(.*)_SHD" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 5a47d688b5..74148219d3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -48,6 +48,16 @@ } ] }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateFrameRange", + "label": "Validate Frame Range" + } + ] + }, { "type": "dict", "collapsible": true, From 76bf19a2351371389e2702c998f5a1c012850788 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Feb 2022 11:52:15 +0100 Subject: [PATCH 32/59] added additional applications to system settings --- openpype/lib/applications.py | 42 ++++++++++++++++-- .../system_settings/applications.json | 3 +- openpype/settings/entities/enum_entity.py | 20 ++++++++- .../system_schema/schema_applications.json | 43 +++++++++++++++++++ openpype/settings/lib.py | 19 +++++++- .../settings/local_settings/apps_widget.py | 9 ++++ 6 files changed, 128 insertions(+), 8 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index aa37abfdd3..afa0a31af1 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -14,8 +14,7 @@ import six from openpype.settings import ( get_system_settings, - get_project_settings, - get_environments + get_project_settings ) from openpype.settings.constants import ( METADATA_KEYS, @@ -29,8 +28,7 @@ from .profiles_filtering import filter_profiles from .local_settings import get_openpype_username from .avalon_context import ( get_workdir_data, - get_workdir_with_workdir_data, - get_workfile_template_key + get_workdir_with_workdir_data ) from .python_module_tools import ( @@ -408,11 +406,47 @@ class ApplicationManager: clear_metadata=False, exclude_locals=False ) + all_app_defs = {} + # Prepare known applications app_defs = settings["applications"] + additional_apps = {} for group_name, variant_defs in app_defs.items(): if group_name in METADATA_KEYS: continue + if group_name == "additional_apps": + additional_apps = variant_defs + else: + all_app_defs[group_name] = variant_defs + + # Prepare additional applications + # - First find dynamic keys that can be used as labels of group + dynamic_keys = {} + for group_name, variant_defs in additional_apps.items(): + if group_name == M_DYNAMIC_KEY_LABEL: + dynamic_keys = variant_defs + break + + # Add additional apps to known applications + for group_name, variant_defs in additional_apps.items(): + if group_name in METADATA_KEYS: + continue + + # Determine group label + label = variant_defs.get("label") + if not label: + # Look for label set in dynamic labels + label = dynamic_keys.get(group_name) + if not label: + label = group_name + variant_defs["label"] = label + + all_app_defs[group_name] = variant_defs + + for group_name, variant_defs in all_app_defs.items(): + if group_name in METADATA_KEYS: + continue + group = ApplicationGroup(group_name, variant_defs, self) self.app_groups[group_name] = group for app in group: diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index b1f84944c5..e6b9ccdc67 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1196,5 +1196,6 @@ "1-1": "1.1" } } - } + }, + "additional_apps": {} } \ No newline at end of file diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 010377426a..92a397afba 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -280,12 +280,30 @@ class AppsEnumEntity(BaseEnumEntity): valid_keys = set() enum_items_list = [] applications_entity = system_settings_entity["applications"] + app_entities = {} + additional_app_names = set() + additional_apps_entity = None for group_name, app_group in applications_entity.items(): + if group_name != "additional_apps": + app_entities[group_name] = app_group + continue + + additional_apps_entity = app_group + for _group_name, _group in app_group.items(): + additional_app_names.add(_group_name) + app_entities[_group_name] = _group + + for group_name, app_group in app_entities.items(): enabled_entity = app_group.get("enabled") if enabled_entity and not enabled_entity.value: continue - group_label = app_group["label"].value + if group_name in additional_app_names: + group_label = additional_apps_entity.get_key_label(group_name) + if not group_label: + group_label = group_name + else: + group_label = app_group["label"].value variants_entity = app_group["variants"] for variant_name, variant_entity in variants_entity.items(): enabled_entity = variant_entity.get("enabled") diff --git a/openpype/settings/entities/schemas/system_schema/schema_applications.json b/openpype/settings/entities/schemas/system_schema/schema_applications.json index dcca9a186f..20be33320d 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_applications.json +++ b/openpype/settings/entities/schemas/system_schema/schema_applications.json @@ -88,6 +88,49 @@ { "type": "schema", "name": "schema_djv" + }, + { + "type": "dict-modifiable", + "key": "additional_apps", + "label": "Additional", + "collapsible": true, + "collapsible_key": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "schema_template", + "name": "template_host_unchangables", + "skip_paths": ["host_name", "label"] + }, + { + "key": "environment", + "label": "Environment", + "type": "raw-json" + }, + { + "type": "dict-modifiable", + "key": "variants", + "collapsible_key": true, + "use_label_wrap": false, + "object_type": { + "type": "dict", + "collapsible": true, + "children": [ + { + "type": "schema_template", + "name": "template_host_variant_items" + } + ] + } + } + ] + } } ] } diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 1b5682536a..596c6628e0 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -580,11 +580,26 @@ def apply_local_settings_on_system_settings(system_settings, local_settings): return current_platform = platform.system().lower() + apps_settings = system_settings["applications"] + additional_apps = apps_settings["additional_apps"] for app_group_name, value in local_settings["applications"].items(): - if not value or app_group_name not in system_settings["applications"]: + if not value: continue - variants = system_settings["applications"][app_group_name]["variants"] + if ( + app_group_name not in apps_settings + and app_group_name not in additional_apps + ): + continue + + if app_group_name in apps_settings: + variants = apps_settings[app_group_name]["variants"] + + else: + variants = ( + apps_settings["additional_apps"][app_group_name]["variants"] + ) + for app_name, app_value in value.items(): if ( not app_value diff --git a/openpype/tools/settings/local_settings/apps_widget.py b/openpype/tools/settings/local_settings/apps_widget.py index 28bc726300..344979218a 100644 --- a/openpype/tools/settings/local_settings/apps_widget.py +++ b/openpype/tools/settings/local_settings/apps_widget.py @@ -180,7 +180,16 @@ class LocalApplicationsWidgets(QtWidgets.QWidget): self.content_layout.removeItem(item) self.widgets_by_group_name.clear() + app_items = {} for key, entity in self.system_settings_entity["applications"].items(): + if key != "additional_apps": + app_items[key] = entity + continue + + for _key, _entity in entity.items(): + app_items[_key] = _entity + + for key, entity in app_items.items(): # Filter not enabled app groups if not entity["enabled"].value: continue From 5c1a1a769023378408ecb6abd8b0e373db5636ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Feb 2022 11:52:30 +0100 Subject: [PATCH 33/59] skip actions without label --- openpype/tools/launcher/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 02aeb094e3..ecee8b1575 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -320,7 +320,7 @@ class ActionModel(QtGui.QStandardItemModel): action = action[0] compare_data = {} - if action: + if action and action.label: compare_data = { "app_label": action.label.lower(), "project_name": self.dbcon.Session["AVALON_PROJECT"], From 24d85facd56c688f776cb10fe0297b311aad353a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Feb 2022 12:05:17 +0100 Subject: [PATCH 34/59] processing comment on pr https://github.com/pypeclub/OpenPype/pull/2630#discussion_r799396455 --- openpype/lib/__init__.py | 2 ++ openpype/lib/avalon_context.py | 28 +++++++++---------- .../publish/collect_anatomy_context_data.py | 14 ++++++---- openpype/tools/workfiles/app.py | 21 +++++--------- 4 files changed, 31 insertions(+), 34 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 7dd9a8793b..ebe7648ad7 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -84,6 +84,7 @@ from .avalon_context import ( get_hierarchy, get_linked_assets, get_latest_version, + get_system_general_anatomy_data, get_workfile_template_key, get_workfile_template_key_from_context, @@ -222,6 +223,7 @@ __all__ = [ "get_hierarchy", "get_linked_assets", "get_latest_version", + "get_system_general_anatomy_data", "get_workfile_template_key", "get_workfile_template_key_from_context", diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 694aa376b1..3ce205c499 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -261,6 +261,18 @@ def get_hierarchy(asset_name=None): return "/".join(hierarchy_items) +def get_system_general_anatomy_data(): + system_settings = get_system_settings() + studio_name = system_settings["general"]["studio_name"] + studio_code = system_settings["general"]["studio_code"] + return { + "studio": { + "name": studio_name, + "code": studio_code + } + } + + def get_linked_asset_ids(asset_doc): """Return linked asset ids for `asset_doc` from DB @@ -497,18 +509,6 @@ def get_workfile_template_key( return default -def _get_system_general_data(): - system_settings = get_system_settings() - studio_name = system_settings["general"]["studio_name"] - studio_code = system_settings["general"]["studio_code"] - return { - "studio": { - "name": studio_name, - "code": studio_code - } - } - - # TODO rename function as is not just "work" specific def get_workdir_data(project_doc, asset_doc, task_name, host_name): """Prepare data for workdir template filling from entered information. @@ -552,7 +552,7 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): "hierarchy": hierarchy, } - system_general_data = _get_system_general_data() + system_general_data = get_system_general_anatomy_data() data.update(system_general_data) return data @@ -1537,7 +1537,7 @@ def _get_task_context_data_for_anatomy( } } - system_general_data = _get_system_general_data() + system_general_data = get_system_general_anatomy_data() data.update(system_general_data) return data diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 8b6aa3c2a6..03db64f191 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -12,12 +12,14 @@ Provides: context -> anatomyData """ -import os import json from openpype.settings import ( get_system_settings ) +from openpype.lib import ( + get_system_general_anatomy_data +) from avalon import api import pyblish.api @@ -82,13 +84,13 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): "short": task_code, }, "username": context.data["user"], - "app": context.data["hostName"], - "studio": { - "name": studio_name, - "code": studio_code - } + "app": context.data["hostName"] } + # add system general settings anatomy data + system_general_data = get_system_general_anatomy_data() + context_data.update(system_general_data) + datetime_data = context.data.get("datetimeData") or {} context_data.update(datetime_data) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 884b78f3a3..40edec76bd 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -21,15 +21,12 @@ from openpype.tools.utils.tasks_widget import TasksWidget from openpype.tools.utils.delegates import PrettyTimeDelegate from openpype.lib import ( Anatomy, - get_workdir, get_workfile_doc, create_workfile_doc, save_workfile_data_to_doc, get_workfile_template_key, - create_workdir_extra_folders -) -from openpype.settings import ( - get_system_settings + create_workdir_extra_folders, + get_system_general_anatomy_data ) from .model import FilesModel from .view import FilesView @@ -94,10 +91,6 @@ class NameWindow(QtWidgets.QDialog): if asset_parents: parent_name = asset_parents[-1] - system_settings = get_system_settings() - studio_name = system_settings["general"]["studio_name"] - studio_code = system_settings["general"]["studio_code"] - self.data = { "project": { "name": project_doc["name"], @@ -113,13 +106,13 @@ class NameWindow(QtWidgets.QDialog): "version": 1, "user": getpass.getuser(), "comment": "", - "ext": None, - "studio": { - "name": studio_name, - "code": studio_code - } + "ext": None } + # add system general settings anatomy data + system_general_data = get_system_general_anatomy_data() + self.data.update(system_general_data) + # Store project anatomy self.anatomy = anatomy self.template = anatomy.templates[template_key]["file"] From d6350df7aa0913600a3adac4cfbed63f93f1b0cc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Feb 2022 12:06:30 +0100 Subject: [PATCH 35/59] hound catches --- openpype/plugins/publish/collect_anatomy_context_data.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 03db64f191..b0474b93ce 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -13,10 +13,6 @@ Provides: """ import json - -from openpype.settings import ( - get_system_settings -) from openpype.lib import ( get_system_general_anatomy_data ) @@ -48,9 +44,6 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): label = "Collect Anatomy Context Data" def process(self, context): - system_settings = get_system_settings() - studio_name = system_settings["general"]["studio_name"] - studio_code = system_settings["general"]["studio_code"] task_name = api.Session["AVALON_TASK"] From 026308feab67093f08ba1c0f8f5e20f691c51723 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Feb 2022 14:33:59 +0100 Subject: [PATCH 36/59] fixed click on default --- openpype/tools/settings/settings/widgets.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 85610f7c19..45b4afe616 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -1051,9 +1051,11 @@ class ProjectListWidget(QtWidgets.QWidget): self._entity = entity def _on_item_right_clicked(self, index): + if not index.isValid(): + return project_name = index.data(PROJECT_NAME_ROLE) if project_name is None: - return + project_name = DEFAULT_PROJECT_LABEL if self.current_project != project_name: self.on_item_clicked(index) @@ -1080,9 +1082,11 @@ class ProjectListWidget(QtWidgets.QWidget): menu.exec_(QtGui.QCursor.pos()) def on_item_clicked(self, new_index): + if not new_index.isValid(): + return new_project_name = new_index.data(PROJECT_NAME_ROLE) if new_project_name is None: - return + new_project_name = DEFAULT_PROJECT_LABEL if self.current_project == new_project_name: return From e7fef1e6d2278b73b493dbedd673110b7f43d91a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Feb 2022 18:11:07 +0100 Subject: [PATCH 37/59] added another label and moved widgets around --- openpype/style/style.css | 22 ++-- .../tools/settings/settings/categories.py | 108 ++++++++++++++---- openpype/tools/settings/settings/widgets.py | 36 ++++-- 3 files changed, 125 insertions(+), 41 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 54e217f362..c96e87aa02 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1117,19 +1117,19 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { #ExpandLabel[state="invalid"]:hover, #SettingsLabel[state="invalid"]:hover { color: {color:settings:invalid-dark}; } +#SettingsOutdatedSourceVersion { + color: {color:settings:source-version-outdated}; +} #SourceVersionLabel { - border-radius: 0.48em; - padding-left: 3px; - padding-right: 3px; + padding-left: 3px; + padding-right: 3px; } #SourceVersionLabel[state="same"] { - border: 1px solid {color:settings:source-version}; - color: {color:settings:source-version}; + color: {color:settings:source-version}; } #SourceVersionLabel[state="different"] { - border: 1px solid {color:settings:source-version-outdated}; - color: {color:settings:source-version-outdated}; + color: {color:settings:source-version-outdated}; } /* TODO Replace these with explicit widget types if possible */ @@ -1146,8 +1146,8 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { border-color: {color:settings:invalid-dark}; } -#GroupWidget { - border-bottom: 1px solid #21252B; +#SettingsFooter { + border-top: 1px solid #21252B; } #ProjectListWidget QLabel { @@ -1155,6 +1155,10 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { font-weight: bold; } +#ProjectListContentWidget { + background: {color:bg-view}; +} + #MultiSelectionComboBox { font-size: 12px; } diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 74f29133f6..18a9764b34 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -93,6 +93,20 @@ class SettingsCategoryWidget(QtWidgets.QWidget): restart_required_trigger = QtCore.Signal() full_path_requested = QtCore.Signal(str, str) + require_restart_label_text = ( + "Your changes require restart of" + " all running OpenPype processes to take affect." + ) + outdated_version_label_text = ( + "Your settings are loaded from an older version." + ) + source_version_tooltip = "Using settings of current OpenPype version" + source_version_tooltip_outdated = ( + "Please check that all settings are still correct (blue colour\n" + "indicates potential changes in the new version) and save your\n" + "settings to update them to you current running OpenPype version." + ) + def __init__(self, user_role, parent=None): super(SettingsCategoryWidget, self).__init__(parent) @@ -204,6 +218,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): conf_wrapper_widget = QtWidgets.QWidget(self) configurations_widget = QtWidgets.QWidget(conf_wrapper_widget) + # Breadcrumbs/Path widget breadcrumbs_widget = QtWidgets.QWidget(self) breadcrumbs_label = QtWidgets.QLabel("Path:", breadcrumbs_widget) breadcrumbs_bar = BreadcrumbsAddressBar(breadcrumbs_widget) @@ -219,8 +234,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget): breadcrumbs_layout.addWidget(breadcrumbs_bar, 1) breadcrumbs_layout.addWidget(refresh_btn, 0) + # Widgets representing settings entities scroll_widget = QtWidgets.QScrollArea(configurations_widget) - scroll_widget.setObjectName("GroupWidget") content_widget = QtWidgets.QWidget(scroll_widget) scroll_widget.setWidgetResizable(True) scroll_widget.setWidget(content_widget) @@ -230,28 +245,46 @@ class SettingsCategoryWidget(QtWidgets.QWidget): content_layout.setSpacing(5) content_layout.setAlignment(QtCore.Qt.AlignTop) - footer_widget = QtWidgets.QWidget(configurations_widget) + # Footer widget + footer_widget = QtWidgets.QWidget(self) + footer_widget.setObjectName("SettingsFooter") + # Info labels + # TODO dynamic labels + labels_alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter + empty_label = QtWidgets.QLabel(footer_widget) + + outdated_version_label = QtWidgets.QLabel( + self.outdated_version_label_text, footer_widget + ) + outdated_version_label.setToolTip(self.source_version_tooltip_outdated) + outdated_version_label.setAlignment(labels_alignment) + outdated_version_label.setVisible(False) + outdated_version_label.setObjectName("SettingsOutdatedSourceVersion") + + require_restart_label = QtWidgets.QLabel( + self.require_restart_label_text, footer_widget + ) + require_restart_label.setAlignment(labels_alignment) + require_restart_label.setVisible(False) + + # Label showing source version of loaded settings source_version_label = QtWidgets.QLabel("", footer_widget) source_version_label.setObjectName("SourceVersionLabel") set_style_property(source_version_label, "state", "") - source_version_label.setToolTip( - "Version of OpenPype from which are settings loaded." - "\nThe 'legacy' are settings that were not stored per version." - ) + source_version_label.setToolTip(self.source_version_tooltip) save_btn = QtWidgets.QPushButton("Save", footer_widget) - require_restart_label = QtWidgets.QLabel(footer_widget) - require_restart_label.setAlignment(QtCore.Qt.AlignCenter) footer_layout = QtWidgets.QHBoxLayout(footer_widget) footer_layout.setContentsMargins(5, 5, 5, 5) - footer_layout.setSpacing(10) if self.user_role == "developer": self._add_developer_ui(footer_layout, footer_widget) - footer_layout.addWidget(source_version_label, 0) + footer_layout.addWidget(empty_label, 1) + footer_layout.addWidget(outdated_version_label, 1) footer_layout.addWidget(require_restart_label, 1) + footer_layout.addWidget(source_version_label, 0) footer_layout.addWidget(save_btn, 0) configurations_layout = QtWidgets.QVBoxLayout(configurations_widget) @@ -259,7 +292,6 @@ class SettingsCategoryWidget(QtWidgets.QWidget): configurations_layout.setSpacing(0) configurations_layout.addWidget(scroll_widget, 1) - configurations_layout.addWidget(footer_widget, 0) conf_wrapper_layout = QtWidgets.QHBoxLayout(conf_wrapper_widget) conf_wrapper_layout.setContentsMargins(0, 0, 0, 0) @@ -271,20 +303,29 @@ class SettingsCategoryWidget(QtWidgets.QWidget): main_layout.setSpacing(0) main_layout.addWidget(breadcrumbs_widget, 0) main_layout.addWidget(conf_wrapper_widget, 1) + main_layout.addWidget(footer_widget, 0) save_btn.clicked.connect(self._save) refresh_btn.clicked.connect(self._on_refresh) breadcrumbs_bar.path_edited.connect(self._on_path_edit) + self._require_restart_label = require_restart_label + self._outdated_version_label = outdated_version_label + self._empty_label = empty_label + + self._is_loaded_version_outdated = False + self.save_btn = save_btn self._source_version_label = source_version_label - self.refresh_btn = refresh_btn - self.require_restart_label = require_restart_label + self.scroll_widget = scroll_widget self.content_layout = content_layout self.content_widget = content_widget self.breadcrumbs_bar = breadcrumbs_bar + self.breadcrumbs_model = None + self.refresh_btn = refresh_btn + self.conf_wrapper_layout = conf_wrapper_layout self.main_layout = main_layout @@ -448,13 +489,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): return def _on_require_restart_change(self): - value = "" - if self.entity.require_restart: - value = ( - "Your changes require restart of" - " all running OpenPype processes to take affect." - ) - self.require_restart_label.setText(value) + self._update_labels_visibility() def reset(self): self.set_state(CategoryState.Working) @@ -537,13 +572,22 @@ class SettingsCategoryWidget(QtWidgets.QWidget): # Update source version label state_value = "" + tooltip = "" + outdated = False if source_version: if source_version != self._current_version: state_value = "different" + tooltip = self.source_version_tooltip_outdated + outdated = True else: state_value = "same" + tooltip = self.source_version_tooltip + + self._is_loaded_version_outdated = outdated self._source_version_label.setText(source_version) + self._source_version_label.setToolTip(tooltip) set_style_property(self._source_version_label, "state", state_value) + self._update_labels_visibility() self.set_state(CategoryState.Idle) @@ -654,7 +698,29 @@ class SettingsCategoryWidget(QtWidgets.QWidget): if require_restart: self.restart_required_trigger.emit() - self.require_restart_label.setText("") + + def _update_labels_visibility(self): + visible_label = None + labels = { + self._empty_label, + self._outdated_version_label, + self._require_restart_label, + } + if self.entity.require_restart: + visible_label = self._require_restart_label + elif self._is_loaded_version_outdated: + visible_label = self._outdated_version_label + else: + visible_label = self._empty_label + + if visible_label.isVisible(): + return + + for label in labels: + if label is visible_label: + visible_label.setVisible(True) + else: + label.setVisible(False) def _on_refresh(self): self.reset() diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 45b4afe616..f793aab057 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -1011,7 +1011,10 @@ class ProjectListWidget(QtWidgets.QWidget): super(ProjectListWidget, self).__init__(parent) self.setObjectName("ProjectListWidget") - project_list = ProjectView(self) + content_frame = QtWidgets.QFrame(self) + content_frame.setObjectName("ProjectListContentWidget") + + project_list = ProjectView(content_frame) project_model = ProjectModel(only_active) project_proxy = ProjectSortFilterProxy() @@ -1019,22 +1022,33 @@ class ProjectListWidget(QtWidgets.QWidget): project_proxy.setSourceModel(project_model) project_list.setModel(project_proxy) - layout = QtWidgets.QVBoxLayout(self) - layout.setSpacing(3) - layout.addWidget(project_list, 1) + content_layout = QtWidgets.QVBoxLayout(content_frame) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(0) + content_layout.addWidget(project_list, 1) - if only_active: - inactive_chk = None - else: - inactive_chk = QtWidgets.QCheckBox(" Show Inactive Projects ") + inactive_chk = None + if not only_active: + checkbox_wrapper = QtWidgets.QWidget(content_frame) + checkbox_wrapper.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + inactive_chk = QtWidgets.QCheckBox( + "Show Inactive Projects", checkbox_wrapper + ) inactive_chk.setChecked(not project_proxy.is_filter_enabled()) - layout.addSpacing(5) - layout.addWidget(inactive_chk, 0) - layout.addSpacing(5) + wrapper_layout = QtWidgets.QHBoxLayout(checkbox_wrapper) + wrapper_layout.addWidget(inactive_chk, 1) + + content_layout.addWidget(checkbox_wrapper, 0) inactive_chk.stateChanged.connect(self.on_inactive_vis_changed) + layout = QtWidgets.QVBoxLayout(self) + # Margins '3' are matching to configurables widget scroll area on right + layout.setContentsMargins(5, 3, 3, 3) + layout.addWidget(content_frame, 1) + project_list.left_mouse_released_at.connect(self.on_item_clicked) project_list.right_mouse_released_at.connect( self._on_item_right_clicked From 62f02f92bd9c72a3f81cd17a04fa44a438388533 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Feb 2022 11:45:51 +0100 Subject: [PATCH 38/59] sync description in sync to avalon action --- openpype/modules/default_modules/ftrack/lib/avalon_sync.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py index f58eb91485..66cf7645c2 100644 --- a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py @@ -304,7 +304,7 @@ class SyncEntitiesFactory: " from Project where full_name is \"{}\"" ) entities_query = ( - "select id, name, type_id, parent_id, link" + "select id, name, type_id, parent_id, link, description" " from TypedContext where project_id is \"{}\"" ) ignore_custom_attr_key = "avalon_ignore_sync" @@ -1231,6 +1231,8 @@ class SyncEntitiesFactory: data[key] = val if ftrack_id != self.ft_project_id: + data["description"] = entity["description"] + ent_path_items = [ent["name"] for ent in entity["link"]] parents = ent_path_items[1:len(ent_path_items) - 1:] From fd5e251bdcb0478ed0896b0a9936259b4d63ff0b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Feb 2022 11:46:02 +0100 Subject: [PATCH 39/59] hiero: removing obsolete unsupported plugin --- .../Python/Startup/version_everywhere.py | 352 ------------------ 1 file changed, 352 deletions(-) delete mode 100644 openpype/hosts/hiero/api/startup/Python/Startup/version_everywhere.py diff --git a/openpype/hosts/hiero/api/startup/Python/Startup/version_everywhere.py b/openpype/hosts/hiero/api/startup/Python/Startup/version_everywhere.py deleted file mode 100644 index 3d60b213d5..0000000000 --- a/openpype/hosts/hiero/api/startup/Python/Startup/version_everywhere.py +++ /dev/null @@ -1,352 +0,0 @@ -# version_up_everywhere.py -# Adds action to enable a Clip/Shot to be Min/Max/Next/Prev versioned in all shots used in a Project. -# -# Usage: -# 1) Copy file to /Python/Startup -# 2) Right-click on Clip(s) or Bins containing Clips in in the Bin View, or on Shots in the Timeline/Spreadsheet -# 3) Set Version for all Shots > OPTION to update the version in all shots where the Clip is used in the Project. - -import hiero.core -try: - from PySide.QtGui import * - from PySide.QtCore import * -except: - from PySide2.QtGui import * - from PySide2.QtWidgets import * - from PySide2.QtCore import * - - -def whereAmI(self, searchType="TrackItem"): - """returns a list of TrackItem or Sequnece objects in the Project which contain this Clip. - By default this will return a list of TrackItems where the Clip is used in its project. - You can also return a list of Sequences by specifying the searchType to be "Sequence". - Should consider putting this into hiero.core.Clip by default? - - Example usage: - - shotsForClip = clip.whereAmI("TrackItem") - sequencesForClip = clip.whereAmI("Sequence") - """ - proj = self.project() - - if ("TrackItem" not in searchType) and ("Sequence" not in searchType): - print("searchType argument must be \"TrackItem\" or \"Sequence\"") - return None - - # If user specifies a TrackItem, then it will return - searches = hiero.core.findItemsInProject(proj, searchType) - - if len(searches) == 0: - print("Unable to find {} in any items of type: {}".format( - str(self), searchType)) - return None - - # Case 1: Looking for Shots (trackItems) - clipUsedIn = [] - if isinstance(searches[0], hiero.core.TrackItem): - for shot in searches: - # We have to wrap this in a try/except because it's possible through the Python API for a Shot to exist without a Clip in the Bin - try: - - # For versioning to work, we must look to the BinItem that a Clip is wrapped in. - if shot.source().binItem() == self.binItem(): - clipUsedIn.append(shot) - - # If we throw an exception here its because the Shot did not have a Source Clip in the Bin. - except RuntimeError: - hiero.core.log.info( - 'Unable to find Parent Clip BinItem for Shot: %s, Source:%s' - % (shot, shot.source())) - pass - - # Case 1: Looking for Shots (trackItems) - elif isinstance(searches[0], hiero.core.Sequence): - for seq in searches: - # Iterate tracks > shots... - tracks = seq.items() - for track in tracks: - shots = track.items() - for shot in shots: - if shot.source().binItem() == self.binItem(): - clipUsedIn.append(seq) - - return clipUsedIn - - -# Add whereAmI method to Clip object -hiero.core.Clip.whereAmI = whereAmI - - -#### MAIN VERSION EVERYWHERE GUBBINS ##### -class VersionAllMenu(object): - - # These are a set of action names we can use for operating on multiple Clip/TrackItems - eMaxVersion = "Max Version" - eMinVersion = "Min Version" - eNextVersion = "Next Version" - ePreviousVersion = "Previous Version" - - # This is the title used for the Version Menu title. It's long isn't it? - actionTitle = "Set Version for all Shots" - - def __init__(self): - self._versionEverywhereMenu = None - self._versionActions = [] - - hiero.core.events.registerInterest("kShowContextMenu/kBin", - self.binViewEventHandler) - hiero.core.events.registerInterest("kShowContextMenu/kTimeline", - self.binViewEventHandler) - hiero.core.events.registerInterest("kShowContextMenu/kSpreadsheet", - self.binViewEventHandler) - - def showVersionUpdateReportFromShotManifest(self, sequenceShotManifest): - """This just displays an info Message box, based on a Sequence[Shot] manifest dictionary""" - - # Now present an info dialog, explaining where shots were updated - updateReportString = "The following Versions were updated:\n" - for seq in sequenceShotManifest.keys(): - updateReportString += "%s:\n Shots:\n" % (seq.name()) - for shot in sequenceShotManifest[seq]: - updateReportString += ' %s\n (New Version: %s)\n' % ( - shot.name(), shot.currentVersion().name()) - updateReportString += "\n" - - infoBox = QMessageBox(hiero.ui.mainWindow()) - infoBox.setIcon(QMessageBox.Information) - - if len(sequenceShotManifest) <= 0: - infoBox.setText("No Shot Versions were updated") - infoBox.setInformativeText( - "Clip could not be found in any Shots in this Project") - else: - infoBox.setText( - "Versions were updated in %i Sequences of this Project." % - (len(sequenceShotManifest))) - infoBox.setInformativeText("Show Details for more info.") - infoBox.setDetailedText(updateReportString) - - infoBox.exec_() - - def makeVersionActionForSingleClip(self, version): - """This is used to populate the QAction list of Versions when a single Clip is selected in the BinView. - It also triggers the Version Update action based on the version passed to it. - (Not sure if this is good design practice, but it's compact!)""" - action = QAction(version.name(), None) - action.setData(lambda: version) - - def updateAllTrackItems(): - currentClip = version.item() - trackItems = currentClip.whereAmI() - if not trackItems: - return - - proj = currentClip.project() - - # A Sequence-Shot manifest dictionary - sequenceShotManifest = {} - - # Make this all undo-able in a single Group undo - with proj.beginUndo( - "Update All Versions for %s" % currentClip.name()): - for shot in trackItems: - seq = shot.parentSequence() - if seq not in sequenceShotManifest.keys(): - sequenceShotManifest[seq] = [shot] - else: - sequenceShotManifest[seq] += [shot] - shot.setCurrentVersion(version) - - # We also should update the current Version of the selected Clip for completeness... - currentClip.binItem().setActiveVersion(version) - - # Now disaplay a Dialog which informs the user of where and what was changed - self.showVersionUpdateReportFromShotManifest(sequenceShotManifest) - - action.triggered.connect(updateAllTrackItems) - return action - - # This is just a convenience method for returning QActions with a title, triggered method and icon. - def makeAction(self, title, method, icon=None): - action = QAction(title, None) - action.setIcon(QIcon(icon)) - - # We do this magic, so that the title string from the action is used to trigger the version change - def methodWrapper(): - method(title) - - action.triggered.connect(methodWrapper) - return action - - def clipSelectionFromView(self, view): - """Helper method to return a list of Clips in the Active View""" - selection = hiero.ui.activeView().selection() - - if len(selection) == 0: - return None - - if isinstance(view, hiero.ui.BinView): - # We could have a mixture of Bins and Clips selected, so sort of the Clips and Clips inside Bins - clipItems = [ - item.activeItem() for item in selection - if hasattr(item, "activeItem") - and isinstance(item.activeItem(), hiero.core.Clip) - ] - - # We'll also append Bins here, and see if can find Clips inside - bins = [ - item for item in selection if isinstance(item, hiero.core.Bin) - ] - - # We search inside of a Bin for a Clip which is not already in clipBinItems - if len(bins) > 0: - # Grab the Clips inside of a Bin and append them to a list - for bin in bins: - clips = hiero.core.findItemsInBin(bin, "Clip") - for clip in clips: - if clip not in clipItems: - clipItems.append(clip) - - elif isinstance(view, - (hiero.ui.TimelineEditor, hiero.ui.SpreadsheetView)): - # Here, we have shots. To get to the Clip froma TrackItem, just call source() - clipItems = [ - item.source() for item in selection if hasattr(item, "source") - and isinstance(item, hiero.core.TrackItem) - ] - - return clipItems - - # This generates the Version Up Everywhere menu - def createVersionEveryWhereMenuForView(self, view): - - versionEverywhereMenu = QMenu(self.actionTitle) - self._versionActions = [] - # We look to the activeView for a selection of Clips - clips = self.clipSelectionFromView(view) - - # And bail if nothing is found - if len(clips) == 0: - return versionEverywhereMenu - - # Now, if we have just one Clip selected, we'll form a special menu, which lists all versions - if len(clips) == 1: - - # Get a reversed list of Versions, so that bigger ones appear at top - versions = list(reversed(clips[0].binItem().items())) - for version in versions: - self._versionActions += [ - self.makeVersionActionForSingleClip(version) - ] - - elif len(clips) > 1: - # We will add Max/Min/Prev/Next options, which can be called on a TrackItem, without the need for a Version object - self._versionActions += [ - self.makeAction( - self.eMaxVersion, - self.setTrackItemVersionForClipSelection, - icon=None) - ] - self._versionActions += [ - self.makeAction( - self.eMinVersion, - self.setTrackItemVersionForClipSelection, - icon=None) - ] - self._versionActions += [ - self.makeAction( - self.eNextVersion, - self.setTrackItemVersionForClipSelection, - icon=None) - ] - self._versionActions += [ - self.makeAction( - self.ePreviousVersion, - self.setTrackItemVersionForClipSelection, - icon=None) - ] - - for act in self._versionActions: - versionEverywhereMenu.addAction(act) - - return versionEverywhereMenu - - def setTrackItemVersionForClipSelection(self, versionOption): - - view = hiero.ui.activeView() - if not view: - return - - clipSelection = self.clipSelectionFromView(view) - - if len(clipSelection) == 0: - return - - proj = clipSelection[0].project() - - # Create a Sequence-Shot Manifest, to report to users where a Shot was updated - sequenceShotManifest = {} - - with proj.beginUndo("Update multiple Versions"): - for clip in clipSelection: - - # Look to see if it exists in a TrackItem somewhere... - shotUsage = clip.whereAmI("TrackItem") - - # Next, depending on the versionOption, make the appropriate update - # There's probably a more neat/compact way of doing this... - for shot in shotUsage: - - # This step is done for reporting reasons - seq = shot.parentSequence() - if seq not in sequenceShotManifest.keys(): - sequenceShotManifest[seq] = [shot] - else: - sequenceShotManifest[seq] += [shot] - - if versionOption == self.eMaxVersion: - shot.maxVersion() - elif versionOption == self.eMinVersion: - shot.minVersion() - elif versionOption == self.eNextVersion: - shot.nextVersion() - elif versionOption == self.ePreviousVersion: - shot.prevVersion() - - # Finally, for completeness, set the Max/Min version of the Clip too (if chosen) - # Note: It doesn't make sense to do Next/Prev on a Clip here because next/prev means different things for different Shots - if versionOption == self.eMaxVersion: - clip.binItem().maxVersion() - elif versionOption == self.eMinVersion: - clip.binItem().minVersion() - - # Now disaplay a Dialog which informs the user of where and what was changed - self.showVersionUpdateReportFromShotManifest(sequenceShotManifest) - - # This handles events from the Project Bin View - def binViewEventHandler(self, event): - - if not hasattr(event.sender, "selection"): - # Something has gone wrong, we should only be here if raised - # by the Bin view which gives a selection. - return - selection = event.sender.selection() - - # Return if there's no Selection. We won't add the Localise Menu. - if selection == None: - return - - view = hiero.ui.activeView() - # Only add the Menu if Bins or Sequences are selected (this ensures menu isn't added in the Tags Pane) - if len(selection) > 0: - self._versionEverywhereMenu = self.createVersionEveryWhereMenuForView( - view) - hiero.ui.insertMenuAction( - self._versionEverywhereMenu.menuAction(), - event.menu, - after="foundry.menu.version") - return - - -# Instantiate the Menu to get it to register itself. -VersionAllMenu = VersionAllMenu() From b162ee9c386395477fbb3046c8d72f303cb735f3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Feb 2022 11:50:22 +0100 Subject: [PATCH 40/59] OP-2579 - removed subset_template_name Use generic get_subset_name_with_asset_doc function to have only one place in Settings for configuration. --- .../plugins/publish/collect_batch_data.py | 1 + .../publish/collect_published_files.py | 27 ++++++++++++------- .../project_settings/webpublisher.json | 24 ++++++----------- .../schema_project_webpublisher.json | 5 ---- 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py b/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py index 062c5ce0da..ca14538d7d 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py @@ -59,6 +59,7 @@ class CollectBatchData(pyblish.api.ContextPlugin): context.data["asset"] = asset_name context.data["task"] = task_name context.data["taskType"] = task_type + context.data["project_name"] = project_name self._set_ctx_path(batch_data) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index e04f207c01..abad14106f 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -13,8 +13,10 @@ import tempfile from avalon import io import pyblish.api from openpype.lib import prepare_template_data -from openpype.lib.plugin_tools import parse_json - +from openpype.lib.plugin_tools import ( + parse_json, + get_subset_name_with_asset_doc +) class CollectPublishedFiles(pyblish.api.ContextPlugin): """ @@ -47,8 +49,13 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): self.log.info("task_sub:: {}".format(task_subfolders)) asset_name = context.data["asset"] + asset_doc = io.find_one({ + "type": "asset", + "name": asset_name + }) task_name = context.data["task"] task_type = context.data["taskType"] + project_name = context.data["project_name"] for task_dir in task_subfolders: task_data = parse_json(os.path.join(task_dir, "manifest.json")) @@ -57,20 +64,21 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): is_sequence = len(task_data["files"]) > 1 _, extension = os.path.splitext(task_data["files"][0]) - family, families, subset_template, tags = self._get_family( + family, families, tags = self._get_family( self.task_type_to_family, task_type, is_sequence, extension.replace(".", '')) - subset = self._get_subset_name( - family, subset_template, task_name, task_data["variant"] + subset_name = get_subset_name_with_asset_doc( + family, task_data["variant"], task_name, asset_doc, + project_name=project_name, host_name="webpublisher" ) - version = self._get_last_version(asset_name, subset) + 1 + version = self._get_last_version(asset_name, subset_name) + 1 - instance = context.create_instance(subset) + instance = context.create_instance(subset_name) instance.data["asset"] = asset_name - instance.data["subset"] = subset + instance.data["subset"] = subset_name instance.data["family"] = family instance.data["families"] = families instance.data["version"] = version @@ -149,7 +157,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): extension (str): without '.' Returns: - (family, [families], subset_template_name, tags) tuple + (family, [families], tags) tuple AssertionError if not matching family found """ task_type = task_type.lower() @@ -184,7 +192,6 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): return (found_family, config["families"], - config["subset_template_name"], config["tags"]) def _get_last_version(self, asset_name, subset_name): diff --git a/openpype/settings/defaults/project_settings/webpublisher.json b/openpype/settings/defaults/project_settings/webpublisher.json index 2ab135e59b..77168c25e6 100644 --- a/openpype/settings/defaults/project_settings/webpublisher.json +++ b/openpype/settings/defaults/project_settings/webpublisher.json @@ -10,8 +10,7 @@ ], "families": [], "tags": [], - "result_family": "workfile", - "subset_template_name": "{family}{Variant}" + "result_family": "workfile" }, { "is_sequence": true, @@ -27,8 +26,7 @@ "tags": [ "review" ], - "result_family": "render", - "subset_template_name": "{family}{Variant}" + "result_family": "render" } ], "Compositing": [ @@ -39,8 +37,7 @@ ], "families": [], "tags": [], - "result_family": "workfile", - "subset_template_name": "{family}{Variant}" + "result_family": "workfile" }, { "is_sequence": true, @@ -56,8 +53,7 @@ "tags": [ "review" ], - "result_family": "render", - "subset_template_name": "{family}{Variant}" + "result_family": "render" } ], "Layout": [ @@ -68,8 +64,7 @@ ], "families": [], "tags": [], - "result_family": "workfile", - "subset_template_name": "{family}{Variant}" + "result_family": "workfile" }, { "is_sequence": false, @@ -86,8 +81,7 @@ "tags": [ "review" ], - "result_family": "image", - "subset_template_name": "{family}{Variant}" + "result_family": "image" } ], "default_task_type": [ @@ -99,8 +93,7 @@ ], "families": [], "tags": [], - "result_family": "workfile", - "subset_template_name": "{family}{Variant}" + "result_family": "workfile" }, { "is_sequence": true, @@ -116,8 +109,7 @@ "tags": [ "review" ], - "result_family": "render", - "subset_template_name": "{family}{Variant}" + "result_family": "render" } ], "__dynamic_keys_labels__": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json index d15140db4d..b76a0fa844 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json @@ -59,11 +59,6 @@ "type": "text", "key": "result_family", "label": "Resulting family" - }, - { - "type": "text", - "key": "subset_template_name", - "label": "Subset template name" } ] } From 03371dd7bb3ff84caa371761f02a549b2c98b9db Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Feb 2022 12:09:08 +0100 Subject: [PATCH 41/59] sync description on change --- .../event_sync_to_avalon.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py index a4982627ff..200e6620be 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -48,8 +48,8 @@ class SyncToAvalonEvent(BaseEvent): ) entities_query_by_id = ( - "select id, name, parent_id, link, custom_attributes from TypedContext" - " where project_id is \"{}\" and id in ({})" + "select id, name, parent_id, link, custom_attributes, description" + " from TypedContext where project_id is \"{}\" and id in ({})" ) # useful for getting all tasks for asset @@ -1073,9 +1073,8 @@ class SyncToAvalonEvent(BaseEvent): self.create_entity_in_avalon(entity, parent_avalon_ent) for child in entity["children"]: - if child.entity_type.lower() == "task": - continue - children_queue.append(child) + if child.entity_type.lower() != "task": + children_queue.append(child) while children_queue: entity = children_queue.popleft() @@ -1145,7 +1144,8 @@ class SyncToAvalonEvent(BaseEvent): "entityType": ftrack_ent.entity_type, "parents": parents, "tasks": {}, - "visualParent": vis_par + "visualParent": vis_par, + "description": ftrack_ent["description"] } } cust_attrs = self.get_cust_attr_values(ftrack_ent) @@ -1822,7 +1822,15 @@ class SyncToAvalonEvent(BaseEvent): if ent_cust_attrs is None: ent_cust_attrs = {} - for key, values in ent_info["changes"].items(): + ent_changes = ent_info["changes"] + if "description" in ent_changes: + if "data" not in self.updates[mongo_id]: + self.updates[mongo_id]["data"] = {} + self.updates[mongo_id]["data"]["description"] = ( + ent_changes["description"]["new"] or "" + ) + + for key, values in ent_changes.items(): if key in hier_attrs_by_key: self.hier_cust_attrs_changes[key].append(ftrack_id) continue From 94fab5b8b6a55bba686ba64710a1a3da0f873bf5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Feb 2022 12:23:27 +0100 Subject: [PATCH 42/59] doc: add example to `repack-version` command --- website/docs/admin_openpype_commands.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md index 83ab6a1ee6..74cb895ac9 100644 --- a/website/docs/admin_openpype_commands.md +++ b/website/docs/admin_openpype_commands.md @@ -85,8 +85,8 @@ openpype_console eventserver --ftrack-url= --ftrack-user= --ftrack-ap | `--asset` | Asset name (default taken from `AVALON_ASSET` if set) | | `--task` | Task name (default taken from `AVALON_TASK` is set) | | `--tools` | *Optional: Additional tools to add* | -| `--user` | *Optional: User on behalf to run* | -| `--ftrack-server` / `-fs` | *Optional: Ftrack server URL* | +| `--user` | *Optional: User on behalf to run* | +| `--ftrack-server` / `-fs` | *Optional: Ftrack server URL* | | `--ftrack-user` / `-fu` | *Optional: Ftrack user* | | `--ftrack-key` / `-fk` | *Optional: Ftrack API key* | @@ -166,3 +166,6 @@ Takes path to unzipped and possibly modified OpenPype version. Files will be zipped, checksums recalculated and version will be determined by folder name (and written to `version.py`). +```shell +./openpype_console repack-version /path/to/some/modified/unzipped/version/openpype-v3.8.3-modified +``` From 32e344bb27d182553e12cb57cdd7473f55aa470a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Feb 2022 13:35:01 +0100 Subject: [PATCH 43/59] OP-2579 - fix broken settings after change --- .../webserver_service/webpublish_routes.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index e2d041b512..b55011effd 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -359,12 +359,20 @@ class ConfiguredExtensionsEndpoint(_RestApiEndpoint): "studio_exts": set(["psd", "psb", "tvpp", "tvp"]) } collect_conf = sett["webpublisher"]["publish"]["CollectPublishedFiles"] - for _, mapping in collect_conf.get("task_type_to_family", {}).items(): - for _family, config in mapping.items(): - if config["is_sequence"]: - configured["sequence_exts"].update(config["extensions"]) - else: - configured["file_exts"].update(config["extensions"]) + configs = collect_conf.get("task_type_to_family", []) + mappings = [] + if isinstance(configs, dict): # backward compatibility, remove soon + # mappings = [mapp for mapp in configs.values() if mapp] + for _, conf_mappings in configs.items(): + for conf_mapping in conf_mappings: + mappings.append(conf_mapping) + else: + mappings = configs + for mapping in mappings: + if mapping["is_sequence"]: + configured["sequence_exts"].update(mapping["extensions"]) + else: + configured["file_exts"].update(mapping["extensions"]) return Response( status=200, From b02699018a53883efd8961c2f7ba4d58773916a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Feb 2022 14:36:55 +0100 Subject: [PATCH 44/59] fix menu callbacks to use lambda to avoid passing unexpected arguments --- openpype/hosts/maya/api/menu.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 40ffd825f3..b1934c757d 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -108,17 +108,17 @@ def install(): cmds.menuItem( "Reset Frame Range", - command=reset_frame_range + command=lambda *args: reset_frame_range() ) cmds.menuItem( "Reset Resolution", - command=lib.reset_scene_resolution + command=lambda *args: lib.reset_scene_resolution() ) cmds.menuItem( "Set Colorspace", - command=lib.set_colorspace, + command=lambda *args: lib.set_colorspace(), ) cmds.menuItem(divider=True, parent=MENU_NAME) cmds.menuItem( From 43f1574719e630bfe9c43a1ec75063498b59eb8d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Feb 2022 14:43:45 +0100 Subject: [PATCH 45/59] hiero: fix effect collector name and order --- .../{precollect_clip_effects.py => collect_clip_effects.py} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename openpype/hosts/hiero/plugins/publish/{precollect_clip_effects.py => collect_clip_effects.py} (97%) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py similarity index 97% rename from openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py rename to openpype/hosts/hiero/plugins/publish/collect_clip_effects.py index 9ade7603e0..8d2ed9a9c2 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py @@ -2,11 +2,11 @@ import re import pyblish.api -class PreCollectClipEffects(pyblish.api.InstancePlugin): +class CollectClipEffects(pyblish.api.InstancePlugin): """Collect soft effects instances.""" - order = pyblish.api.CollectorOrder - 0.479 - label = "Precollect Clip Effects Instances" + order = pyblish.api.CollectorOrder - 0.078 + label = "Collect Clip Effects Instances" families = ["clip"] def process(self, instance): From 2459f19802aad3a1f274874547bad25074430787 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Feb 2022 15:22:16 +0100 Subject: [PATCH 46/59] OP-2579 - better fix for arrays in Settings --- .../webserver_service/webpublish_routes.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index b55011effd..1f9089aa27 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -361,13 +361,12 @@ class ConfiguredExtensionsEndpoint(_RestApiEndpoint): collect_conf = sett["webpublisher"]["publish"]["CollectPublishedFiles"] configs = collect_conf.get("task_type_to_family", []) mappings = [] - if isinstance(configs, dict): # backward compatibility, remove soon - # mappings = [mapp for mapp in configs.values() if mapp] - for _, conf_mappings in configs.items(): - for conf_mapping in conf_mappings: - mappings.append(conf_mapping) - else: - mappings = configs + for _, conf_mappings in configs.items(): + if isinstance(conf_mappings, dict): + conf_mappings = conf_mappings.values() + for conf_mapping in conf_mappings: + mappings.append(conf_mapping) + for mapping in mappings: if mapping["is_sequence"]: configured["sequence_exts"].update(mapping["extensions"]) From f9472ab309a0074679256dc576a15965d47f1c51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Feb 2022 15:49:31 +0100 Subject: [PATCH 47/59] add to cleanup paths also staging dirs of representations --- openpype/plugins/publish/cleanup_farm.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/plugins/publish/cleanup_farm.py b/openpype/plugins/publish/cleanup_farm.py index 51bc216e73..5e8f32dae9 100644 --- a/openpype/plugins/publish/cleanup_farm.py +++ b/openpype/plugins/publish/cleanup_farm.py @@ -41,6 +41,12 @@ class CleanUpFarm(pyblish.api.ContextPlugin): if staging_dir: dirpaths_to_remove.add(os.path.normpath(staging_dir)) + if "representations" in instance.data: + for repre in instance.data["reresentations"]: + staging_dir = repre.get("stagingDir") + if staging_dir: + dirpaths_to_remove.add(os.path.normpath(staging_dir)) + if not dirpaths_to_remove: self.log.info("Nothing to remove. Skipping") return From 67ac956512df8d1db36cbf145ba1a2d52bec5046 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 8 Feb 2022 16:30:13 +0100 Subject: [PATCH 48/59] fix typo --- openpype/plugins/publish/cleanup_farm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/cleanup_farm.py b/openpype/plugins/publish/cleanup_farm.py index 5e8f32dae9..ab0c6e469e 100644 --- a/openpype/plugins/publish/cleanup_farm.py +++ b/openpype/plugins/publish/cleanup_farm.py @@ -42,7 +42,7 @@ class CleanUpFarm(pyblish.api.ContextPlugin): dirpaths_to_remove.add(os.path.normpath(staging_dir)) if "representations" in instance.data: - for repre in instance.data["reresentations"]: + for repre in instance.data["representations"]: staging_dir = repre.get("stagingDir") if staging_dir: dirpaths_to_remove.add(os.path.normpath(staging_dir)) From a5273cb7589fb17a6429bbc67d7e307cbf878afe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 8 Feb 2022 17:06:59 +0100 Subject: [PATCH 49/59] Only allow scroll wheel edits when spinbox is active (cherry picked from commit ebc3d626d152a07692f2de598c294348ad199597) --- .../project_manager/delegates.py | 8 +++--- .../project_manager/widgets.py | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 842352cba1..31487ff132 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -2,7 +2,9 @@ from Qt import QtWidgets, QtCore from .widgets import ( NameTextEdit, - FilterComboBox + FilterComboBox, + SpinBoxScrollFixed, + DoubleSpinBoxScrollFixed ) from .multiselection_combobox import MultiSelectionComboBox @@ -89,9 +91,9 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): def createEditor(self, parent, option, index): if self.decimals > 0: - editor = QtWidgets.QDoubleSpinBox(parent) + editor = DoubleSpinBoxScrollFixed(parent) else: - editor = QtWidgets.QSpinBox(parent) + editor = SpinBoxScrollFixed(parent) editor.setObjectName("NumberEditor") # Set min/max diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 4b5aca35ef..02d4eda0fc 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -429,3 +429,29 @@ class ConfirmProjectDeletion(QtWidgets.QDialog): def _on_confirm_text_change(self): enabled = self._confirm_input.text() == self._project_name self._confirm_btn.setEnabled(enabled) + + +class SpinBoxScrollFixed(QtWidgets.QSpinBox): + """QSpinBox which only allow edits change with scroll wheel when active""" + def __init__(self, *args, **kwargs): + super(SpinBoxScrollFixed, self).__init__(*args, **kwargs) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + def wheelEvent(self, event): + if not self.hasFocus(): + event.ignore() + else: + super(SpinBoxScrollFixed, self).wheelEvent(event) + + +class DoubleSpinBoxScrollFixed(QtWidgets.QDoubleSpinBox): + """QDoubleSpinBox which only allow edits with scroll wheel when active""" + def __init__(self, *args, **kwargs): + super(DoubleSpinBoxScrollFixed, self).__init__(*args, **kwargs) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + def wheelEvent(self, event): + if not self.hasFocus(): + event.ignore() + else: + super(DoubleSpinBoxScrollFixed, self).wheelEvent(event) From 57a5a09c457dab96c6affc143cf95c385fe13098 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Feb 2022 17:52:13 +0100 Subject: [PATCH 50/59] add setting for cleanupfarm and disable the plugin by default --- .../defaults/project_settings/global.json | 3 +++ .../schemas/schema_global_publish.json | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 9c0c6f6958..f3aad2a51b 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -208,6 +208,9 @@ "CleanUp": { "paterns": [], "remove_temp_renders": false + }, + "CleanUpFarm": { + "enabled": false } }, "tools": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 3f9776bcd6..e608e9ff63 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -677,8 +677,22 @@ "label": "Remove Temp renders", "default": false } - ] + }, + { + "type": "dict", + "collapsible": false, + "key": "CleanUpFarm", + "label": "Clean Up Farm", + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] } ] } From 821e1bf7bdff0b6568403ce6c8d23bed95978526 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 9 Feb 2022 03:38:16 +0000 Subject: [PATCH 51/59] [Automated] Bump version --- CHANGELOG.md | 56 +++++++++++++++++++++++++++++++-------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0593f9837..4f72580c29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,38 @@ # Changelog +## [3.8.3-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.2...HEAD) + +### 📖 Documentation + +- documentation: add example to `repack-version` command [\#2669](https://github.com/pypeclub/OpenPype/pull/2669) +- Update docusaurus [\#2639](https://github.com/pypeclub/OpenPype/pull/2639) +- Documentation: Fixed relative links [\#2621](https://github.com/pypeclub/OpenPype/pull/2621) + +**🚀 Enhancements** + +- Ftrack: Sync description to assets [\#2670](https://github.com/pypeclub/OpenPype/pull/2670) +- Houdini: Moved to OpenPype [\#2658](https://github.com/pypeclub/OpenPype/pull/2658) +- Maya: Move implementation to OpenPype [\#2649](https://github.com/pypeclub/OpenPype/pull/2649) + +**🐛 Bug fixes** + +- Maya: Fix menu callbacks [\#2671](https://github.com/pypeclub/OpenPype/pull/2671) +- hiero: removing obsolete unsupported plugin [\#2667](https://github.com/pypeclub/OpenPype/pull/2667) + +**Merged pull requests:** + +- Fix python install in docker for centos7 [\#2664](https://github.com/pypeclub/OpenPype/pull/2664) +- Deadline: Be able to pass Mongo url to job [\#2616](https://github.com/pypeclub/OpenPype/pull/2616) + ## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.1...3.8.2) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.8.2-nightly.3...3.8.2) + +### 📖 Documentation + +- Cosmetics: Fix common typos in openpype/website [\#2617](https://github.com/pypeclub/OpenPype/pull/2617) **🚀 Enhancements** @@ -11,21 +41,18 @@ - nuke: adding clear button to write nodes [\#2627](https://github.com/pypeclub/OpenPype/pull/2627) - Ftrack: Family to Asset type mapping is in settings [\#2602](https://github.com/pypeclub/OpenPype/pull/2602) - Nuke: load color space from representation data [\#2576](https://github.com/pypeclub/OpenPype/pull/2576) +- New Publisher: New features and preparations for new standalone publisher [\#2556](https://github.com/pypeclub/OpenPype/pull/2556) **🐛 Bug fixes** - Fix pulling of cx\_freeze 6.10 [\#2628](https://github.com/pypeclub/OpenPype/pull/2628) - -### 📖 Documentation - -- Cosmetics: Fix common typos in openpype/website [\#2617](https://github.com/pypeclub/OpenPype/pull/2617) +- Global: fix broken otio review extractor [\#2590](https://github.com/pypeclub/OpenPype/pull/2590) **Merged pull requests:** - Docker: enhance dockerfiles with metadata, fix pyenv initialization [\#2647](https://github.com/pypeclub/OpenPype/pull/2647) - WebPublisher: fix instance duplicates [\#2641](https://github.com/pypeclub/OpenPype/pull/2641) - Fix - safer pulling of task name for webpublishing from PS [\#2613](https://github.com/pypeclub/OpenPype/pull/2613) -- Webpublisher: Skip version collect [\#2591](https://github.com/pypeclub/OpenPype/pull/2591) ## [3.8.1](https://github.com/pypeclub/OpenPype/tree/3.8.1) (2022-02-01) @@ -34,7 +61,6 @@ **🚀 Enhancements** - Webpublisher: Thumbnail extractor [\#2600](https://github.com/pypeclub/OpenPype/pull/2600) -- Webpublisher: Added endpoint to reprocess batch through UI [\#2555](https://github.com/pypeclub/OpenPype/pull/2555) - Loader: Allow to toggle default family filters between "include" or "exclude" filtering [\#2541](https://github.com/pypeclub/OpenPype/pull/2541) - Launcher: Added context menu to to skip opening last workfile [\#2536](https://github.com/pypeclub/OpenPype/pull/2536) @@ -44,34 +70,37 @@ - hotfix: OIIO tool path - add extension on windows [\#2618](https://github.com/pypeclub/OpenPype/pull/2618) - Settings: Enum does not store empty string if has single item to select [\#2615](https://github.com/pypeclub/OpenPype/pull/2615) - switch distutils to sysconfig for `get\_platform\(\)` [\#2594](https://github.com/pypeclub/OpenPype/pull/2594) -- Global: fix broken otio review extractor [\#2590](https://github.com/pypeclub/OpenPype/pull/2590) - Fix poetry index and speedcopy update [\#2589](https://github.com/pypeclub/OpenPype/pull/2589) - Webpublisher: Fix - subset names from processed .psd used wrong value for task [\#2586](https://github.com/pypeclub/OpenPype/pull/2586) - `vrscene` creator Deadline webservice URL handling [\#2580](https://github.com/pypeclub/OpenPype/pull/2580) - global: track name was failing if duplicated root word in name [\#2568](https://github.com/pypeclub/OpenPype/pull/2568) -- Validate Maya Rig produces no cycle errors [\#2484](https://github.com/pypeclub/OpenPype/pull/2484) **Merged pull requests:** - Bump pillow from 8.4.0 to 9.0.0 [\#2595](https://github.com/pypeclub/OpenPype/pull/2595) +- Webpublisher: Skip version collect [\#2591](https://github.com/pypeclub/OpenPype/pull/2591) - build\(deps\): bump pillow from 8.4.0 to 9.0.0 [\#2523](https://github.com/pypeclub/OpenPype/pull/2523) ## [3.8.0](https://github.com/pypeclub/OpenPype/tree/3.8.0) (2022-01-24) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.8.0-nightly.7...3.8.0) +### 📖 Documentation + +- Variable in docs renamed to proper name [\#2546](https://github.com/pypeclub/OpenPype/pull/2546) + **🆕 New features** - Flame: extracting segments with trans-coding [\#2547](https://github.com/pypeclub/OpenPype/pull/2547) - Maya : V-Ray Proxy - load all ABC files via proxy [\#2544](https://github.com/pypeclub/OpenPype/pull/2544) - Maya to Unreal: Extended static mesh workflow [\#2537](https://github.com/pypeclub/OpenPype/pull/2537) - Flame: collecting publishable instances [\#2519](https://github.com/pypeclub/OpenPype/pull/2519) -- Flame: create publishable clips [\#2495](https://github.com/pypeclub/OpenPype/pull/2495) **🚀 Enhancements** - Webpublisher: Moved error at the beginning of the log [\#2559](https://github.com/pypeclub/OpenPype/pull/2559) - Ftrack: Use ApplicationManager to get DJV path [\#2558](https://github.com/pypeclub/OpenPype/pull/2558) +- Webpublisher: Added endpoint to reprocess batch through UI [\#2555](https://github.com/pypeclub/OpenPype/pull/2555) - Settings: PathInput strip passed string [\#2550](https://github.com/pypeclub/OpenPype/pull/2550) - Global: Exctract Review anatomy fill data with output name [\#2548](https://github.com/pypeclub/OpenPype/pull/2548) - Cosmetics: Clean up some cosmetics / typos [\#2542](https://github.com/pypeclub/OpenPype/pull/2542) @@ -79,9 +108,6 @@ - General: Be able to use anatomy data in ffmpeg output arguments [\#2525](https://github.com/pypeclub/OpenPype/pull/2525) - Expose toggle publish plug-in settings for Maya Look Shading Engine Naming [\#2521](https://github.com/pypeclub/OpenPype/pull/2521) - Photoshop: Move implementation to OpenPype [\#2510](https://github.com/pypeclub/OpenPype/pull/2510) -- Slack: notifications are sent with Openpype logo and bot name [\#2499](https://github.com/pypeclub/OpenPype/pull/2499) -- Slack: Add review to notification message [\#2498](https://github.com/pypeclub/OpenPype/pull/2498) -- Maya: Collect 'fps' animation data only for "review" instances [\#2486](https://github.com/pypeclub/OpenPype/pull/2486) **🐛 Bug fixes** @@ -103,10 +129,6 @@ - Maya: reset empty string attributes correctly to "" instead of "None" [\#2506](https://github.com/pypeclub/OpenPype/pull/2506) - Improve FusionPreLaunch hook errors [\#2505](https://github.com/pypeclub/OpenPype/pull/2505) -### 📖 Documentation - -- Variable in docs renamed to proper name [\#2546](https://github.com/pypeclub/OpenPype/pull/2546) - **Merged pull requests:** - AfterEffects: Move implementation to OpenPype [\#2543](https://github.com/pypeclub/OpenPype/pull/2543) diff --git a/openpype/version.py b/openpype/version.py index 12f9b5d13e..a2859ba2bc 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.8.2" +__version__ = "3.8.3-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 1dafc7d279..5a2ebe3aa0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.8.2" # OpenPype +version = "3.8.3-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 5cbea5037641415b2a1b5a3efb6ce5372c09c0ef Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Feb 2022 16:08:07 +0100 Subject: [PATCH 52/59] 'query_custom_attributes' is used right way in all cases for hierarchical values --- .../action_push_frame_values_to_task.py | 30 +++------ .../event_push_frame_values_to_task.py | 36 +++++------ .../event_handlers_server/event_sync_links.py | 2 +- .../event_sync_to_avalon.py | 25 +++----- .../action_clean_hierarchical_attributes.py | 23 +++---- .../action_create_cust_attrs.py | 27 +++----- .../default_modules/ftrack/ftrack_module.py | 7 ++- .../default_modules/ftrack/lib/avalon_sync.py | 63 +++---------------- .../ftrack/lib/custom_attributes.py | 45 +++++++------ 9 files changed, 92 insertions(+), 166 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py index 3f63ce6fac..868bbb8463 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py @@ -2,7 +2,10 @@ import sys import json import collections import ftrack_api -from openpype_modules.ftrack.lib import ServerAction +from openpype_modules.ftrack.lib import ( + ServerAction, + query_custom_attributes +) class PushHierValuesToNonHier(ServerAction): @@ -51,10 +54,6 @@ class PushHierValuesToNonHier(ServerAction): " from CustomAttributeConfiguration" " where key in ({})" ) - cust_attr_value_query = ( - "select value, entity_id from CustomAttributeValue" - " where entity_id in ({}) and configuration_id in ({})" - ) # configurable settings_key = "sync_hier_entity_attributes" @@ -344,25 +343,11 @@ class PushHierValuesToNonHier(ServerAction): all_ids_with_parents.add(parent_id) _entity_id = parent_id - joined_entity_ids = self.join_query_keys(all_ids_with_parents) - - hier_attr_ids = self.join_query_keys( - tuple(hier_attr["id"] for hier_attr in hier_attrs) - ) + hier_attr_ids = tuple(hier_attr["id"] for hier_attr in hier_attrs) hier_attrs_key_by_id = { hier_attr["id"]: hier_attr["key"] for hier_attr in hier_attrs } - call_expr = [{ - "action": "query", - "expression": self.cust_attr_value_query.format( - joined_entity_ids, hier_attr_ids - ) - }] - if hasattr(session, "call"): - [values] = session.call(call_expr) - else: - [values] = session._call(call_expr) values_per_entity_id = {} for entity_id in all_ids_with_parents: @@ -370,7 +355,10 @@ class PushHierValuesToNonHier(ServerAction): for key in hier_attrs_key_by_id.values(): values_per_entity_id[entity_id][key] = None - for item in values["data"]: + values = query_custom_attributes( + session, all_ids_with_parents, hier_attr_ids, True + ) + for item in values: entity_id = item["entity_id"] key = hier_attrs_key_by_id[item["configuration_id"]] diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/default_modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 10b165e7f6..0914933de4 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -17,11 +17,6 @@ class PushFrameValuesToTaskEvent(BaseEvent): " (object_type_id in ({}) or is_hierarchical is true)" ) - cust_attr_query = ( - "select value, entity_id from ContextCustomAttributeValue " - "where entity_id in ({}) and configuration_id in ({})" - ) - _cached_task_object_id = None _cached_interest_object_ids = None _cached_user_id = None @@ -273,16 +268,23 @@ class PushFrameValuesToTaskEvent(BaseEvent): hier_attr_ids.append(attr_id) conf_ids = list(hier_attr_ids) + task_conf_ids = [] for key, attr_id in task_attrs.items(): attr_key_by_id[attr_id] = key nonhier_id_by_key[key] = attr_id conf_ids.append(attr_id) + task_conf_ids.append(attr_id) # Query custom attribute values # - result does not contain values for all entities only result of # query callback to ftrack server result = query_custom_attributes( - session, conf_ids, whole_hierarchy_ids + session, list(hier_attr_ids), whole_hierarchy_ids, True + ) + result.extend( + query_custom_attributes( + session, task_conf_ids, whole_hierarchy_ids, False + ) ) # Prepare variables where result will be stored @@ -547,7 +549,7 @@ class PushFrameValuesToTaskEvent(BaseEvent): ) attr_ids = set(attr_id_to_key.keys()) - current_values_by_id = self.current_values( + current_values_by_id = self.get_current_values( session, attr_ids, entity_ids, task_entity_ids, hier_attrs ) @@ -642,27 +644,17 @@ class PushFrameValuesToTaskEvent(BaseEvent): return interesting_data, changed_keys_by_object_id - def current_values( + def get_current_values( self, session, attr_ids, entity_ids, task_entity_ids, hier_attrs ): current_values_by_id = {} if not attr_ids or not entity_ids: return current_values_by_id - joined_conf_ids = self.join_query_keys(attr_ids) - joined_entity_ids = self.join_query_keys(entity_ids) - call_expr = [{ - "action": "query", - "expression": self.cust_attr_query.format( - joined_entity_ids, joined_conf_ids - ) - }] - if hasattr(session, "call"): - [values] = session.call(call_expr) - else: - [values] = session._call(call_expr) - - for item in values["data"]: + values = query_custom_attributes( + session, attr_ids, entity_ids, True + ) + for item in values: entity_id = item["entity_id"] attr_id = item["configuration_id"] if entity_id in task_entity_ids and attr_id in hier_attrs: diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_links.py b/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_links.py index 83132acd85..9610e7f5de 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_links.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_links.py @@ -128,7 +128,7 @@ class SyncLinksToAvalon(BaseEvent): def _get_mongo_ids_by_ftrack_ids(self, session, attr_id, ftrack_ids): output = query_custom_attributes( - session, [attr_id], ftrack_ids + session, [attr_id], ftrack_ids, True ) mongo_id_by_ftrack_id = {} for item in output: diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py index 200e6620be..9f85000dbb 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -17,6 +17,7 @@ from avalon.api import AvalonMongoDB from openpype_modules.ftrack.lib import ( get_openpype_attr, + query_custom_attributes, CUST_ATTR_ID_KEY, CUST_ATTR_AUTO_SYNC, @@ -2130,22 +2131,12 @@ class SyncToAvalonEvent(BaseEvent): for key in hier_cust_attrs_keys: configuration_ids.add(hier_attr_id_by_key[key]) - entity_ids_joined = self.join_query_keys(cust_attrs_ftrack_ids) - attributes_joined = self.join_query_keys(configuration_ids) - - queries = [{ - "action": "query", - "expression": ( - "select value, entity_id, configuration_id" - " from CustomAttributeValue " - "where entity_id in ({}) and configuration_id in ({})" - ).format(entity_ids_joined, attributes_joined) - }] - - if hasattr(self.process_session, "call"): - [values] = self.process_session.call(queries) - else: - [values] = self.process_session._call(queries) + values = query_custom_attributes( + self.process_session, + configuration_ids, + cust_attrs_ftrack_ids, + True + ) ftrack_project_id = self.cur_project["id"] @@ -2170,7 +2161,7 @@ class SyncToAvalonEvent(BaseEvent): # PREPARE DATA BEFORE THIS avalon_hier = [] - for item in values["data"]: + for item in values: value = item["value"] if value is None: continue diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py index dc97ed972d..f06162bfda 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py @@ -19,8 +19,8 @@ class CleanHierarchicalAttrsAction(BaseAction): " from TypedContext where project_id is \"{}\"" ) cust_attr_query = ( - "select value, entity_id from CustomAttributeValue " - "where entity_id in ({}) and configuration_id is \"{}\"" + "select value, entity_id from CustomAttributeValue" + " where entity_id in ({}) and configuration_id is \"{}\"" ) settings_key = "clean_hierarchical_attr" @@ -65,17 +65,14 @@ class CleanHierarchicalAttrsAction(BaseAction): ) ) configuration_id = attr["id"] - call_expr = [{ - "action": "query", - "expression": self.cust_attr_query.format( + values = session.query( + self.cust_attr_query.format( entity_ids_joined, configuration_id ) - }] - - [values] = self.session.call(call_expr) + ).all() data = {} - for item in values["data"]: + for item in values: value = item["value"] if value is None: data[item["entity_id"]] = value @@ -90,10 +87,10 @@ class CleanHierarchicalAttrsAction(BaseAction): len(data), configuration_key )) for entity_id, value in data.items(): - entity_key = collections.OrderedDict({ - "configuration_id": configuration_id, - "entity_id": entity_id - }) + entity_key = collections.OrderedDict(( + ("configuration_id", configuration_id), + ("entity_id", entity_id) + )) session.recorded_operations.push( ftrack_api.operation.DeleteEntityOperation( "CustomAttributeValue", diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py index 0bd243ab4c..961f3b9793 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py @@ -306,8 +306,8 @@ class CustomAttributes(BaseAction): } cust_attr_query = ( - "select value, entity_id from ContextCustomAttributeValue " - "where configuration_id is {}" + "select value, entity_id from CustomAttributeValue" + " where configuration_id is {}" ) for attr_def in object_type_attrs: attr_ent_type = attr_def["entity_type"] @@ -328,21 +328,14 @@ class CustomAttributes(BaseAction): self.log.debug(( "Converting Avalon MongoID attr for Entity type \"{}\"." ).format(entity_type_label)) - - call_expr = [{ - "action": "query", - "expression": cust_attr_query.format(attr_def["id"]) - }] - if hasattr(session, "call"): - [values] = session.call(call_expr) - else: - [values] = session._call(call_expr) - - for value in values["data"]: - table_values = collections.OrderedDict({ - "configuration_id": hierarchical_attr["id"], - "entity_id": value["entity_id"] - }) + values = session.query( + cust_attr_query.format(attr_def["id"]) + ).all() + for value in values: + table_values = collections.OrderedDict(( + ("configuration_id", hierarchical_attr["id"]), + ("entity_id", value["entity_id"]) + )) session.recorded_operations.push( ftrack_api.operation.UpdateEntityOperation( diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index 38ec02749a..24fc2c5cad 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -303,9 +303,10 @@ class FtrackModule( # TODO add add permissions check # TODO add value validations # - value type and list items - entity_key = collections.OrderedDict() - entity_key["configuration_id"] = configuration["id"] - entity_key["entity_id"] = project_id + entity_key = collections.OrderedDict(( + ("configuration_id", configuration["id"]) + ("entity_id", project_id) + )) session.recorded_operations.push( ftrack_api.operation.UpdateEntityOperation( diff --git a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py index 66cf7645c2..29d2df57ee 100644 --- a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py @@ -1,11 +1,8 @@ -import os import re import json import collections import copy -import six - from avalon.api import AvalonMongoDB import avalon @@ -18,7 +15,7 @@ from openpype.api import ( from openpype.lib import ApplicationManager from .constants import CUST_ATTR_ID_KEY -from .custom_attributes import get_openpype_attr +from .custom_attributes import get_openpype_attr, query_custom_attributes from bson.objectid import ObjectId from bson.errors import InvalidId @@ -235,33 +232,19 @@ def get_hierarchical_attributes_values( entity_ids = [item["id"] for item in entity["link"]] - join_ent_ids = join_query_keys(entity_ids) - join_attribute_ids = join_query_keys(attr_key_by_id.keys()) - - queries = [] - queries.append({ - "action": "query", - "expression": ( - "select value, configuration_id, entity_id" - " from CustomAttributeValue" - " where entity_id in ({}) and configuration_id in ({})" - ).format(join_ent_ids, join_attribute_ids) - }) - - if hasattr(session, "call"): - [values] = session.call(queries) - else: - [values] = session._call(queries) + values = query_custom_attributes( + session, list(attr_key_by_id.keys()), entity_ids, True + ) hier_values = {} for key, val in defaults.items(): hier_values[key] = val - if not values["data"]: + if not values: return hier_values values_by_entity_id = collections.defaultdict(dict) - for item in values["data"]: + for item in values: value = item["value"] if value is None: continue @@ -861,33 +844,6 @@ class SyncEntitiesFactory: self.entities_dict[parent_id]["children"].remove(ftrack_id) - def _query_custom_attributes(self, session, conf_ids, entity_ids): - output = [] - # Prepare values to query - attributes_joined = join_query_keys(conf_ids) - attributes_len = len(conf_ids) - chunk_size = int(5000 / attributes_len) - for idx in range(0, len(entity_ids), chunk_size): - entity_ids_joined = join_query_keys( - entity_ids[idx:idx + chunk_size] - ) - - call_expr = [{ - "action": "query", - "expression": ( - "select value, entity_id from ContextCustomAttributeValue " - "where entity_id in ({}) and configuration_id in ({})" - ).format(entity_ids_joined, attributes_joined) - }] - if hasattr(session, "call"): - [result] = session.call(call_expr) - else: - [result] = session._call(call_expr) - - for item in result["data"]: - output.append(item) - return output - def set_cutom_attributes(self): self.log.debug("* Preparing custom attributes") # Get custom attributes and values @@ -994,7 +950,7 @@ class SyncEntitiesFactory: copy.deepcopy(prepared_avalon_attr_ca_id) ) - items = self._query_custom_attributes( + items = query_custom_attributes( self.session, list(attribute_key_by_id.keys()), sync_ids @@ -1082,10 +1038,11 @@ class SyncEntitiesFactory: for key, val in prepare_dict_avalon.items(): entity_dict["avalon_attrs"][key] = val - items = self._query_custom_attributes( + items = query_custom_attributes( self.session, list(attribute_key_by_id.keys()), - sync_ids + sync_ids, + True ) avalon_hier = [] diff --git a/openpype/modules/default_modules/ftrack/lib/custom_attributes.py b/openpype/modules/default_modules/ftrack/lib/custom_attributes.py index 53facd4ab2..8b1cbb8e54 100644 --- a/openpype/modules/default_modules/ftrack/lib/custom_attributes.py +++ b/openpype/modules/default_modules/ftrack/lib/custom_attributes.py @@ -88,26 +88,36 @@ def join_query_keys(keys): return ",".join(["\"{}\"".format(key) for key in keys]) -def query_custom_attributes(session, conf_ids, entity_ids, table_name=None): +def query_custom_attributes( + session, conf_ids, entity_ids, only_set_values=False +): """Query custom attribute values from ftrack database. Using ftrack call method result may differ based on used table name and version of ftrack server. + For hierarchical attributes you shou always use `only_set_values=True` + otherwise result will be default value of custom attribute and it would not + be possible to differentiate if value is set on entity or default value is + used. + Args: session(ftrack_api.Session): Connected ftrack session. conf_id(list, set, tuple): Configuration(attribute) ids which are queried. entity_ids(list, set, tuple): Entity ids for which are values queried. - table_name(str): Table nam from which values are queried. Not - recommended to change until you know what it means. + only_set_values(bool): Entities that don't have explicitly set + value won't return a value. If is set to False then default custom + attribute value is returned if value is not set. """ output = [] # Just skip if not conf_ids or not entity_ids: return output - if table_name is None: + if only_set_values: + table_name = "CustomAttributeValue" + else: table_name = "ContextCustomAttributeValue" # Prepare values to query @@ -122,19 +132,16 @@ def query_custom_attributes(session, conf_ids, entity_ids, table_name=None): entity_ids_joined = join_query_keys( entity_ids[idx:idx + chunk_size] ) - - call_expr = [{ - "action": "query", - "expression": ( - "select value, entity_id from {}" - " where entity_id in ({}) and configuration_id in ({})" - ).format(table_name, entity_ids_joined, attributes_joined) - }] - if hasattr(session, "call"): - [result] = session.call(call_expr) - else: - [result] = session._call(call_expr) - - for item in result["data"]: - output.append(item) + output.extend( + session.query( + ( + "select value, entity_id from {}" + " where entity_id in ({}) and configuration_id in ({})" + ).format( + table_name, + entity_ids_joined, + attributes_joined + ) + ).all() + ) return output From a98f5da9866c4aaf8348ee65366f92294303d034 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Feb 2022 16:50:57 +0100 Subject: [PATCH 53/59] fix OrderedDict creation --- .../event_handlers_user/action_create_cust_attrs.py | 4 ++-- openpype/modules/default_modules/ftrack/ftrack_module.py | 4 ++-- .../modules/default_modules/ftrack/lib/avalon_sync.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py index 961f3b9793..cb5b88ad50 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py @@ -332,10 +332,10 @@ class CustomAttributes(BaseAction): cust_attr_query.format(attr_def["id"]) ).all() for value in values: - table_values = collections.OrderedDict(( + table_values = collections.OrderedDict([ ("configuration_id", hierarchical_attr["id"]), ("entity_id", value["entity_id"]) - )) + ]) session.recorded_operations.push( ftrack_api.operation.UpdateEntityOperation( diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index 24fc2c5cad..0c4dedc694 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -303,10 +303,10 @@ class FtrackModule( # TODO add add permissions check # TODO add value validations # - value type and list items - entity_key = collections.OrderedDict(( + entity_key = collections.OrderedDict([ ("configuration_id", configuration["id"]) ("entity_id", project_id) - )) + ]) session.recorded_operations.push( ftrack_api.operation.UpdateEntityOperation( diff --git a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py index 29d2df57ee..06e8784287 100644 --- a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py @@ -1763,10 +1763,10 @@ class SyncEntitiesFactory: configuration_id = self.entities_dict[ftrack_id][ "avalon_attrs_id"][CUST_ATTR_ID_KEY] - _entity_key = collections.OrderedDict({ - "configuration_id": configuration_id, - "entity_id": ftrack_id - }) + _entity_key = collections.OrderedDict([ + ("configuration_id", configuration_id), + ("entity_id", ftrack_id) + ]) self.session.recorded_operations.push( ftrack_api.operation.UpdateEntityOperation( From 0716c1c921c4e08a7f7f27476bfdbb907dd124ba Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Feb 2022 16:57:42 +0100 Subject: [PATCH 54/59] fix missing comma --- openpype/modules/default_modules/ftrack/ftrack_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index 0c4dedc694..5c38df2e03 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -304,7 +304,7 @@ class FtrackModule( # TODO add value validations # - value type and list items entity_key = collections.OrderedDict([ - ("configuration_id", configuration["id"]) + ("configuration_id", configuration["id"]), ("entity_id", project_id) ]) From 9ad09904f4eb28a71ca48ebc6863c6d067cfcd22 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Feb 2022 17:10:35 +0100 Subject: [PATCH 55/59] fix fps validation popup --- openpype/hosts/maya/api/lib.py | 37 ++++++++++++++-------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 27a7061f74..1f6c8c1deb 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2440,34 +2440,27 @@ def validate_fps(): # rounding, we have to round those numbers coming from Maya. current_fps = float_round(mel.eval('currentTimeUnitToFPS()'), 2) - if current_fps != fps: + fps_match = current_fps == fps + if not fps_match and not IS_HEADLESS: + from openpype.widgets import popup - from Qt import QtWidgets - from ...widgets import popup + parent = get_main_window() - # Find maya main window - top_level_widgets = {w.objectName(): w for w in - QtWidgets.QApplication.topLevelWidgets()} + dialog = popup.Popup2(parent=parent) + dialog.setModal(True) + dialog.setWindowTitle("Maya scene not in line with project") + dialog.setMessage("The FPS is out of sync, please fix") - parent = top_level_widgets.get("MayaWindow", None) - if parent is None: - pass - else: - dialog = popup.Popup2(parent=parent) - dialog.setModal(True) - dialog.setWindowTitle("Maya scene not in line with project") - dialog.setMessage("The FPS is out of sync, please fix") + # Set new text for button (add optional argument for the popup?) + toggle = dialog.widgets["toggle"] + update = toggle.isChecked() + dialog.on_show.connect(lambda: set_scene_fps(fps, update)) - # Set new text for button (add optional argument for the popup?) - toggle = dialog.widgets["toggle"] - update = toggle.isChecked() - dialog.on_show.connect(lambda: set_scene_fps(fps, update)) + dialog.show() - dialog.show() + return False - return False - - return True + return fps_match def bake(nodes, From dd004e91429ee9cab7924bff629a647d19b6399b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Feb 2022 17:36:47 +0100 Subject: [PATCH 56/59] use static method of QApplication to get deskop --- openpype/widgets/popup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/widgets/popup.py b/openpype/widgets/popup.py index 3c3f6283c4..e661d3d293 100644 --- a/openpype/widgets/popup.py +++ b/openpype/widgets/popup.py @@ -132,12 +132,12 @@ class Popup2(Popup): """ parent_widget = self.parent() - app = QtWidgets.QApplication.instance() + desktop = QtWidgets.QApplication.desktop() if parent_widget: - screen = app.desktop().screenNumber(parent_widget) + screen = desktop.screenNumber(parent_widget) else: - screen = app.desktop().screenNumber(app.desktop().cursor().pos()) - center_point = app.desktop().screenGeometry(screen).center() + screen = desktop.screenNumber(desktop.cursor().pos()) + center_point = desktop.screenGeometry(screen).center() frame_geo = self.frameGeometry() frame_geo.moveCenter(center_point) From 920492c475648518fac08f42a62870a5ed3285c3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Feb 2022 11:00:13 +0100 Subject: [PATCH 57/59] fixed launch crash --- openpype/lib/applications.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index afa0a31af1..88f333a2af 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -929,7 +929,9 @@ class ApplicationLaunchContext: # --- START: Backwards compatibility --- hooks_dir = os.path.join(pype_dir, "hooks") - subfolder_names = ["global", self.host_name] + subfolder_names = ["global"] + if self.host_name: + subfolder_names.append(self.host_name) for subfolder_name in subfolder_names: path = os.path.join(hooks_dir, subfolder_name) if ( @@ -940,10 +942,12 @@ class ApplicationLaunchContext: paths.append(path) # --- END: Backwards compatibility --- - subfolders_list = ( - ["hooks"], - ("hosts", self.host_name, "hooks") - ) + subfolders_list = [ + ["hooks"] + ] + if self.host_name: + subfolders_list.append(["hosts", self.host_name, "hooks"]) + for subfolders in subfolders_list: path = os.path.join(pype_dir, *subfolders) if ( From 36730e38d4d6e3434a633e8c1f31a8aeb19ed2db Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 10 Feb 2022 13:29:56 +0100 Subject: [PATCH 58/59] fix redshift prefix, creator options and repair action --- openpype/hosts/maya/plugins/create/create_render.py | 6 ++---- .../hosts/maya/plugins/publish/validate_rendersettings.py | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index fa5e73f3ed..6469a43201 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -86,7 +86,7 @@ class CreateRender(plugin.Creator): 'vray': 'maya///', 'arnold': 'maya///{aov_separator}', # noqa 'renderman': 'maya///{aov_separator}', - 'redshift': 'maya///{aov_separator}' # noqa + 'redshift': 'maya///' # noqa } _aov_chars = { @@ -455,9 +455,7 @@ class CreateRender(plugin.Creator): if renderer == "vray": self._set_vray_settings(asset) if renderer == "redshift": - _ = self._set_renderer_option( - "RedshiftOptions", "{}.imageFormat", 1 - ) + cmds.setAttr("redshiftOptions.imageFormat", 1) # resolution cmds.setAttr( diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 6079d34fbe..8570e46bfd 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -329,7 +329,8 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): for aov in rs_aovs: # fix AOV prefixes cmds.setAttr( - "{}.filePrefix".format(aov), redshift_AOV_prefix) + "{}.filePrefix".format(aov), + redshift_AOV_prefix, type="string") # fix AOV file format default_ext = cmds.getAttr( "redshiftOptions.imageFormat", asString=True) From 5385c32900a5d2c2f9ff84a2182def826459dd6e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 10 Feb 2022 15:09:03 +0100 Subject: [PATCH 59/59] fixed aov separator for redshift, validator error message --- openpype/hosts/maya/api/lib_renderproducts.py | 2 +- .../hosts/maya/plugins/publish/validate_rendersettings.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index e8e4b9aaef..0c34998874 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -911,7 +911,7 @@ class RenderProductsRedshift(ARenderProducts): """ prefix = super(RenderProductsRedshift, self).get_renderer_prefix() - prefix = "{}.".format(prefix) + prefix = "{}{}".format(prefix, self.aov_separator) return prefix def get_render_products(self): diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 8570e46bfd..e24e88cab7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -172,8 +172,8 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): cls.log.error(("AOV ({}) image prefix is not set " "correctly {} != {}").format( cmds.getAttr("{}.name".format(aov)), - cmds.getAttr("{}.filePrefix".format(aov)), - aov_prefix + aov_prefix, + redshift_AOV_prefix )) invalid = True # get aov format