From ec405eb9130c6fe9c8b13dde34983fb43341507d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 11:52:46 +0200 Subject: [PATCH 01/23] prepared some classes to handle settings locks --- openpype/settings/handlers.py | 162 ++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 15ae2351fd..8d13875d0b 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -22,6 +22,168 @@ from .constants import ( ) +class SettingsStateInfo: + """Helper state information for Settings state. + + Is used to hold information about last save and last opened UI. Keep + information about the time when that happened and on which machine under + which user. + + To create currrent machine and time information use 'create_new' method. + """ + + timestamp_format = "%Y-%m-%d %H:%M:%S.%f" + + def __init__( + self, timestamp, hostname, hostip, username, system_name, local_id + ): + self.timestamp = timestamp + self._timestamp_obj = datetime.datetime.strptime( + timestamp, self.timestamp_format + ) + self.hostname = hostname + self.hostip = hostip + self.username = username + self.system_name = system_name + self.local_id = local_id + + def copy(self): + return self.from_data(self.to_data()) + + @property + def timestamp_obj(self): + return self._timestamp_obj + + @classmethod + def create_new(cls): + """Create information about this machine for current time.""" + + from openpype.lib.pype_info import get_workstation_info + + now = datetime.datetime.now() + workstation_info = get_workstation_info() + + return cls( + now.strftime(cls.timestamp_format), + workstation_info["hostname"], + workstation_info["hostip"], + workstation_info["username"], + workstation_info["system_name"], + workstation_info["local_id"] + ) + + @classmethod + def from_data(cls, data): + """Create object from data.""" + + return cls( + data["timestamp"], + data["hostname"], + data["hostip"], + data["username"], + data["system_name"], + data["local_id"] + ) + + def to_data(self): + return { + "timestamp": self.timestamp, + "hostname": self.hostname, + "hostip": self.hostip, + "username": self.username, + "system_name": self.system_name, + "local_id": self.local_id, + } + + def __eq__(self, other): + if not isinstance(other, SettingsStateInfo): + return False + + if other.timestamp_obj != self.timestamp_obj: + return False + + return ( + self.hostname == other.hostname + and self.hostip == other.hostip + and self.username == other.username + and self.system_name == other.system_name + and self.local_id == other.local_id + ) + + +class SettingsState: + """State of settings with last saved and last opened. + + Args: + openpype_version (str): OpenPype version string. + settings_type (str): Type of settings. System or project settings. + last_saved_info (Union[None, SettingsStateInfo]): Information about + machine and time when were settings saved last time. + last_opened_info (Union[None, SettingsStateInfo]): This is settings UI + specific information similar to last saved describes who had opened + settings as last. + project_name (Union[None, str]): Identifier for project settings. + """ + + def __init__( + self, + openpype_version, + settings_type, + last_saved_info, + last_opened_info, + project_name=None + ): + self.openpype_version = openpype_version + self.settings_type = settings_type + self.last_saved_info = last_saved_info + self.last_opened_info = last_opened_info + self.project_name = project_name + + def __eq__(self, other): + if not isinstance(other, SettingsState): + return False + + return ( + self.openpype_version == other.openpype_version + and self.settings_type == other.settings_type + and self.last_saved_info == other.last_saved_info + and self.last_opened_info == other.last_opened_info + and self.project_name == other.project_name + ) + + def copy(self): + return self.__class__( + self.openpype_version, + self.settings_type, + self.last_saved_info.copy(), + self.last_opened_info.copy(), + self.project_name + ) + + def on_save(self, openpype_version): + self.openpype_version = openpype_version + self.last_saved_info = SettingsStateInfo.create_new() + + @classmethod + def from_document(cls, openpype_version, settings_type, document): + document = document or {} + last_saved_info = document.get("last_saved_info") + if last_saved_info: + last_saved_info = SettingsStateInfo.from_data(last_saved_info) + + last_opened_info = document.get("last_opened_info") + if last_opened_info: + last_opened_info = SettingsStateInfo.from_data(last_opened_info) + + return cls( + openpype_version, + settings_type, + last_saved_info, + last_opened_info, + document.get("project_name") + ) + + @six.add_metaclass(ABCMeta) class SettingsHandler: @abstractmethod From 17957a760c4c75ded01e944605f164ce058b252c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 11:53:24 +0200 Subject: [PATCH 02/23] 'update_data' and 'update_from_document' always require version --- openpype/settings/handlers.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 8d13875d0b..5a0a30e4a6 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -453,13 +453,12 @@ class CacheValues: return {} return copy.deepcopy(self.data) - def update_data(self, data, version=None): + def update_data(self, data, version): self.data = data self.creation_time = datetime.datetime.now() - if version is not None: - self.version = version + self.version = version - def update_from_document(self, document, version=None): + def update_from_document(self, document, version): data = {} if document: if "data" in document: @@ -468,9 +467,9 @@ class CacheValues: value = document["value"] if value: data = json.loads(value) + self.data = data - if version is not None: - self.version = version + self.version = version def to_json_string(self): return json.dumps(self.data or {}) @@ -1567,7 +1566,7 @@ class MongoLocalSettingsHandler(LocalSettingsHandler): """ data = data or {} - self.local_settings_cache.update_data(data) + self.local_settings_cache.update_data(data, None) self.collection.replace_one( { @@ -1590,6 +1589,6 @@ class MongoLocalSettingsHandler(LocalSettingsHandler): "site_id": self.local_site_id }) - self.local_settings_cache.update_from_document(document) + self.local_settings_cache.update_from_document(document, None) return self.local_settings_cache.data_copy() From 0dc4f1a78622edf932d8623b8ecafdae8c18afc7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 11:53:49 +0200 Subject: [PATCH 03/23] cache also can have settings state --- openpype/settings/handlers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 5a0a30e4a6..0ee1f74692 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -447,6 +447,7 @@ class CacheValues: self.data = None self.creation_time = None self.version = None + self.settings_state = None def data_copy(self): if not self.data: @@ -458,6 +459,9 @@ class CacheValues: self.creation_time = datetime.datetime.now() self.version = version + def update_settings_state(self, settings_state): + self.settings_state = settings_state + def update_from_document(self, document, version): data = {} if document: From 0e6ff4a21d224ed188cdf076a4fd00a1a8f696ea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 15:59:51 +0200 Subject: [PATCH 04/23] cache can be set to outdated --- openpype/settings/handlers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 0ee1f74692..f6e81a7d0a 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -485,6 +485,9 @@ class CacheValues: delta = (datetime.datetime.now() - self.creation_time).seconds return delta > self.cache_lifetime + def set_outdated(self): + self.create_time = None + class MongoSettingsHandler(SettingsHandler): """Settings handler that use mongo for storing and loading of settings.""" From 4a322f1ceda11843c13627ee5c1f1d9070c82f12 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 16:00:23 +0200 Subject: [PATCH 05/23] removed 'SettingsState' and kept only 'SettingsStateInfo' --- openpype/settings/handlers.py | 152 +++++++++++++++------------------- 1 file changed, 66 insertions(+), 86 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index f6e81a7d0a..e4b4bc3dc4 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -23,11 +23,11 @@ from .constants import ( class SettingsStateInfo: - """Helper state information for Settings state. + """Helper state information about some settings state. - Is used to hold information about last save and last opened UI. Keep + Is used to hold information about last saved and last opened UI. Keep information about the time when that happened and on which machine under - which user. + which user and on which openpype version. To create currrent machine and time information use 'create_new' method. """ @@ -35,12 +35,28 @@ class SettingsStateInfo: timestamp_format = "%Y-%m-%d %H:%M:%S.%f" def __init__( - self, timestamp, hostname, hostip, username, system_name, local_id + self, + openpype_version, + settings_type, + project_name, + timestamp, + hostname, + hostip, + username, + system_name, + local_id ): + self.openpype_version = openpype_version + self.settings_type = settings_type + self.project_name = project_name + + timestamp_obj = None + if timestamp: + timestamp_obj = datetime.datetime.strptime( + timestamp, self.timestamp_format + ) self.timestamp = timestamp - self._timestamp_obj = datetime.datetime.strptime( - timestamp, self.timestamp_format - ) + self.timestamp_obj = timestamp_obj self.hostname = hostname self.hostip = hostip self.username = username @@ -50,12 +66,8 @@ class SettingsStateInfo: def copy(self): return self.from_data(self.to_data()) - @property - def timestamp_obj(self): - return self._timestamp_obj - @classmethod - def create_new(cls): + def create_new(cls, openpype_version, settings_type, project_name): """Create information about this machine for current time.""" from openpype.lib.pype_info import get_workstation_info @@ -64,6 +76,9 @@ class SettingsStateInfo: workstation_info = get_workstation_info() return cls( + openpype_version, + settings_type, + project_name, now.strftime(cls.timestamp_format), workstation_info["hostname"], workstation_info["hostip"], @@ -77,6 +92,9 @@ class SettingsStateInfo: """Create object from data.""" return cls( + data["openpype_version"], + data["settings_type"], + data["project_name"], data["timestamp"], data["hostname"], data["hostip"], @@ -86,6 +104,40 @@ class SettingsStateInfo: ) def to_data(self): + data = self.to_document_data() + data.update({ + "openpype_version": self.openpype_version, + "settings_type": self.settings_type, + "project_name": self.project_name + }) + return data + + @classmethod + def from_document(cls, openpype_version, settings_type, document): + document = document or {} + project_name = document.get("project_name") + last_saved_info = document.get("last_saved_info") + if last_saved_info: + copy_last_saved_info = copy.deepcopy(last_saved_info) + copy_last_saved_info.update({ + "openpype_version": openpype_version, + "settings_type": settings_type, + "project_name": project_name, + }) + return cls.from_data(copy_last_saved_info) + return cls( + openpype_version, + settings_type, + project_name, + None, + None, + None, + None, + None, + None + ) + + def to_document_data(self): return { "timestamp": self.timestamp, "hostname": self.hostname, @@ -103,7 +155,8 @@ class SettingsStateInfo: return False return ( - self.hostname == other.hostname + self.openpype_version == other.openpype_version + and self.hostname == other.hostname and self.hostip == other.hostip and self.username == other.username and self.system_name == other.system_name @@ -111,79 +164,6 @@ class SettingsStateInfo: ) -class SettingsState: - """State of settings with last saved and last opened. - - Args: - openpype_version (str): OpenPype version string. - settings_type (str): Type of settings. System or project settings. - last_saved_info (Union[None, SettingsStateInfo]): Information about - machine and time when were settings saved last time. - last_opened_info (Union[None, SettingsStateInfo]): This is settings UI - specific information similar to last saved describes who had opened - settings as last. - project_name (Union[None, str]): Identifier for project settings. - """ - - def __init__( - self, - openpype_version, - settings_type, - last_saved_info, - last_opened_info, - project_name=None - ): - self.openpype_version = openpype_version - self.settings_type = settings_type - self.last_saved_info = last_saved_info - self.last_opened_info = last_opened_info - self.project_name = project_name - - def __eq__(self, other): - if not isinstance(other, SettingsState): - return False - - return ( - self.openpype_version == other.openpype_version - and self.settings_type == other.settings_type - and self.last_saved_info == other.last_saved_info - and self.last_opened_info == other.last_opened_info - and self.project_name == other.project_name - ) - - def copy(self): - return self.__class__( - self.openpype_version, - self.settings_type, - self.last_saved_info.copy(), - self.last_opened_info.copy(), - self.project_name - ) - - def on_save(self, openpype_version): - self.openpype_version = openpype_version - self.last_saved_info = SettingsStateInfo.create_new() - - @classmethod - def from_document(cls, openpype_version, settings_type, document): - document = document or {} - last_saved_info = document.get("last_saved_info") - if last_saved_info: - last_saved_info = SettingsStateInfo.from_data(last_saved_info) - - last_opened_info = document.get("last_opened_info") - if last_opened_info: - last_opened_info = SettingsStateInfo.from_data(last_opened_info) - - return cls( - openpype_version, - settings_type, - last_saved_info, - last_opened_info, - document.get("project_name") - ) - - @six.add_metaclass(ABCMeta) class SettingsHandler: @abstractmethod From 20509b4610250c6d6725c50659b0ce3a065b0e92 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 16:02:46 +0200 Subject: [PATCH 06/23] changed 'settings_state' to 'last_saved_info' in cache --- openpype/settings/handlers.py | 44 ++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index e4b4bc3dc4..e34a4c3540 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -368,7 +368,7 @@ class SettingsHandler: """OpenPype versions that have any studio project anatomy overrides. Returns: - list: OpenPype versions strings. + List[str]: OpenPype versions strings. """ pass @@ -379,7 +379,7 @@ class SettingsHandler: """OpenPype versions that have any studio project settings overrides. Returns: - list: OpenPype versions strings. + List[str]: OpenPype versions strings. """ pass @@ -393,8 +393,39 @@ class SettingsHandler: project_name(str): Name of project. Returns: - list: OpenPype versions strings. + List[str]: OpenPype versions strings. """ + + pass + + @abstractmethod + def get_system_last_saved_info(self): + """State of last system settings overrides at the moment when called. + + This method must provide most recent data so using cached data is not + the way. + + Returns: + SettingsStateInfo: Information about system settings overrides. + """ + + pass + + @abstractmethod + def get_project_last_saved_info(self, project_name): + """State of last project settings overrides at the moment when called. + + This method must provide most recent data so using cached data is not + the way. + + Args: + project_name (Union[None, str]): Project name for which state + should be returned. + + Returns: + SettingsStateInfo: Information about project settings overrides. + """ + pass @@ -427,7 +458,7 @@ class CacheValues: self.data = None self.creation_time = None self.version = None - self.settings_state = None + self.last_saved_info = None def data_copy(self): if not self.data: @@ -439,8 +470,8 @@ class CacheValues: self.creation_time = datetime.datetime.now() self.version = version - def update_settings_state(self, settings_state): - self.settings_state = settings_state + def update_last_saved_info(self, last_saved_info): + self.last_saved_info = last_saved_info def update_from_document(self, document, version): data = {} @@ -1288,6 +1319,7 @@ class MongoSettingsHandler(SettingsHandler): self.project_anatomy_cache[project_name].update_from_document( document, version ) + else: project_doc = get_project(project_name) self.project_anatomy_cache[project_name].update_data( From ba434d5f713de2eb478bd1cea533163d482d4033 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 16:03:49 +0200 Subject: [PATCH 07/23] changed how update of settings happens --- openpype/settings/handlers.py | 49 +++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index e34a4c3540..43b1d37c34 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -696,20 +696,28 @@ class MongoSettingsHandler(SettingsHandler): system_settings_data ) - # Store system settings - self.collection.replace_one( + system_settings_doc = self.collection.find_one( { "type": self._system_settings_key, "version": self._current_version }, - { - "type": self._system_settings_key, - "data": system_settings_data, - "version": self._current_version - }, - upsert=True + {"_id": True} ) + # Store system settings + new_system_settings_doc = { + "type": self._system_settings_key, + "version": self._current_version, + "data": system_settings_data, + } + if not system_settings_doc: + self.collections.insert_one(new_system_settings_doc) + else: + self.collections.update_one( + {"_id": system_settings_doc["_id"]}, + {"$set": new_system_settings_doc} + ) + # Store global settings self.collection.replace_one( { @@ -844,26 +852,33 @@ class MongoSettingsHandler(SettingsHandler): def _save_project_data(self, project_name, doc_type, data_cache): is_default = bool(project_name is None) - replace_filter = { + query_filter = { "type": doc_type, "is_default": is_default, "version": self._current_version } - replace_data = { + last_saved_info = data_cache.last_saved_info + new_project_settings_doc = { "type": doc_type, "data": data_cache.data, "is_default": is_default, - "version": self._current_version + "version": self._current_version, } if not is_default: - replace_filter["project_name"] = project_name - replace_data["project_name"] = project_name + query_filter["project_name"] = project_name + new_project_settings_doc["project_name"] = project_name - self.collection.replace_one( - replace_filter, - replace_data, - upsert=True + project_settings_doc = self.collection.find_one( + query_filter, + {"_id": True} ) + if project_settings_doc: + self.collection.update_one( + {"_id": project_settings_doc["_id"]}, + new_project_settings_doc + ) + else: + self.collection.insert_one(new_project_settings_doc) def _get_versions_order_doc(self, projection=None): # TODO cache From 6c9d6b3865cfdce385aba868d61b9cb09985ab88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 16:05:15 +0200 Subject: [PATCH 08/23] added helper methods for query of override documents --- openpype/settings/handlers.py | 57 +++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 43b1d37c34..6080b5e77f 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -1205,18 +1205,7 @@ class MongoSettingsHandler(SettingsHandler): globals_document = self.collection.find_one({ "type": GLOBAL_SETTINGS_KEY }) - document = ( - self._get_studio_system_settings_overrides_for_version() - ) - 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 + document, version = self._get_system_settings_overrides_doc() merged_document = self._apply_global_settings( document, globals_document @@ -1232,21 +1221,27 @@ class MongoSettingsHandler(SettingsHandler): return data, cache.version return data + def _get_system_settings_overrides_doc(self): + document = ( + self._get_studio_system_settings_overrides_for_version() + ) + 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 + + return document, version + 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( + document, version = self._get_project_settings_overrides_doc( 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, version ) @@ -1257,6 +1252,22 @@ class MongoSettingsHandler(SettingsHandler): return data, cache.version return data + def _get_project_settings_overrides_doc(self, project_name): + 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 + + return document, version + def get_studio_project_settings_overrides(self, return_version): """Studio overrides of default project settings.""" return self._get_project_settings_overrides(None, return_version) From 8f121275bdbb08eb686ce482f529b97c96dfe1cb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 16:06:11 +0200 Subject: [PATCH 09/23] implemented methods to get last saved information --- openpype/settings/handlers.py | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 6080b5e77f..af2bf104de 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -688,6 +688,15 @@ class MongoSettingsHandler(SettingsHandler): # Update cache self.system_settings_cache.update_data(data, self._current_version) + last_saved_info = SettingsStateInfo.create_new( + self._current_version, + SYSTEM_SETTINGS_KEY, + None + ) + self.system_settings_cache.update_last_saved_info( + last_saved_info + ) + # Get copy of just updated cache system_settings_data = self.system_settings_cache.data_copy() @@ -709,6 +718,7 @@ class MongoSettingsHandler(SettingsHandler): "type": self._system_settings_key, "version": self._current_version, "data": system_settings_data, + "last_saved_info": last_saved_info.to_document_data() } if not system_settings_doc: self.collections.insert_one(new_system_settings_doc) @@ -749,6 +759,14 @@ class MongoSettingsHandler(SettingsHandler): data_cache = self.project_settings_cache[project_name] data_cache.update_data(overrides, self._current_version) + last_saved_info = SettingsStateInfo.create_new( + self._current_version, + PROJECT_SETTINGS_KEY, + project_name + ) + + data_cache.update_last_saved_info(last_saved_info) + self._save_project_data( project_name, self._project_settings_key, data_cache ) @@ -863,6 +881,7 @@ class MongoSettingsHandler(SettingsHandler): "data": data_cache.data, "is_default": is_default, "version": self._current_version, + "last_saved_info": last_saved_info.to_data() } if not is_default: query_filter["project_name"] = project_name @@ -1207,6 +1226,9 @@ class MongoSettingsHandler(SettingsHandler): }) document, version = self._get_system_settings_overrides_doc() + last_saved_info = SettingsStateInfo.from_document( + version, SYSTEM_SETTINGS_KEY, document + ) merged_document = self._apply_global_settings( document, globals_document ) @@ -1214,6 +1236,9 @@ class MongoSettingsHandler(SettingsHandler): self.system_settings_cache.update_from_document( merged_document, version ) + self.system_settings_cache.update_last_saved_info( + last_saved_info + ) cache = self.system_settings_cache data = cache.data_copy() @@ -1237,6 +1262,13 @@ class MongoSettingsHandler(SettingsHandler): return document, version + def get_system_last_saved_info(self): + # Make sure settings are recaches + self.system_settings_cache.set_outdated() + self.get_studio_system_settings_overrides(False) + + return self.system_settings_cache.last_saved_info.copy() + def _get_project_settings_overrides(self, project_name, return_version): if self.project_settings_cache[project_name].is_outdated: document, version = self._get_project_settings_overrides_doc( @@ -1245,6 +1277,12 @@ class MongoSettingsHandler(SettingsHandler): self.project_settings_cache[project_name].update_from_document( document, version ) + last_saved_info = SettingsStateInfo.from_document( + version, PROJECT_SETTINGS_KEY, document + ) + self.project_settings_cache[project_name].update_last_saved_info( + last_saved_info + ) cache = self.project_settings_cache[project_name] data = cache.data_copy() @@ -1268,6 +1306,13 @@ class MongoSettingsHandler(SettingsHandler): return document, version + def get_project_last_saved_info(self, project_name): + # Make sure settings are recaches + self.project_settings_cache[project_name].set_outdated() + self._get_project_settings_overrides(project_name, False) + + return self.project_settings_cache[project_name].last_saved_info.copy() + def get_studio_project_settings_overrides(self, return_version): """Studio overrides of default project settings.""" return self._get_project_settings_overrides(None, return_version) From 9ea46a2a3690df28c2781cdd0919ed44e0d275fc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 16:45:37 +0200 Subject: [PATCH 10/23] make available api functions in settings to have access to lock information and last saved information --- openpype/settings/handlers.py | 127 +++++++++++++++++++++++++++++++++- openpype/settings/lib.py | 25 +++++++ 2 files changed, 149 insertions(+), 3 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index af2bf104de..3dc33503ea 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -67,7 +67,9 @@ class SettingsStateInfo: return self.from_data(self.to_data()) @classmethod - def create_new(cls, openpype_version, settings_type, project_name): + def create_new( + cls, openpype_version, settings_type=None, project_name=None + ): """Create information about this machine for current time.""" from openpype.lib.pype_info import get_workstation_info @@ -112,6 +114,20 @@ class SettingsStateInfo: }) return data + @classmethod + def create_new_empty(cls, openpype_version, settings_type=None): + return cls( + openpype_version, + settings_type, + None, + None, + None, + None, + None, + None, + None + ) + @classmethod def from_document(cls, openpype_version, settings_type, document): document = document or {} @@ -428,6 +444,54 @@ class SettingsHandler: pass + # UI related calls + @abstractmethod + def get_last_opened_info(self): + """Get information about last opened UI. + + Last opened UI is empty if there is noone who would have opened UI at + the moment when called. + + Returns: + Union[None, SettingsStateInfo]: Information about machine who had + opened Settings UI. + """ + + pass + + @abstractmethod + def opened_ui(self): + """Callback called when settings UI is opened. + + Information about this machine must be available when + 'get_last_opened_info' is called from anywhere until 'closed_ui' is + called again. + + Returns: + SettingsStateInfo: Object representing information about this + machine. Must be passed to 'closed_ui' when finished. + """ + + pass + + @abstractmethod + def closed_ui(self, info_obj): + """Callback called when settings UI is closed. + + From the moment this method is called the information about this + machine is removed and no more available when 'get_last_opened_info' + is called. + + Callback should validate if this machine is still stored as opened ui + before changing any value. + + Args: + info_obj (SettingsStateInfo): Object created when 'opened_ui' was + called. + """ + + pass + @six.add_metaclass(ABCMeta) class LocalSettingsHandler: @@ -690,8 +754,7 @@ class MongoSettingsHandler(SettingsHandler): last_saved_info = SettingsStateInfo.create_new( self._current_version, - SYSTEM_SETTINGS_KEY, - None + SYSTEM_SETTINGS_KEY ) self.system_settings_cache.update_last_saved_info( last_saved_info @@ -1610,6 +1673,64 @@ class MongoSettingsHandler(SettingsHandler): return output return self._sort_versions(output) + def get_last_opened_info(self): + doc = self.collection.find_one({ + "type": "last_opened_settings_ui", + "version": self._current_version + }) or {} + info_data = doc.get("info") + if not info_data: + return SettingsStateInfo.create_new_empty(self._current_version) + + # Fill not available information + info_data["openpype_version"] = self._current_version + info_data["settings_type"] = None + info_data["project_name"] = None + return SettingsStateInfo.from_data(info_data) + + def opened_ui(self): + doc_filter = { + "type": "last_opened_settings_ui", + "version": self._current_version + } + + opened_info = SettingsStateInfo.create_new(self._current_version) + new_doc_data = copy.deepcopy(doc_filter) + new_doc_data["info"] = opened_info.to_document_data() + + doc = self.collection.find_one( + doc_filter, + {"_id": True} + ) + if doc: + self.collection.update_one( + {"_id": doc["_id"]}, + {"$set": new_doc_data} + ) + else: + self.collection.insert_one(new_doc_data) + return opened_info + + def closed_ui(self, info_obj): + doc_filter = { + "type": "last_opened_settings_ui", + "version": self._current_version + } + doc = self.collection.find_one(doc_filter) or {} + info_data = doc.get("info") + if not info_data: + return + + info_data["openpype_version"] = self._current_version + info_data["settings_type"] = None + info_data["project_name"] = None + current_info = SettingsStateInfo.from_data(info_data) + if current_info == info_obj: + self.collection.update_one( + {"_id": doc["_id"]}, + {"$set": {"info": None}} + ) + class MongoLocalSettingsHandler(LocalSettingsHandler): """Settings handler that use mongo for store and load local settings. diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 6df41112c8..58cfd3862c 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -91,6 +91,31 @@ def calculate_changes(old_value, new_value): return changes +@require_handler +def get_system_last_saved_info(): + return _SETTINGS_HANDLER.get_system_last_saved_info() + + +@require_handler +def get_project_last_saved_info(project_name): + return _SETTINGS_HANDLER.get_project_last_saved_info(project_name) + + +@require_handler +def get_last_opened_info(): + return _SETTINGS_HANDLER.get_last_opened_info() + + +@require_handler +def opened_ui(): + return _SETTINGS_HANDLER.opened_ui() + + +@require_handler +def closed_ui(info_obj): + return _SETTINGS_HANDLER.closed_ui(info_obj) + + @require_handler def save_studio_settings(data): """Save studio overrides of system settings. From 9251a0fd4294725a5fb1dfbef68788c409554230 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 18:09:55 +0200 Subject: [PATCH 11/23] changed function names --- openpype/settings/handlers.py | 20 ++++++++++---------- openpype/settings/lib.py | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 3dc33503ea..1b59531943 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -460,22 +460,22 @@ class SettingsHandler: pass @abstractmethod - def opened_ui(self): + def opened_settings_ui(self): """Callback called when settings UI is opened. Information about this machine must be available when - 'get_last_opened_info' is called from anywhere until 'closed_ui' is - called again. + 'get_last_opened_info' is called from anywhere until + 'closed_settings_ui' is called again. Returns: SettingsStateInfo: Object representing information about this - machine. Must be passed to 'closed_ui' when finished. + machine. Must be passed to 'closed_settings_ui' when finished. """ pass @abstractmethod - def closed_ui(self, info_obj): + def closed_settings_ui(self, info_obj): """Callback called when settings UI is closed. From the moment this method is called the information about this @@ -486,8 +486,8 @@ class SettingsHandler: before changing any value. Args: - info_obj (SettingsStateInfo): Object created when 'opened_ui' was - called. + info_obj (SettingsStateInfo): Object created when + 'opened_settings_ui' was called. """ pass @@ -1680,7 +1680,7 @@ class MongoSettingsHandler(SettingsHandler): }) or {} info_data = doc.get("info") if not info_data: - return SettingsStateInfo.create_new_empty(self._current_version) + return None # Fill not available information info_data["openpype_version"] = self._current_version @@ -1688,7 +1688,7 @@ class MongoSettingsHandler(SettingsHandler): info_data["project_name"] = None return SettingsStateInfo.from_data(info_data) - def opened_ui(self): + def opened_settings_ui(self): doc_filter = { "type": "last_opened_settings_ui", "version": self._current_version @@ -1711,7 +1711,7 @@ class MongoSettingsHandler(SettingsHandler): self.collection.insert_one(new_doc_data) return opened_info - def closed_ui(self, info_obj): + def closed_settings_ui(self, info_obj): doc_filter = { "type": "last_opened_settings_ui", "version": self._current_version diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 58cfd3862c..5eaddf6e6e 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -107,13 +107,13 @@ def get_last_opened_info(): @require_handler -def opened_ui(): - return _SETTINGS_HANDLER.opened_ui() +def opened_settings_ui(): + return _SETTINGS_HANDLER.opened_settings_ui() @require_handler -def closed_ui(info_obj): - return _SETTINGS_HANDLER.closed_ui(info_obj) +def closed_settings_ui(info_obj): + return _SETTINGS_HANDLER.closed_settings_ui(info_obj) @require_handler From baa2505fbfa2055aded2e2c439805d44a3a82347 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 18:10:32 +0200 Subject: [PATCH 12/23] show a dialog if someone else has opened settings UI --- openpype/tools/settings/settings/window.py | 136 ++++++++++++++++++++- 1 file changed, 131 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 22778e4a5b..96f11f3932 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -1,4 +1,16 @@ from Qt import QtWidgets, QtGui, QtCore + +from openpype import style + +from openpype.lib import is_admin_password_required +from openpype.widgets import PasswordDialog + +from openpype.settings.lib import ( + get_last_opened_info, + opened_settings_ui, + closed_settings_ui, +) + from .categories import ( CategoryState, SystemWidget, @@ -10,10 +22,6 @@ from .widgets import ( SettingsTabWidget ) from .search_dialog import SearchEntitiesDialog -from openpype import style - -from openpype.lib import is_admin_password_required -from openpype.widgets import PasswordDialog class MainWidget(QtWidgets.QWidget): @@ -25,6 +33,10 @@ class MainWidget(QtWidgets.QWidget): def __init__(self, user_role, parent=None, reset_on_show=True): super(MainWidget, self).__init__(parent) + # Object referencing to this machine and time when UI was opened + # - is used on close event + self._last_opened_info = None + self._user_passed = False self._reset_on_show = reset_on_show @@ -74,7 +86,7 @@ class MainWidget(QtWidgets.QWidget): self._on_restart_required ) tab_widget.reset_started.connect(self._on_reset_started) - tab_widget.reset_started.connect(self._on_reset_finished) + tab_widget.reset_finished.connect(self._on_reset_finished) tab_widget.full_path_requested.connect(self._on_full_path_request) header_tab_widget.context_menu_requested.connect( @@ -131,11 +143,38 @@ class MainWidget(QtWidgets.QWidget): def showEvent(self, event): super(MainWidget, self).showEvent(event) + if self._reset_on_show: self._reset_on_show = False # Trigger reset with 100ms delay QtCore.QTimer.singleShot(100, self.reset) + elif not self._last_opened_info: + self._check_on_ui_open() + + def _check_on_ui_open(self): + last_opened_info = get_last_opened_info() + if last_opened_info is not None: + if self._last_opened_info != last_opened_info: + self._last_opened_info = None + else: + self._last_opened_info = opened_settings_ui() + + if self._last_opened_info is not None: + return + + dialog = SettingsUIOpenedElsewhere(last_opened_info, self) + dialog.exec_() + if dialog.result() == 1: + self._last_opened_info = opened_settings_ui() + return + + def closeEvent(self, event): + if self._last_opened_info: + closed_settings_ui(self._last_opened_info) + self._last_opened_info = None + super(MainWidget, self).closeEvent(event) + def _show_password_dialog(self): if self._password_dialog: self._password_dialog.open() @@ -221,6 +260,8 @@ class MainWidget(QtWidgets.QWidget): if current_widget is widget: self._update_search_dialog() + self._check_on_ui_open() + def keyPressEvent(self, event): if event.matches(QtGui.QKeySequence.Find): # todo: search in all widgets (or in active)? @@ -231,3 +272,88 @@ class MainWidget(QtWidgets.QWidget): return return super(MainWidget, self).keyPressEvent(event) + + +class SettingsUIOpenedElsewhere(QtWidgets.QDialog): + def __init__(self, info_obj, parent=None): + super(SettingsUIOpenedElsewhere, self).__init__(parent) + + self._result = 0 + + self.setWindowTitle("Someone else has opened Settings UI") + + message_label = QtWidgets.QLabel(( + "Someone else has opened Settings UI. That may cause data loss." + " Please contact the person on the other side." + "

You can open the UI in view-only mode or take" + " the control which will cause the other settings won't be able" + " to save changes.
" + ), self) + message_label.setWordWrap(True) + + separator_widget_1 = QtWidgets.QFrame(self) + separator_widget_2 = QtWidgets.QFrame(self) + for separator_widget in ( + separator_widget_1, + separator_widget_2 + ): + separator_widget.setObjectName("Separator") + separator_widget.setMinimumHeight(1) + separator_widget.setMaximumHeight(1) + + other_information = QtWidgets.QWidget(self) + other_information_layout = QtWidgets.QFormLayout(other_information) + other_information_layout.setContentsMargins(0, 0, 0, 0) + for label, value in ( + ("Username", info_obj.username), + ("Host name", info_obj.hostname), + ("Host IP", info_obj.hostip), + ("System name", info_obj.system_name), + ("Local ID", info_obj.local_id), + ("Time Stamp", info_obj.timestamp), + ): + other_information_layout.addRow( + label, + QtWidgets.QLabel(value, other_information) + ) + + footer_widget = QtWidgets.QWidget(self) + buttons_widget = QtWidgets.QWidget(footer_widget) + + take_control_btn = QtWidgets.QPushButton( + "Take control", buttons_widget + ) + view_mode_btn = QtWidgets.QPushButton( + "View only", buttons_widget + ) + + buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons_layout.addWidget(take_control_btn, 1) + buttons_layout.addWidget(view_mode_btn, 1) + + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addStretch(1) + footer_layout.addWidget(buttons_widget, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(message_label, 0) + layout.addWidget(separator_widget_1, 0) + layout.addWidget(other_information, 1, QtCore.Qt.AlignHCenter) + layout.addWidget(separator_widget_2, 0) + layout.addWidget(footer_widget, 0) + + take_control_btn.clicked.connect(self._on_take_control) + view_mode_btn.clicked.connect(self._on_view_mode) + + def result(self): + return self._result + + def _on_take_control(self): + self._result = 1 + self.close() + + def _on_view_mode(self): + self._result = 0 + self.close() From 936363a6608aed3bf4bc4b2a2b6977b9f6d03142 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 18:45:54 +0200 Subject: [PATCH 13/23] settings can take go to view mode or take control --- .../tools/settings/settings/categories.py | 19 ++++++++++ openpype/tools/settings/settings/window.py | 38 +++++++++++++++---- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index f42027d9e2..0410fa1810 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -121,6 +121,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.user_role = user_role self.entity = None + self._edit_mode = None self._state = CategoryState.Idle @@ -191,6 +192,21 @@ class SettingsCategoryWidget(QtWidgets.QWidget): ) raise TypeError("Unknown type: {}".format(label)) + def set_edit_mode(self, enabled): + if enabled is self._edit_mode: + return + + self.save_btn.setEnabled(enabled) + if enabled: + tooltip = ( + "Someone else has opened settings UI." + "\nTry hit refresh to check if settings are already available." + ) + else: + tooltip = "Save settings" + + self.save_btn.setToolTip(tooltip) + @property def state(self): return self._state @@ -434,6 +450,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.set_state(CategoryState.Idle) def save(self): + if not self._edit_mode: + return + if not self.items_are_valid(): return diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 96f11f3932..013a273e98 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -36,6 +36,8 @@ class MainWidget(QtWidgets.QWidget): # Object referencing to this machine and time when UI was opened # - is used on close event self._last_opened_info = None + self._edit_mode = None + self._main_reset = False self._user_passed = False self._reset_on_show = reset_on_show @@ -152,6 +154,12 @@ class MainWidget(QtWidgets.QWidget): elif not self._last_opened_info: self._check_on_ui_open() + def closeEvent(self, event): + if self._last_opened_info: + closed_settings_ui(self._last_opened_info) + self._last_opened_info = None + super(MainWidget, self).closeEvent(event) + def _check_on_ui_open(self): last_opened_info = get_last_opened_info() if last_opened_info is not None: @@ -161,19 +169,27 @@ class MainWidget(QtWidgets.QWidget): self._last_opened_info = opened_settings_ui() if self._last_opened_info is not None: + if self._edit_mode is not True: + self._set_edit_mode(True) + return + + if self._edit_mode is False: return dialog = SettingsUIOpenedElsewhere(last_opened_info, self) dialog.exec_() - if dialog.result() == 1: + edit_enabled = dialog.result() == 1 + if edit_enabled: self._last_opened_info = opened_settings_ui() + self._set_edit_mode(edit_enabled) + + def _set_edit_mode(self, mode): + if self._edit_mode is mode: return - def closeEvent(self, event): - if self._last_opened_info: - closed_settings_ui(self._last_opened_info) - self._last_opened_info = None - super(MainWidget, self).closeEvent(event) + self._edit_mode = mode + for tab_widget in self.tab_widgets: + tab_widget.set_edit_mode(mode) def _show_password_dialog(self): if self._password_dialog: @@ -215,8 +231,11 @@ class MainWidget(QtWidgets.QWidget): if self._reset_on_show: self._reset_on_show = False + self._main_reset = True for tab_widget in self.tab_widgets: tab_widget.reset() + self._main_reset = False + self._check_on_ui_open() def _update_search_dialog(self, clear=False): if self._search_dialog.isVisible(): @@ -260,7 +279,8 @@ class MainWidget(QtWidgets.QWidget): if current_widget is widget: self._update_search_dialog() - self._check_on_ui_open() + if not self._main_reset: + self._check_on_ui_open() def keyPressEvent(self, event): if event.matches(QtGui.QKeySequence.Find): @@ -340,7 +360,9 @@ class SettingsUIOpenedElsewhere(QtWidgets.QDialog): layout = QtWidgets.QVBoxLayout(self) layout.addWidget(message_label, 0) layout.addWidget(separator_widget_1, 0) - layout.addWidget(other_information, 1, QtCore.Qt.AlignHCenter) + layout.addStretch(1) + layout.addWidget(other_information, 0, QtCore.Qt.AlignHCenter) + layout.addStretch(1) layout.addWidget(separator_widget_2, 0) layout.addWidget(footer_widget, 0) From 407b6b518e0da63e8efd021952c01b6311cf0640 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Aug 2022 12:05:38 +0200 Subject: [PATCH 14/23] use controlled to handle last opened info --- .../tools/settings/settings/categories.py | 23 ++- openpype/tools/settings/settings/window.py | 136 ++++++++++++------ 2 files changed, 114 insertions(+), 45 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 0410fa1810..2e3c6d9dda 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -115,13 +115,19 @@ class SettingsCategoryWidget(QtWidgets.QWidget): "settings to update them to you current running OpenPype version." ) - def __init__(self, user_role, parent=None): + def __init__(self, controller, parent=None): super(SettingsCategoryWidget, self).__init__(parent) - self.user_role = user_role + self._controller = controller + controller.event_system.add_callback( + "edit.mode.changed", + self._edit_mode_changed + ) self.entity = None self._edit_mode = None + self._last_saved_info = None + self._reset_crashed = False self._state = CategoryState.Idle @@ -192,11 +198,16 @@ class SettingsCategoryWidget(QtWidgets.QWidget): ) raise TypeError("Unknown type: {}".format(label)) + def _edit_mode_changed(self, event): + self.set_edit_mode(event["edit_mode"]) + def set_edit_mode(self, enabled): if enabled is self._edit_mode: return - self.save_btn.setEnabled(enabled) + self._edit_mode = enabled + + self.save_btn.setEnabled(enabled and not self._reset_crashed) if enabled: tooltip = ( "Someone else has opened settings UI." @@ -302,7 +313,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): footer_layout = QtWidgets.QHBoxLayout(footer_widget) footer_layout.setContentsMargins(5, 5, 5, 5) - if self.user_role == "developer": + if self._controller.user_role == "developer": self._add_developer_ui(footer_layout, footer_widget) footer_layout.addWidget(empty_label, 1) @@ -683,14 +694,16 @@ class SettingsCategoryWidget(QtWidgets.QWidget): ) def _on_reset_crash(self): + self._reset_crashed = True self.save_btn.setEnabled(False) if self.breadcrumbs_model is not None: self.breadcrumbs_model.set_entity(None) def _on_reset_success(self): + self._reset_crashed = True if not self.save_btn.isEnabled(): - self.save_btn.setEnabled(True) + self.save_btn.setEnabled(self._edit_mode) if self.breadcrumbs_model is not None: path = self.breadcrumbs_bar.path() diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 013a273e98..612975e30a 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -3,6 +3,7 @@ from Qt import QtWidgets, QtGui, QtCore from openpype import style from openpype.lib import is_admin_password_required +from openpype.lib.events import EventSystem from openpype.widgets import PasswordDialog from openpype.settings.lib import ( @@ -24,6 +25,81 @@ from .widgets import ( from .search_dialog import SearchEntitiesDialog +class SettingsController: + """Controller for settings tools. + + Added when tool was finished for checks of last opened in settings + categories and being able communicated with main widget logic. + """ + + def __init__(self, user_role): + self._user_role = user_role + self._event_system = EventSystem() + + self._opened_info = None + self._last_opened_info = None + self._edit_mode = None + + @property + def user_role(self): + return self._user_role + + @property + def event_system(self): + return self._event_system + + @property + def opened_info(self): + return self._opened_info + + @property + def last_opened_info(self): + return self._last_opened_info + + @property + def edit_mode(self): + return self._edit_mode + + def ui_closed(self): + if self._opened_info is not None: + closed_settings_ui(self._opened_info) + + self._opened_info = None + self._edit_mode = None + + def set_edit_mode(self, enabled): + if self._edit_mode is enabled: + return + + opened_info = None + if enabled: + opened_info = opened_settings_ui() + self._last_opened_info = opened_info + + self._opened_info = opened_info + self._edit_mode = enabled + + self.event_system.emit( + "edit.mode.changed", + {"edit_mode": enabled}, + "controller" + ) + + def update_last_opened_info(self): + print("update_last_opened_info") + last_opened_info = get_last_opened_info() + enabled = False + if ( + last_opened_info is None + or self._opened_info == last_opened_info + ): + enabled = True + + self._last_opened_info = last_opened_info + + self.set_edit_mode(enabled) + + class MainWidget(QtWidgets.QWidget): trigger_restart = QtCore.Signal() @@ -33,11 +109,12 @@ class MainWidget(QtWidgets.QWidget): def __init__(self, user_role, parent=None, reset_on_show=True): super(MainWidget, self).__init__(parent) + controller = SettingsController(user_role) + # Object referencing to this machine and time when UI was opened # - is used on close event - self._last_opened_info = None - self._edit_mode = None self._main_reset = False + self._controller = controller self._user_passed = False self._reset_on_show = reset_on_show @@ -55,8 +132,8 @@ class MainWidget(QtWidgets.QWidget): header_tab_widget = SettingsTabWidget(parent=self) - studio_widget = SystemWidget(user_role, header_tab_widget) - project_widget = ProjectWidget(user_role, header_tab_widget) + studio_widget = SystemWidget(controller, header_tab_widget) + project_widget = ProjectWidget(controller, header_tab_widget) tab_widgets = [ studio_widget, @@ -151,45 +228,24 @@ class MainWidget(QtWidgets.QWidget): # Trigger reset with 100ms delay QtCore.QTimer.singleShot(100, self.reset) - elif not self._last_opened_info: - self._check_on_ui_open() - def closeEvent(self, event): - if self._last_opened_info: - closed_settings_ui(self._last_opened_info) - self._last_opened_info = None + self._controller.ui_closed() + super(MainWidget, self).closeEvent(event) - def _check_on_ui_open(self): - last_opened_info = get_last_opened_info() - if last_opened_info is not None: - if self._last_opened_info != last_opened_info: - self._last_opened_info = None - else: - self._last_opened_info = opened_settings_ui() - - if self._last_opened_info is not None: - if self._edit_mode is not True: - self._set_edit_mode(True) + def _check_on_reset(self): + self._controller.update_last_opened_info() + if self._controller.edit_mode: return - if self._edit_mode is False: - return + # if self._edit_mode is False: + # return - dialog = SettingsUIOpenedElsewhere(last_opened_info, self) + dialog = SettingsUIOpenedElsewhere( + self._controller.last_opened_info, self + ) dialog.exec_() - edit_enabled = dialog.result() == 1 - if edit_enabled: - self._last_opened_info = opened_settings_ui() - self._set_edit_mode(edit_enabled) - - def _set_edit_mode(self, mode): - if self._edit_mode is mode: - return - - self._edit_mode = mode - for tab_widget in self.tab_widgets: - tab_widget.set_edit_mode(mode) + self._controller.set_edit_mode(dialog.result() == 1) def _show_password_dialog(self): if self._password_dialog: @@ -235,7 +291,7 @@ class MainWidget(QtWidgets.QWidget): for tab_widget in self.tab_widgets: tab_widget.reset() self._main_reset = False - self._check_on_ui_open() + self._check_on_reset() def _update_search_dialog(self, clear=False): if self._search_dialog.isVisible(): @@ -280,7 +336,7 @@ class MainWidget(QtWidgets.QWidget): self._update_search_dialog() if not self._main_reset: - self._check_on_ui_open() + self._check_on_reset() def keyPressEvent(self, event): if event.matches(QtGui.QKeySequence.Find): @@ -306,8 +362,8 @@ class SettingsUIOpenedElsewhere(QtWidgets.QDialog): "Someone else has opened Settings UI. That may cause data loss." " Please contact the person on the other side." "

You can open the UI in view-only mode or take" - " the control which will cause the other settings won't be able" - " to save changes.
" + " the control which will cause settings on the other side" + " won't be able to save changes.
" ), self) message_label.setWordWrap(True) From 4f03f2dd09f1eed5a0f23d2d9c9a428ae7656560 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Aug 2022 12:33:32 +0200 Subject: [PATCH 15/23] modified dialog --- openpype/tools/settings/settings/window.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 612975e30a..fcbcd129d0 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -359,11 +359,12 @@ class SettingsUIOpenedElsewhere(QtWidgets.QDialog): self.setWindowTitle("Someone else has opened Settings UI") message_label = QtWidgets.QLabel(( - "Someone else has opened Settings UI. That may cause data loss." + "Someone else has opened Settings UI which could cause data loss." " Please contact the person on the other side." - "

You can open the UI in view-only mode or take" - " the control which will cause settings on the other side" - " won't be able to save changes.
" + "

You can open the UI in view-only mode." + " All changes in view mode will be lost." + "

You can take the control which will cause that" + " all changes of settings on the other side will be lost.
" ), self) message_label.setWordWrap(True) @@ -435,3 +436,7 @@ class SettingsUIOpenedElsewhere(QtWidgets.QDialog): def _on_view_mode(self): self._result = 0 self.close() + + def showEvent(self, event): + super(SettingsUIOpenedElsewhere, self).showEvent(event) + self.resize(600, 400) From 5d14fdd1cef6eb2d40925efeadfbcab3af219ca5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Aug 2022 19:02:10 +0200 Subject: [PATCH 16/23] fix _reset_crashed --- openpype/tools/settings/settings/categories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 2e3c6d9dda..fd95b4ca71 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -701,7 +701,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.breadcrumbs_model.set_entity(None) def _on_reset_success(self): - self._reset_crashed = True + self._reset_crashed = False if not self.save_btn.isEnabled(): self.save_btn.setEnabled(self._edit_mode) From 890d1becaa8a4fcc597977d6b0cbe25e21bf34d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Aug 2022 19:11:00 +0200 Subject: [PATCH 17/23] moved dialog to separated file --- openpype/tools/settings/settings/dialogs.py | 115 ++++++++++++++++++++ openpype/tools/settings/settings/window.py | 93 +--------------- 2 files changed, 116 insertions(+), 92 deletions(-) create mode 100644 openpype/tools/settings/settings/dialogs.py diff --git a/openpype/tools/settings/settings/dialogs.py b/openpype/tools/settings/settings/dialogs.py new file mode 100644 index 0000000000..dea056b89d --- /dev/null +++ b/openpype/tools/settings/settings/dialogs.py @@ -0,0 +1,115 @@ +from Qt import QtWidgets, QtCore + + +class BaseInfoDialog(QtWidgets.QDialog): + width = 600 + height = 400 + + def __init__(self, message, title, info_obj, parent=None): + super(BaseInfoDialog, self).__init__(parent) + self._result = 0 + self._info_obj = info_obj + + self.setWindowTitle(title) + + message_label = QtWidgets.QLabel(message, self) + message_label.setWordWrap(True) + + separator_widget_1 = QtWidgets.QFrame(self) + separator_widget_2 = QtWidgets.QFrame(self) + for separator_widget in ( + separator_widget_1, + separator_widget_2 + ): + separator_widget.setObjectName("Separator") + separator_widget.setMinimumHeight(1) + separator_widget.setMaximumHeight(1) + + other_information = QtWidgets.QWidget(self) + other_information_layout = QtWidgets.QFormLayout(other_information) + other_information_layout.setContentsMargins(0, 0, 0, 0) + for label, value in ( + ("Username", info_obj.username), + ("Host name", info_obj.hostname), + ("Host IP", info_obj.hostip), + ("System name", info_obj.system_name), + ("Local ID", info_obj.local_id), + ("Time Stamp", info_obj.timestamp), + ): + other_information_layout.addRow( + label, + QtWidgets.QLabel(value, other_information) + ) + + footer_widget = QtWidgets.QWidget(self) + buttons_widget = QtWidgets.QWidget(footer_widget) + + buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons = self.get_buttons(buttons_widget) + for button in buttons: + buttons_layout.addWidget(button, 1) + + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addStretch(1) + footer_layout.addWidget(buttons_widget, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(message_label, 0) + layout.addWidget(separator_widget_1, 0) + layout.addStretch(1) + layout.addWidget(other_information, 0, QtCore.Qt.AlignHCenter) + layout.addStretch(1) + layout.addWidget(separator_widget_2, 0) + layout.addWidget(footer_widget, 0) + + def showEvent(self, event): + super(BaseInfoDialog, self).showEvent(event) + self.resize(self.width, self.height) + + def result(self): + return self._result + + def get_buttons(self, parent): + return [] + + +class SettingsUIOpenedElsewhere(BaseInfoDialog): + def __init__(self, info_obj, parent=None): + title = "Someone else has opened Settings UI" + message = ( + "Someone else has opened Settings UI which could cause data loss." + " Please contact the person on the other side." + "

You can continue in view-only mode." + " All changes in view mode will be lost." + "

You can take control which will cause that" + " all changes of settings on the other side will be lost.
" + ) + super(SettingsUIOpenedElsewhere, self).__init__( + message, title, info_obj, parent + ) + + def _on_take_control(self): + self._result = 1 + self.close() + + def _on_view_mode(self): + self._result = 0 + self.close() + + def get_buttons(self, parent): + take_control_btn = QtWidgets.QPushButton( + "Take control", parent + ) + view_mode_btn = QtWidgets.QPushButton( + "View only", parent + ) + + take_control_btn.clicked.connect(self._on_take_control) + view_mode_btn.clicked.connect(self._on_view_mode) + + return [ + take_control_btn, + view_mode_btn + ] diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index fcbcd129d0..2750785535 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -12,6 +12,7 @@ from openpype.settings.lib import ( closed_settings_ui, ) +from .dialogs import SettingsUIOpenedElsewhere from .categories import ( CategoryState, SystemWidget, @@ -348,95 +349,3 @@ class MainWidget(QtWidgets.QWidget): return return super(MainWidget, self).keyPressEvent(event) - - -class SettingsUIOpenedElsewhere(QtWidgets.QDialog): - def __init__(self, info_obj, parent=None): - super(SettingsUIOpenedElsewhere, self).__init__(parent) - - self._result = 0 - - self.setWindowTitle("Someone else has opened Settings UI") - - message_label = QtWidgets.QLabel(( - "Someone else has opened Settings UI which could cause data loss." - " Please contact the person on the other side." - "

You can open the UI in view-only mode." - " All changes in view mode will be lost." - "

You can take the control which will cause that" - " all changes of settings on the other side will be lost.
" - ), self) - message_label.setWordWrap(True) - - separator_widget_1 = QtWidgets.QFrame(self) - separator_widget_2 = QtWidgets.QFrame(self) - for separator_widget in ( - separator_widget_1, - separator_widget_2 - ): - separator_widget.setObjectName("Separator") - separator_widget.setMinimumHeight(1) - separator_widget.setMaximumHeight(1) - - other_information = QtWidgets.QWidget(self) - other_information_layout = QtWidgets.QFormLayout(other_information) - other_information_layout.setContentsMargins(0, 0, 0, 0) - for label, value in ( - ("Username", info_obj.username), - ("Host name", info_obj.hostname), - ("Host IP", info_obj.hostip), - ("System name", info_obj.system_name), - ("Local ID", info_obj.local_id), - ("Time Stamp", info_obj.timestamp), - ): - other_information_layout.addRow( - label, - QtWidgets.QLabel(value, other_information) - ) - - footer_widget = QtWidgets.QWidget(self) - buttons_widget = QtWidgets.QWidget(footer_widget) - - take_control_btn = QtWidgets.QPushButton( - "Take control", buttons_widget - ) - view_mode_btn = QtWidgets.QPushButton( - "View only", buttons_widget - ) - - buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) - buttons_layout.setContentsMargins(0, 0, 0, 0) - buttons_layout.addWidget(take_control_btn, 1) - buttons_layout.addWidget(view_mode_btn, 1) - - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - footer_layout.setContentsMargins(0, 0, 0, 0) - footer_layout.addStretch(1) - footer_layout.addWidget(buttons_widget, 0) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(message_label, 0) - layout.addWidget(separator_widget_1, 0) - layout.addStretch(1) - layout.addWidget(other_information, 0, QtCore.Qt.AlignHCenter) - layout.addStretch(1) - layout.addWidget(separator_widget_2, 0) - layout.addWidget(footer_widget, 0) - - take_control_btn.clicked.connect(self._on_take_control) - view_mode_btn.clicked.connect(self._on_view_mode) - - def result(self): - return self._result - - def _on_take_control(self): - self._result = 1 - self.close() - - def _on_view_mode(self): - self._result = 0 - self.close() - - def showEvent(self, event): - super(SettingsUIOpenedElsewhere, self).showEvent(event) - self.resize(600, 400) From f4106714aeabd13eec6d8085cb3f3be39feb4f00 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 11:27:19 +0200 Subject: [PATCH 18/23] added dialogs for 2 other cases --- openpype/tools/settings/settings/dialogs.py | 64 +++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/openpype/tools/settings/settings/dialogs.py b/openpype/tools/settings/settings/dialogs.py index dea056b89d..a3eed68ae3 100644 --- a/openpype/tools/settings/settings/dialogs.py +++ b/openpype/tools/settings/settings/dialogs.py @@ -113,3 +113,67 @@ class SettingsUIOpenedElsewhere(BaseInfoDialog): take_control_btn, view_mode_btn ] + + +class SettingsLastSavedChanged(BaseInfoDialog): + width = 500 + height = 300 + + def __init__(self, info_obj, parent=None): + title = "Settings has changed" + message = ( + "Settings has changed while you had opened this settings session." + "

It is recommended to refresh settings" + " and re-apply changes in the new session." + ) + super(SettingsLastSavedChanged, self).__init__( + message, title, info_obj, parent + ) + + def _on_save(self): + self._result = 1 + self.close() + + def _on_close(self): + self._result = 0 + self.close() + + def get_buttons(self, parent): + close_btn = QtWidgets.QPushButton( + "Close", parent + ) + save_btn = QtWidgets.QPushButton( + "Save anyway", parent + ) + + close_btn.clicked.connect(self._on_close) + save_btn.clicked.connect(self._on_save) + + return [ + close_btn, + save_btn + ] + + +class SettingsControlTaken(BaseInfoDialog): + width = 500 + height = 300 + + def __init__(self, info_obj, parent=None): + title = "Settings control taken" + message = ( + "Someone took control over your settings." + "

It is not possible to save changes of currently" + " opened session. Copy changes you want to keep and hit refresh." + ) + super(SettingsControlTaken, self).__init__( + message, title, info_obj, parent + ) + + def _on_confirm(self): + self.close() + + def get_buttons(self, parent): + confirm_btn = QtWidgets.QPushButton("Understand", parent) + confirm_btn.clicked.connect(self._on_confirm) + return [confirm_btn] From 537bfaa8d683213f03a78b962d1767698947c390 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 11:27:45 +0200 Subject: [PATCH 19/23] removed print --- openpype/tools/settings/settings/window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 2750785535..a907a034d1 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -87,7 +87,6 @@ class SettingsController: ) def update_last_opened_info(self): - print("update_last_opened_info") last_opened_info = get_last_opened_info() enabled = False if ( From 80b5c0c064c47d4bab6e870759a2fc4caf54cfd9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 11:28:00 +0200 Subject: [PATCH 20/23] categories have checks related to last saved settings --- .../tools/settings/settings/categories.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index fd95b4ca71..f4b2c13a12 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -36,6 +36,11 @@ from openpype.settings.entities.op_version_entity import ( ) from openpype.settings import SaveWarningExc +from openpype.settings.lib import ( + get_system_last_saved_info, + get_project_last_saved_info, +) +from .dialogs import SettingsLastSavedChanged, SettingsControlTaken from .widgets import ( ProjectListWidget, VersionAction @@ -205,6 +210,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): if enabled is self._edit_mode: return + was_false = self._edit_mode is False self._edit_mode = enabled self.save_btn.setEnabled(enabled and not self._reset_crashed) @@ -218,6 +224,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.save_btn.setToolTip(tooltip) + # Reset when last saved information has changed + if was_false and not self._check_last_saved_info(): + self.reset() + @property def state(self): return self._state @@ -748,7 +758,24 @@ class SettingsCategoryWidget(QtWidgets.QWidget): """Callback on any tab widget save.""" return + def _check_last_saved_info(self): + raise NotImplementedError(( + "{} does not have implemented '_check_last_saved_info'" + ).format(self.__class__.__name__)) + def _save(self): + self._controller.update_last_opened_info() + if not self._controller.opened_info: + dialog = SettingsControlTaken(self._last_saved_info, self) + dialog.exec_() + return + + if not self._check_last_saved_info(): + dialog = SettingsLastSavedChanged(self._last_saved_info, self) + dialog.exec_() + if dialog.result() == 0: + return + # Don't trigger restart if defaults are modified if self.is_modifying_defaults: require_restart = False @@ -807,6 +834,13 @@ class SystemWidget(SettingsCategoryWidget): self._actions = [] super(SystemWidget, self).__init__(*args, **kwargs) + def _check_last_saved_info(self): + if self.is_modifying_defaults: + return True + + last_saved_info = get_system_last_saved_info() + return self._last_saved_info == last_saved_info + def contain_category_key(self, category): if category == "system_settings": return True @@ -821,6 +855,10 @@ class SystemWidget(SettingsCategoryWidget): ) entity.on_change_callbacks.append(self._on_entity_change) self.entity = entity + last_saved_info = None + if not self.is_modifying_defaults: + last_saved_info = get_system_last_saved_info() + self._last_saved_info = last_saved_info try: if self.is_modifying_defaults: entity.set_defaults_state() @@ -854,6 +892,13 @@ class ProjectWidget(SettingsCategoryWidget): def __init__(self, *args, **kwargs): super(ProjectWidget, self).__init__(*args, **kwargs) + def _check_last_saved_info(self): + if self.is_modifying_defaults: + return True + + last_saved_info = get_project_last_saved_info(self.project_name) + return self._last_saved_info == last_saved_info + def contain_category_key(self, category): if category in ("project_settings", "project_anatomy"): return True @@ -933,6 +978,11 @@ class ProjectWidget(SettingsCategoryWidget): entity.on_change_callbacks.append(self._on_entity_change) self.project_list_widget.set_entity(entity) self.entity = entity + + last_saved_info = None + if not self.is_modifying_defaults: + last_saved_info = get_project_last_saved_info(self.project_name) + self._last_saved_info = last_saved_info try: if self.is_modifying_defaults: self.entity.set_defaults_state() From 7cd6a423ead42bae21a2fa2e491bf32870402fe8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 11:55:04 +0200 Subject: [PATCH 21/23] fix attribute name --- openpype/settings/handlers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 1b59531943..09f36aa16e 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -784,9 +784,9 @@ class MongoSettingsHandler(SettingsHandler): "last_saved_info": last_saved_info.to_document_data() } if not system_settings_doc: - self.collections.insert_one(new_system_settings_doc) + self.collection.insert_one(new_system_settings_doc) else: - self.collections.update_one( + self.collection.update_one( {"_id": system_settings_doc["_id"]}, {"$set": new_system_settings_doc} ) From 9b60b9faa89c8551c88977c42ad9f404869c7680 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 15:19:26 +0200 Subject: [PATCH 22/23] change title if in view mode --- openpype/tools/settings/settings/window.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index a907a034d1..77a2f64dac 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -105,6 +105,7 @@ class MainWidget(QtWidgets.QWidget): widget_width = 1000 widget_height = 600 + window_title = "OpenPype Settings" def __init__(self, user_role, parent=None, reset_on_show=True): super(MainWidget, self).__init__(parent) @@ -122,7 +123,7 @@ class MainWidget(QtWidgets.QWidget): self._password_dialog = None self.setObjectName("SettingsMainWidget") - self.setWindowTitle("OpenPype Settings") + self.setWindowTitle(self.window_title) self.resize(self.widget_width, self.widget_height) @@ -155,6 +156,11 @@ class MainWidget(QtWidgets.QWidget): self._shadow_widget = ShadowWidget("Working...", self) self._shadow_widget.setVisible(False) + controller.event_system.add_callback( + "edit.mode.changed", + self._edit_mode_changed + ) + header_tab_widget.currentChanged.connect(self._on_tab_changed) search_dialog.path_clicked.connect(self._on_search_path_clicked) @@ -301,6 +307,12 @@ class MainWidget(QtWidgets.QWidget): entity = widget.entity self._search_dialog.set_root_entity(entity) + def _edit_mode_changed(self, event): + title = self.window_title + if not event["edit_mode"]: + title += " [View only]" + self.setWindowTitle(title) + def _on_tab_changed(self): self._update_search_dialog() From 1b64160644a1af0a8fd4349814a1c5e3fe496b85 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 15:21:48 +0200 Subject: [PATCH 23/23] use pretty time instead of timestamp --- openpype/tools/settings/settings/dialogs.py | 25 ++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dialogs.py b/openpype/tools/settings/settings/dialogs.py index a3eed68ae3..f25374a48c 100644 --- a/openpype/tools/settings/settings/dialogs.py +++ b/openpype/tools/settings/settings/dialogs.py @@ -1,5 +1,7 @@ from Qt import QtWidgets, QtCore +from openpype.tools.utils.delegates import pretty_date + class BaseInfoDialog(QtWidgets.QDialog): width = 600 @@ -34,13 +36,17 @@ class BaseInfoDialog(QtWidgets.QDialog): ("Host IP", info_obj.hostip), ("System name", info_obj.system_name), ("Local ID", info_obj.local_id), - ("Time Stamp", info_obj.timestamp), ): other_information_layout.addRow( label, QtWidgets.QLabel(value, other_information) ) + timestamp_label = QtWidgets.QLabel( + pretty_date(info_obj.timestamp_obj), other_information + ) + other_information_layout.addRow("Time", timestamp_label) + footer_widget = QtWidgets.QWidget(self) buttons_widget = QtWidgets.QWidget(footer_widget) @@ -64,10 +70,27 @@ class BaseInfoDialog(QtWidgets.QDialog): layout.addWidget(separator_widget_2, 0) layout.addWidget(footer_widget, 0) + timestamp_timer = QtCore.QTimer() + timestamp_timer.setInterval(1000) + timestamp_timer.timeout.connect(self._on_timestamp_timer) + + self._timestamp_label = timestamp_label + self._timestamp_timer = timestamp_timer + def showEvent(self, event): super(BaseInfoDialog, self).showEvent(event) + self._timestamp_timer.start() self.resize(self.width, self.height) + def closeEvent(self, event): + self._timestamp_timer.stop() + super(BaseInfoDialog, self).closeEvent(event) + + def _on_timestamp_timer(self): + self._timestamp_label.setText( + pretty_date(self._info_obj.timestamp_obj) + ) + def result(self): return self._result