From 8dbc80c2d7643eae7835c908ec770c4c12730439 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Jan 2022 18:11:10 +0100 Subject: [PATCH 01/14] 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 02/14] 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 03/14] 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 04/14] 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 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 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 05/14] 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 06/14] 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 07/14] 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 08/14] 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 09/14] 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 10/14] 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 11/14] 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 12/14] 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 026308feab67093f08ba1c0f8f5e20f691c51723 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Feb 2022 14:33:59 +0100 Subject: [PATCH 13/14] 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 14/14] 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