diff --git a/openpype/modules/base.py b/openpype/modules/base.py index b8d76aa028..50bd6411ce 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -680,6 +680,10 @@ class TrayModulesManager(ModulesManager): output.append(module) return output + def restart_tray(self): + if self.tray_manager: + self.tray_manager.restart() + def tray_init(self): report = {} time_start = time.time() diff --git a/openpype/modules/settings_action.py b/openpype/modules/settings_action.py index 5651868f68..f5bcb5342d 100644 --- a/openpype/modules/settings_action.py +++ b/openpype/modules/settings_action.py @@ -67,6 +67,10 @@ class SettingsAction(PypeModule, ITrayAction): return from openpype.tools.settings import MainWidget self.settings_window = MainWidget(self.user_role) + self.settings_window.trigger_restart.connect(self._on_trigger_restart) + + def _on_trigger_restart(self): + self.manager.restart_tray() def show_settings_window(self): """Show settings tool window. diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 90efb73fbc..c6bff1ff47 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -111,6 +111,8 @@ class BaseItemEntity(BaseEntity): self.file_item = None # Reference to `RootEntity` self.root_item = None + # Change of value requires restart of OpenPype + self._require_restart_on_change = False # Entity is in hierarchy of dynamically created entity self.is_in_dynamic_item = False @@ -171,6 +173,14 @@ class BaseItemEntity(BaseEntity): roles = [roles] self.roles = roles + @property + def require_restart_on_change(self): + return self._require_restart_on_change + + @property + def require_restart(self): + return False + @property def has_studio_override(self): """Says if entity or it's children has studio overrides.""" @@ -261,6 +271,14 @@ class BaseItemEntity(BaseEntity): self, "Dynamic entity has set `is_group` to true." ) + if ( + self.require_restart_on_change + and (self.is_dynamic_item or self.is_in_dynamic_item) + ): + raise EntitySchemaError( + self, "Dynamic entity can't require restart." + ) + @abstractmethod def set_override_state(self, state): """Set override state and trigger it on children. @@ -788,6 +806,15 @@ class ItemEntity(BaseItemEntity): # Root item reference self.root_item = self.parent.root_item + # Item require restart on value change + require_restart_on_change = self.schema_data.get("require_restart") + if ( + require_restart_on_change is None + and not (self.is_dynamic_item or self.is_in_dynamic_item) + ): + require_restart_on_change = self.parent.require_restart_on_change + self._require_restart_on_change = require_restart_on_change + # File item reference if self.parent.is_file: self.file_item = self.parent diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 409e6a66b4..295333eb60 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -68,8 +68,18 @@ class EndpointEntity(ItemEntity): def on_change(self): for callback in self.on_change_callbacks: callback() + + if self.require_restart_on_change: + if self.require_restart: + self.root_item.add_item_require_restart(self) + else: + self.root_item.remove_item_require_restart(self) self.parent.on_child_change(self) + @property + def require_restart(self): + return self.has_unsaved_changes + def update_default_value(self, value): value = self._check_update_value(value, "default") self._default_value = value @@ -115,6 +125,10 @@ class InputEntity(EndpointEntity): """Entity's value without metadata.""" return self._current_value + @property + def require_restart(self): + return self._value_is_modified + def _settings_value(self): return copy.deepcopy(self.value) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index b89473d9fb..401d3980c9 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -55,6 +55,8 @@ class RootEntity(BaseItemEntity): def __init__(self, schema_data, reset): super(RootEntity, self).__init__(schema_data) + self._require_restart_callbacks = [] + self._item_ids_require_restart = set() self._item_initalization() if reset: self.reset() @@ -64,6 +66,31 @@ class RootEntity(BaseItemEntity): """Current OverrideState.""" return self._override_state + @property + def require_restart(self): + return bool(self._item_ids_require_restart) + + def add_require_restart_change_callback(self, callback): + self._require_restart_callbacks.append(callback) + + def _on_require_restart_change(self): + for callback in self._require_restart_callbacks: + callback() + + def add_item_require_restart(self, item): + was_empty = len(self._item_ids_require_restart) == 0 + self._item_ids_require_restart.add(item.id) + if was_empty: + self._on_require_restart_change() + + def remove_item_require_restart(self, item): + if item.id not in self._item_ids_require_restart: + return + + self._item_ids_require_restart.remove(item.id) + if not self._item_ids_require_restart: + self._on_require_restart_change() + @abstractmethod def reset(self): """Reset values and entities to initial state. diff --git a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json index 50ec330a11..5f659522c3 100644 --- a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json +++ b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json @@ -3,6 +3,7 @@ "key": "ftrack", "label": "Ftrack", "collapsible": true, + "require_restart": true, "checkbox_key": "enabled", "children": [ { diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index 568ccad5b9..fe5a8d8203 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -34,7 +34,8 @@ "key": "environment", "label": "Environment", "type": "raw-json", - "env_group_key": "global" + "env_group_key": "global", + "require_restart": true }, { "type": "splitter" @@ -44,7 +45,8 @@ "key": "openpype_path", "label": "Versions Repository", "multiplatform": true, - "multipath": true + "multipath": true, + "require_restart": true } ] } diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index b643293c87..16251b5f27 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -10,6 +10,7 @@ "key": "avalon", "label": "Avalon", "collapsible": true, + "require_restart": true, "children": [ { "type": "number", @@ -35,6 +36,7 @@ "key": "timers_manager", "label": "Timers Manager", "collapsible": true, + "require_restart": true, "checkbox_key": "enabled", "children": [ { @@ -66,6 +68,7 @@ "key": "clockify", "label": "Clockify", "collapsible": true, + "require_restart": true, "checkbox_key": "enabled", "children": [ { @@ -84,6 +87,7 @@ "key": "sync_server", "label": "Site Sync", "collapsible": true, + "require_restart": true, "checkbox_key": "enabled", "children": [ { @@ -114,6 +118,7 @@ "type": "dict", "key": "deadline", "label": "Deadline", + "require_restart": true, "collapsible": true, "checkbox_key": "enabled", "children": [ @@ -133,6 +138,7 @@ "type": "dict", "key": "muster", "label": "Muster", + "require_restart": true, "collapsible": true, "checkbox_key": "enabled", "children": [ diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 01d4babd0f..b072a7f337 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -73,6 +73,7 @@ class IgnoreInputChangesObj: class SettingsCategoryWidget(QtWidgets.QWidget): state_changed = QtCore.Signal() saved = QtCore.Signal(QtWidgets.QWidget) + restart_required_trigger = QtCore.Signal() def __init__(self, user_role, parent=None): super(SettingsCategoryWidget, self).__init__(parent) @@ -185,9 +186,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): if self.user_role == "developer": self._add_developer_ui(footer_layout) - save_btn = QtWidgets.QPushButton("Save") - spacer_widget = QtWidgets.QWidget() - footer_layout.addWidget(spacer_widget, 1) + save_btn = QtWidgets.QPushButton("Save", footer_widget) + require_restart_label = QtWidgets.QLabel(footer_widget) + require_restart_label.setAlignment(QtCore.Qt.AlignCenter) + footer_layout.addWidget(require_restart_label, 1) footer_layout.addWidget(save_btn, 0) configurations_layout = QtWidgets.QVBoxLayout(configurations_widget) @@ -205,6 +207,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): save_btn.clicked.connect(self._save) self.save_btn = save_btn + self.require_restart_label = require_restart_label self.scroll_widget = scroll_widget self.content_layout = content_layout self.content_widget = content_widget @@ -323,6 +326,15 @@ class SettingsCategoryWidget(QtWidgets.QWidget): def _on_reset_start(self): return + def _on_require_restart_change(self): + value = "" + if self.entity.require_restart: + value = ( + "Your changes require restart of" + " all running OpenPype processes to take affect." + ) + self.require_restart_label.setText(value) + def reset(self): self.set_state(CategoryState.Working) @@ -339,6 +351,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget): dialog = None try: self._create_root_entity() + self.entity.add_require_restart_change_callback( + self._on_require_restart_change + ) self.add_children_gui() @@ -433,6 +448,15 @@ class SettingsCategoryWidget(QtWidgets.QWidget): return def _save(self): + # Don't trigger restart if defaults are modified + if ( + self.modify_defaults_checkbox + and self.modify_defaults_checkbox.isChecked() + ): + require_restart = False + else: + require_restart = self.entity.require_restart + self.set_state(CategoryState.Working) if self.items_are_valid(): @@ -442,6 +466,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.saved.emit(self) + if require_restart: + self.restart_required_trigger.emit() + self.require_restart_label.setText("") + def _on_refresh(self): self.reset() diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 249b4e305d..b20ce5ed66 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -275,8 +275,6 @@ class UnsavedChangesDialog(QtWidgets.QDialog): layout.addWidget(message_label) layout.addWidget(btns_widget) - self.state = None - def on_cancel_pressed(self): self.done(0) @@ -287,6 +285,48 @@ class UnsavedChangesDialog(QtWidgets.QDialog): self.done(2) +class RestartDialog(QtWidgets.QDialog): + message = ( + "Your changes require restart of process to take effect." + " Do you want to restart now?" + ) + + def __init__(self, parent=None): + super(RestartDialog, self).__init__(parent) + message_label = QtWidgets.QLabel(self.message) + + btns_widget = QtWidgets.QWidget(self) + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + + btn_restart = QtWidgets.QPushButton("Restart") + btn_restart.clicked.connect(self.on_restart_pressed) + btn_cancel = QtWidgets.QPushButton("Cancel") + btn_cancel.clicked.connect(self.on_cancel_pressed) + + btns_layout.addStretch(1) + btns_layout.addWidget(btn_restart) + btns_layout.addWidget(btn_cancel) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(message_label) + layout.addWidget(btns_widget) + + self.btn_cancel = btn_cancel + self.btn_restart = btn_restart + + def showEvent(self, event): + super(RestartDialog, self).showEvent(event) + btns_width = max(self.btn_cancel.width(), self.btn_restart.width()) + self.btn_cancel.setFixedWidth(btns_width) + self.btn_restart.setFixedWidth(btns_width) + + def on_cancel_pressed(self): + self.done(0) + + def on_restart_pressed(self): + self.done(1) + + class SpacerWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(SpacerWidget, self).__init__(parent) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 9b368588c3..7a6536fd78 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -4,7 +4,7 @@ from .categories import ( SystemWidget, ProjectWidget ) -from .widgets import ShadowWidget +from .widgets import ShadowWidget, RestartDialog from . import style from openpype.tools.settings import ( @@ -14,6 +14,8 @@ from openpype.tools.settings import ( class MainWidget(QtWidgets.QWidget): + trigger_restart = QtCore.Signal() + widget_width = 1000 widget_height = 600 @@ -60,6 +62,9 @@ class MainWidget(QtWidgets.QWidget): for tab_widget in tab_widgets: tab_widget.saved.connect(self._on_tab_save) tab_widget.state_changed.connect(self._on_state_change) + tab_widget.restart_required_trigger.connect( + self._on_restart_required + ) self.tab_widgets = tab_widgets @@ -132,3 +137,15 @@ class MainWidget(QtWidgets.QWidget): for tab_widget in self.tab_widgets: tab_widget.reset() + + def _on_restart_required(self): + # Don't show dialog if there are not registered slots for + # `trigger_restart` signal. + # - For example when settings are runnin as standalone tool + if self.receivers(self.trigger_restart) < 1: + return + + dialog = RestartDialog(self) + result = dialog.exec_() + if result == 1: + self.trigger_restart.emit() diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 534c99bd90..fa16dbf855 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -1,10 +1,13 @@ import os import sys +import atexit +import subprocess import platform from avalon import style from Qt import QtCore, QtGui, QtWidgets from openpype.api import Logger, resources +from openpype.lib import get_pype_execute_args from openpype.modules import TrayModulesManager, ITrayService from openpype.settings.lib import get_system_settings import openpype.version @@ -92,6 +95,34 @@ class TrayManager: self.tray_widget.menu.addAction(version_action) self.tray_widget.menu.addSeparator() + def restart(self): + """Restart Tray tool. + + First creates new process with same argument and close current tray. + """ + args = get_pype_execute_args() + # Create a copy of sys.argv + additional_args = list(sys.argv) + # Check last argument from `get_pype_execute_args` + # - when running from code it is the same as first from sys.argv + if args[-1] == additional_args[0]: + additional_args.pop(0) + args.extend(additional_args) + + kwargs = {} + if platform.system().lower() == "windows": + flags = ( + subprocess.CREATE_NEW_PROCESS_GROUP + | subprocess.DETACHED_PROCESS + ) + kwargs["creationflags"] = flags + + subprocess.Popen(args, **kwargs) + self.exit() + + def exit(self): + self.tray_widget.exit() + def on_exit(self): self.modules_manager.on_exit() @@ -116,6 +147,8 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): super(SystemTrayIcon, self).__init__(icon, parent) + self._exited = False + # Store parent - QtWidgets.QMainWindow() self.parent = parent @@ -134,6 +167,8 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): # Add menu to Context of SystemTrayIcon self.setContextMenu(self.menu) + atexit.register(self.exit) + def on_systray_activated(self, reason): # show contextMenu if left click if reason == QtWidgets.QSystemTrayIcon.Trigger: @@ -145,6 +180,10 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): - Icon won't stay in tray after exit. """ + if self._exited: + return + self._exited = True + self.hide() self.tray_man.on_exit() QtCore.QCoreApplication.exit()