Merge pull request #3700 from pypeclub/feature/OP-2992_Settings-lock

Settings: Lock settings UI session
This commit is contained in:
Jakub Trllo 2022-08-22 16:39:40 +02:00 committed by GitHub
commit ca9a3483ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 872 additions and 62 deletions

View file

@ -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()

View file

@ -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.

View file

@ -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()

View 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]

View file

@ -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)?