mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge pull request #3700 from pypeclub/feature/OP-2992_Settings-lock
Settings: Lock settings UI session
This commit is contained in:
commit
ca9a3483ff
5 changed files with 872 additions and 62 deletions
|
|
@ -22,6 +22,164 @@ from .constants import (
|
|||
)
|
||||
|
||||
|
||||
class SettingsStateInfo:
|
||||
"""Helper state information about some settings state.
|
||||
|
||||
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 and on which openpype version.
|
||||
|
||||
To create currrent machine and time information use 'create_new' method.
|
||||
"""
|
||||
|
||||
timestamp_format = "%Y-%m-%d %H:%M:%S.%f"
|
||||
|
||||
def __init__(
|
||||
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 = timestamp_obj
|
||||
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())
|
||||
|
||||
@classmethod
|
||||
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
|
||||
|
||||
now = datetime.datetime.now()
|
||||
workstation_info = get_workstation_info()
|
||||
|
||||
return cls(
|
||||
openpype_version,
|
||||
settings_type,
|
||||
project_name,
|
||||
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["openpype_version"],
|
||||
data["settings_type"],
|
||||
data["project_name"],
|
||||
data["timestamp"],
|
||||
data["hostname"],
|
||||
data["hostip"],
|
||||
data["username"],
|
||||
data["system_name"],
|
||||
data["local_id"]
|
||||
)
|
||||
|
||||
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 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 {}
|
||||
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,
|
||||
"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.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
|
||||
and self.local_id == other.local_id
|
||||
)
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class SettingsHandler:
|
||||
@abstractmethod
|
||||
|
|
@ -226,7 +384,7 @@ class SettingsHandler:
|
|||
"""OpenPype versions that have any studio project anatomy overrides.
|
||||
|
||||
Returns:
|
||||
list<str>: OpenPype versions strings.
|
||||
List[str]: OpenPype versions strings.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
@ -237,7 +395,7 @@ class SettingsHandler:
|
|||
"""OpenPype versions that have any studio project settings overrides.
|
||||
|
||||
Returns:
|
||||
list<str>: OpenPype versions strings.
|
||||
List[str]: OpenPype versions strings.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
@ -251,8 +409,87 @@ class SettingsHandler:
|
|||
project_name(str): Name of project.
|
||||
|
||||
Returns:
|
||||
list<str>: 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
|
||||
|
||||
# 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_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_settings_ui' is called again.
|
||||
|
||||
Returns:
|
||||
SettingsStateInfo: Object representing information about this
|
||||
machine. Must be passed to 'closed_settings_ui' when finished.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
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
|
||||
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_settings_ui' was called.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -285,19 +522,22 @@ class CacheValues:
|
|||
self.data = None
|
||||
self.creation_time = None
|
||||
self.version = None
|
||||
self.last_saved_info = None
|
||||
|
||||
def data_copy(self):
|
||||
if not self.data:
|
||||
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_last_saved_info(self, last_saved_info):
|
||||
self.last_saved_info = last_saved_info
|
||||
|
||||
def update_from_document(self, document, version):
|
||||
data = {}
|
||||
if document:
|
||||
if "data" in document:
|
||||
|
|
@ -306,9 +546,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 {})
|
||||
|
|
@ -320,6 +560,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."""
|
||||
|
|
@ -509,6 +752,14 @@ 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
|
||||
)
|
||||
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()
|
||||
|
||||
|
|
@ -517,20 +768,29 @@ 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,
|
||||
"last_saved_info": last_saved_info.to_document_data()
|
||||
}
|
||||
if not system_settings_doc:
|
||||
self.collection.insert_one(new_system_settings_doc)
|
||||
else:
|
||||
self.collection.update_one(
|
||||
{"_id": system_settings_doc["_id"]},
|
||||
{"$set": new_system_settings_doc}
|
||||
)
|
||||
|
||||
# Store global settings
|
||||
self.collection.replace_one(
|
||||
{
|
||||
|
|
@ -562,6 +822,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
|
||||
)
|
||||
|
|
@ -665,26 +933,34 @@ 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,
|
||||
"last_saved_info": last_saved_info.to_data()
|
||||
}
|
||||
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
|
||||
|
|
@ -1011,19 +1287,11 @@ class MongoSettingsHandler(SettingsHandler):
|
|||
globals_document = self.collection.find_one({
|
||||
"type": GLOBAL_SETTINGS_KEY
|
||||
})
|
||||
document = (
|
||||
self._get_studio_system_settings_overrides_for_version()
|
||||
document, version = self._get_system_settings_overrides_doc()
|
||||
|
||||
last_saved_info = SettingsStateInfo.from_document(
|
||||
version, SYSTEM_SETTINGS_KEY, document
|
||||
)
|
||||
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(
|
||||
document, globals_document
|
||||
)
|
||||
|
|
@ -1031,6 +1299,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()
|
||||
|
|
@ -1038,24 +1309,43 @@ 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_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 = 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
|
||||
)
|
||||
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()
|
||||
|
|
@ -1063,6 +1353,29 @@ 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_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)
|
||||
|
|
@ -1140,6 +1453,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(
|
||||
|
|
@ -1359,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 None
|
||||
|
||||
# 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_settings_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_settings_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.
|
||||
|
|
@ -1405,7 +1777,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(
|
||||
{
|
||||
|
|
@ -1428,6 +1800,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()
|
||||
|
|
|
|||
|
|
@ -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_settings_ui():
|
||||
return _SETTINGS_HANDLER.opened_settings_ui()
|
||||
|
||||
|
||||
@require_handler
|
||||
def closed_settings_ui(info_obj):
|
||||
return _SETTINGS_HANDLER.closed_settings_ui(info_obj)
|
||||
|
||||
|
||||
@require_handler
|
||||
def save_studio_settings(data):
|
||||
"""Save studio overrides of system settings.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -115,12 +120,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
|
||||
|
||||
|
|
@ -191,6 +203,31 @@ 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
|
||||
|
||||
was_false = self._edit_mode is False
|
||||
self._edit_mode = enabled
|
||||
|
||||
self.save_btn.setEnabled(enabled and not self._reset_crashed)
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
|
@ -286,7 +323,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)
|
||||
|
|
@ -434,6 +471,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
|
||||
|
||||
|
|
@ -664,14 +704,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 = False
|
||||
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()
|
||||
|
|
@ -716,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
|
||||
|
|
@ -775,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
|
||||
|
|
@ -789,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()
|
||||
|
|
@ -822,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
|
||||
|
|
@ -901,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()
|
||||
|
|
|
|||
202
openpype/tools/settings/settings/dialogs.py
Normal file
202
openpype/tools/settings/settings/dialogs.py
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
from Qt import QtWidgets, QtCore
|
||||
|
||||
from openpype.tools.utils.delegates import pretty_date
|
||||
|
||||
|
||||
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),
|
||||
):
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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."
|
||||
"<br/><br/>You can continue in <b>view-only mode</b>."
|
||||
" All changes in view mode will be lost."
|
||||
"<br/><br/>You can <b>take control</b> which will cause that"
|
||||
" all changes of settings on the other side will be lost.<br/>"
|
||||
)
|
||||
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
|
||||
]
|
||||
|
||||
|
||||
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."
|
||||
"<br/><br/>It is <b>recommended to refresh settings</b>"
|
||||
" 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."
|
||||
"<br/><br/>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]
|
||||
|
|
@ -1,4 +1,18 @@
|
|||
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 (
|
||||
get_last_opened_info,
|
||||
opened_settings_ui,
|
||||
closed_settings_ui,
|
||||
)
|
||||
|
||||
from .dialogs import SettingsUIOpenedElsewhere
|
||||
from .categories import (
|
||||
CategoryState,
|
||||
SystemWidget,
|
||||
|
|
@ -10,10 +24,80 @@ 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 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):
|
||||
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):
|
||||
|
|
@ -21,17 +105,25 @@ 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)
|
||||
|
||||
controller = SettingsController(user_role)
|
||||
|
||||
# Object referencing to this machine and time when UI was opened
|
||||
# - is used on close event
|
||||
self._main_reset = False
|
||||
self._controller = controller
|
||||
|
||||
self._user_passed = False
|
||||
self._reset_on_show = reset_on_show
|
||||
|
||||
self._password_dialog = None
|
||||
|
||||
self.setObjectName("SettingsMainWidget")
|
||||
self.setWindowTitle("OpenPype Settings")
|
||||
self.setWindowTitle(self.window_title)
|
||||
|
||||
self.resize(self.widget_width, self.widget_height)
|
||||
|
||||
|
|
@ -41,8 +133,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,
|
||||
|
|
@ -64,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)
|
||||
|
||||
|
|
@ -74,7 +171,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 +228,31 @@ 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)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._controller.ui_closed()
|
||||
|
||||
super(MainWidget, self).closeEvent(event)
|
||||
|
||||
def _check_on_reset(self):
|
||||
self._controller.update_last_opened_info()
|
||||
if self._controller.edit_mode:
|
||||
return
|
||||
|
||||
# if self._edit_mode is False:
|
||||
# return
|
||||
|
||||
dialog = SettingsUIOpenedElsewhere(
|
||||
self._controller.last_opened_info, self
|
||||
)
|
||||
dialog.exec_()
|
||||
self._controller.set_edit_mode(dialog.result() == 1)
|
||||
|
||||
def _show_password_dialog(self):
|
||||
if self._password_dialog:
|
||||
self._password_dialog.open()
|
||||
|
|
@ -176,8 +293,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_reset()
|
||||
|
||||
def _update_search_dialog(self, clear=False):
|
||||
if self._search_dialog.isVisible():
|
||||
|
|
@ -187,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()
|
||||
|
||||
|
|
@ -221,6 +347,9 @@ class MainWidget(QtWidgets.QWidget):
|
|||
if current_widget is widget:
|
||||
self._update_search_dialog()
|
||||
|
||||
if not self._main_reset:
|
||||
self._check_on_reset()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.matches(QtGui.QKeySequence.Find):
|
||||
# todo: search in all widgets (or in active)?
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue