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

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