From 5d463364ce991d8e5426e26cee6ac97c90706c53 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 7 Feb 2024 16:01:13 +0100 Subject: [PATCH 1/2] move dialogs from widgets to tools/utils --- .../ayon_core/hosts/blender/api/pipeline.py | 5 +- client/ayon_core/hosts/fusion/api/lib.py | 4 +- client/ayon_core/hosts/fusion/api/pipeline.py | 4 +- client/ayon_core/hosts/houdini/api/lib.py | 4 +- .../ayon_core/hosts/houdini/api/pipeline.py | 29 +- client/ayon_core/hosts/max/api/lib.py | 4 +- client/ayon_core/hosts/maya/api/lib.py | 22 +- client/ayon_core/hosts/maya/api/pipeline.py | 4 +- .../hosts/maya/plugins/load/load_look.py | 4 +- .../hosts/substancepainter/api/pipeline.py | 4 +- client/ayon_core/hosts/unreal/addon.py | 4 +- .../ayon_core/hosts/unreal/api/rendering.py | 5 +- .../plugins/inventory/delete_unused_assets.py | 4 +- client/ayon_core/tools/utils/__init__.py | 11 + client/ayon_core/tools/utils/dialogs.py | 253 ++++++++++++++++++ client/ayon_core/widgets/README.md | 102 ------- client/ayon_core/widgets/message_window.py | 141 ---------- client/ayon_core/widgets/popup.py | 165 ------------ 18 files changed, 310 insertions(+), 459 deletions(-) create mode 100644 client/ayon_core/tools/utils/dialogs.py delete mode 100644 client/ayon_core/widgets/README.md delete mode 100644 client/ayon_core/widgets/message_window.py delete mode 100644 client/ayon_core/widgets/popup.py diff --git a/client/ayon_core/hosts/blender/api/pipeline.py b/client/ayon_core/hosts/blender/api/pipeline.py index 5430381214..6801b1f71b 100644 --- a/client/ayon_core/hosts/blender/api/pipeline.py +++ b/client/ayon_core/hosts/blender/api/pipeline.py @@ -198,13 +198,12 @@ def uninstall(): def show_message(title, message): - from ayon_core.widgets.message_window import Window + from ayon_core.tools.utils import show_message_dialog from .ops import BlenderApplication BlenderApplication.get_app() - Window( - parent=None, + show_message_dialog( title=title, message=message, level="warning") diff --git a/client/ayon_core/hosts/fusion/api/lib.py b/client/ayon_core/hosts/fusion/api/lib.py index 88ecfb81f7..b8034afbb0 100644 --- a/client/ayon_core/hosts/fusion/api/lib.py +++ b/client/ayon_core/hosts/fusion/api/lib.py @@ -165,9 +165,9 @@ def _on_repair(): return from . import menu - from ayon_core.widgets import popup + from ayon_core.tools.utils import SimplePopup from ayon_core.style import load_stylesheet - dialog = popup.Popup(parent=menu.menu) + dialog = SimplePopup(parent=menu.menu) dialog.setWindowTitle("Fusion comp has invalid configuration") msg = "Comp preferences mismatches '{}'".format(asset_doc["name"]) diff --git a/client/ayon_core/hosts/fusion/api/pipeline.py b/client/ayon_core/hosts/fusion/api/pipeline.py index bbfb83a573..18e26d74de 100644 --- a/client/ayon_core/hosts/fusion/api/pipeline.py +++ b/client/ayon_core/hosts/fusion/api/pipeline.py @@ -189,9 +189,9 @@ def _on_show_scene_inventory(): frame.ActivateFrame() # raise comp window host_tools.show_scene_inventory() - from ayon_core.widgets import popup + from ayon_core.tools.utils import SimplePopup from ayon_core.style import load_stylesheet - dialog = popup.Popup(parent=menu.menu) + dialog = SimplePopup(parent=menu.menu) dialog.setWindowTitle("Fusion comp has outdated content") dialog.setMessage("There are outdated containers in " "your Fusion comp.") diff --git a/client/ayon_core/hosts/houdini/api/lib.py b/client/ayon_core/hosts/houdini/api/lib.py index 89763aec08..75bb775ee5 100644 --- a/client/ayon_core/hosts/houdini/api/lib.py +++ b/client/ayon_core/hosts/houdini/api/lib.py @@ -24,7 +24,7 @@ from ayon_core.pipeline.create import CreateContext from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.context_tools import get_current_project_asset -from ayon_core.widgets import popup +from ayon_core.tools.utils import PopupUpdateKeys from ayon_core.tools.utils.host_tools import get_tool_by_name import hou @@ -209,7 +209,7 @@ def validate_fps(): if parent is None: pass else: - dialog = popup.PopupUpdateKeys(parent=parent) + dialog = PopupUpdateKeys(parent=parent) dialog.setModal(True) dialog.setWindowTitle("Houdini scene does not match project FPS") dialog.setMessage("Scene %i FPS does not match project %i FPS" % diff --git a/client/ayon_core/hosts/houdini/api/pipeline.py b/client/ayon_core/hosts/houdini/api/pipeline.py index d983fd3e28..1363700b8a 100644 --- a/client/ayon_core/hosts/houdini/api/pipeline.py +++ b/client/ayon_core/hosts/houdini/api/pipeline.py @@ -305,20 +305,21 @@ def _show_outdated_content_popup(): if parent is None: log.info("Skipping outdated content pop-up " "because Houdini window can't be found.") - else: - from ayon_core.widgets import popup - - # Show outdated pop-up - def _on_show_inventory(): - from ayon_core.tools.utils import host_tools - host_tools.show_scene_inventory(parent=parent) - - dialog = popup.Popup(parent=parent) - dialog.setWindowTitle("Houdini scene has outdated content") - dialog.setMessage("There are outdated containers in " - "your Houdini scene.") - dialog.on_clicked.connect(_on_show_inventory) - dialog.show() + return + + from ayon_core.tools.utils import SimplePopup + + # Show outdated pop-up + def _on_show_inventory(): + from ayon_core.tools.utils import host_tools + host_tools.show_scene_inventory(parent=parent) + + dialog = SimplePopup(parent=parent) + dialog.setWindowTitle("Houdini scene has outdated content") + dialog.setMessage("There are outdated containers in " + "your Houdini scene.") + dialog.on_clicked.connect(_on_show_inventory) + dialog.show() def on_open(): diff --git a/client/ayon_core/hosts/max/api/lib.py b/client/ayon_core/hosts/max/api/lib.py index f2bb186551..88d9bde746 100644 --- a/client/ayon_core/hosts/max/api/lib.py +++ b/client/ayon_core/hosts/max/api/lib.py @@ -405,8 +405,8 @@ def check_colorspace(): project_name, "max", project_settings) if max_config_data and color_mgr.Mode != rt.Name("OCIO_Custom"): if not is_headless(): - from ayon_core.widgets import popup - dialog = popup.Popup(parent=parent) + from ayon_core.tools.utils import SimplePopup + dialog = SimplePopup(parent=parent) dialog.setWindowTitle("Warning: Wrong OCIO Mode") dialog.setMessage("This scene has wrong OCIO " "Mode setting.") diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 6fc2c92144..cf3e49d367 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2667,15 +2667,15 @@ def validate_fps(): """ expected_fps = get_fps_for_current_context() - current_fps = mel.eval('currentTimeUnitToFPS()') + current_fps = mel.eval("currentTimeUnitToFPS()") fps_match = current_fps == expected_fps if not fps_match and not IS_HEADLESS: - from ayon_core.widgets import popup + from ayon_core.tools.utils import PopupUpdateKeys parent = get_main_window() - dialog = popup.PopupUpdateKeys(parent=parent) + dialog = PopupUpdateKeys(parent=parent) dialog.setModal(True) dialog.setWindowTitle("Maya scene does not match project FPS") dialog.setMessage( @@ -2686,12 +2686,10 @@ def validate_fps(): dialog.setButtonText("Fix") # Set new text for button (add optional argument for the popup?) - toggle = dialog.widgets["toggle"] - update = toggle.isChecked() - dialog.on_clicked_state.connect( - lambda: set_scene_fps(expected_fps, update) - ) + def on_click(update): + set_scene_fps(expected_fps, update) + dialog.on_clicked_state.connect(on_click) dialog.show() return False @@ -3284,17 +3282,15 @@ def update_content_on_context_change(): def show_message(title, msg): from qtpy import QtWidgets - from ayon_core.widgets import message_window + from ayon_core.tools.utils import show_message_dialog # Find maya main window top_level_widgets = {w.objectName(): w for w in QtWidgets.QApplication.topLevelWidgets()} parent = top_level_widgets.get("MayaWindow", None) - if parent is None: - pass - else: - message_window.message(title=title, message=msg, parent=parent) + if parent is not None: + show_message_dialog(title=title, message=msg, parent=parent) def iter_shader_edits(relationships, shader_nodes, nodes_by_id, label=None): diff --git a/client/ayon_core/hosts/maya/api/pipeline.py b/client/ayon_core/hosts/maya/api/pipeline.py index 929135bdb0..7d918b7b26 100644 --- a/client/ayon_core/hosts/maya/api/pipeline.py +++ b/client/ayon_core/hosts/maya/api/pipeline.py @@ -583,7 +583,7 @@ def on_save(): def on_open(): """On scene open let's assume the containers have changed.""" - from ayon_core.widgets import popup + from ayon_core.tools.utils import SimplePopup # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset @@ -604,7 +604,7 @@ def on_open(): def _on_show_inventory(): host_tools.show_scene_inventory(parent=parent) - dialog = popup.Popup(parent=parent) + dialog = SimplePopup(parent=parent) dialog.setWindowTitle("Maya scene has outdated content") dialog.setMessage("There are outdated containers in " "your Maya scene.") diff --git a/client/ayon_core/hosts/maya/plugins/load/load_look.py b/client/ayon_core/hosts/maya/plugins/load/load_look.py index 10ff42aa4b..ba5891469d 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_look.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_look.py @@ -12,10 +12,10 @@ ) import ayon_core.hosts.maya.api.plugin from ayon_core.hosts.maya.api import lib -from ayon_core.widgets.message_window import ScrollMessageBox - from ayon_core.hosts.maya.api.lib import get_reference_node +from ayon_core.tools.utils import ScrollMessageBox + class LookLoader(ayon_core.hosts.maya.api.plugin.ReferenceLoader): """Specific loader for lookdev""" diff --git a/client/ayon_core/hosts/substancepainter/api/pipeline.py b/client/ayon_core/hosts/substancepainter/api/pipeline.py index 2fbf8d412d..5e28620f15 100644 --- a/client/ayon_core/hosts/substancepainter/api/pipeline.py +++ b/client/ayon_core/hosts/substancepainter/api/pipeline.py @@ -285,7 +285,7 @@ def on_open(): log.info("Running callback on open..") if any_outdated_containers(): - from ayon_core.widgets import popup + from ayon_core.tools.utils import SimplePopup log.warning("Scene has outdated content.") @@ -301,7 +301,7 @@ def _on_show_inventory(): from ayon_core.tools.utils import host_tools host_tools.show_scene_inventory(parent=parent) - dialog = popup.Popup(parent=parent) + dialog = SimplePopup(parent=parent) dialog.setWindowTitle("Substance scene has outdated content") dialog.setMessage("There are outdated containers in " "your Substance scene.") diff --git a/client/ayon_core/hosts/unreal/addon.py b/client/ayon_core/hosts/unreal/addon.py index 3cc695ba99..97ec7a4277 100644 --- a/client/ayon_core/hosts/unreal/addon.py +++ b/client/ayon_core/hosts/unreal/addon.py @@ -25,7 +25,7 @@ def add_implementation_envs(self, env, app): from .lib import get_compatible_integration - from ayon_core.widgets.message_window import Window + from ayon_core.tools.utils import show_message_dialog pattern = re.compile(r'^\d+-\d+$') @@ -34,7 +34,7 @@ def add_implementation_envs(self, env, app): "Unreal application key in the settings must be in format" "'5-0' or '5-1'" ) - Window( + show_message_dialog( parent=None, title="Unreal application name format", message=msg, diff --git a/client/ayon_core/hosts/unreal/api/rendering.py b/client/ayon_core/hosts/unreal/api/rendering.py index 279c34baf7..8717788732 100644 --- a/client/ayon_core/hosts/unreal/api/rendering.py +++ b/client/ayon_core/hosts/unreal/api/rendering.py @@ -5,7 +5,7 @@ from ayon_core.settings import get_project_settings from ayon_core.pipeline import Anatomy from ayon_core.hosts.unreal.api import pipeline -from ayon_core.widgets.message_window import Window +from ayon_core.tools.utils import show_message_dialog queue = None @@ -40,8 +40,7 @@ def start_rendering(): assets = unreal.EditorUtilityLibrary.get_selected_assets() if not assets: - Window( - parent=None, + show_message_dialog( title="No assets selected", message="No assets selected. Select a render instance.", level="warning") diff --git a/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py b/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py index cb831583cb..51d1b64ec2 100644 --- a/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py +++ b/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py @@ -34,10 +34,10 @@ def _delete_unused_assets(self, containers): def _show_confirmation_dialog(self, containers): from qtpy import QtCore - from ayon_core.widgets import popup + from ayon_core.tools.utils import SimplePopup from ayon_core.style import load_stylesheet - dialog = popup.Popup() + dialog = SimplePopup() dialog.setWindowFlags( QtCore.Qt.Window | QtCore.Qt.WindowStaysOnTopHint diff --git a/client/ayon_core/tools/utils/__init__.py b/client/ayon_core/tools/utils/__init__.py index 2a9d1fc56b..7be0ea5e9f 100644 --- a/client/ayon_core/tools/utils/__init__.py +++ b/client/ayon_core/tools/utils/__init__.py @@ -53,6 +53,12 @@ from .thumbnail_paint_widget import ThumbnailPainterWidget from .sliders import NiceSlider from .nice_checkbox import NiceCheckbox +from .dialogs import ( + show_message_dialog, + ScrollMessageBox, + SimplePopup, + PopupUpdateKeys, +) __all__ = ( @@ -110,4 +116,9 @@ "NiceSlider", "NiceCheckbox", + + "show_message_dialog", + "ScrollMessageBox", + "SimplePopup", + "PopupUpdateKeys", ) diff --git a/client/ayon_core/tools/utils/dialogs.py b/client/ayon_core/tools/utils/dialogs.py new file mode 100644 index 0000000000..5dd0ddd54e --- /dev/null +++ b/client/ayon_core/tools/utils/dialogs.py @@ -0,0 +1,253 @@ +import logging +from qtpy import QtWidgets, QtCore + +log = logging.getLogger(__name__) + + +def show_message_dialog(title, message, level=None, parent=None): + """ + + Args: + title (str): Title of dialog. + message (str): Message to display. + level (Literal["info", "warning", "critical"]): Level of dialog. + parent (Optional[QtCore.QObject]): Parent widget. + + """ + if level is None: + level = "info" + + if level == "info": + function = QtWidgets.QMessageBox.information + elif level == "warning": + function = QtWidgets.QMessageBox.warning + elif level == "critical": + function = QtWidgets.QMessageBox.critical + else: + raise ValueError(f"Invalid level: {level}") + function(parent, title, message) + + +class ScrollMessageBox(QtWidgets.QDialog): + """Basic version of scrollable QMessageBox. + + No other existing dialog implementation is scrollable. + + Args: + icon (QtWidgets.QMessageBox.Icon): Icon to display. + title (str): Window title. + messages (list[str]): List of messages. + cancelable (Optional[bool]): True if Cancel button should be added. + + """ + def __init__(self, icon, title, messages, cancelable=False): + super(ScrollMessageBox, self).__init__() + self.setWindowTitle(title) + self.icon = icon + + self._messages = messages + + self.setWindowFlags(QtCore.Qt.WindowTitleHint) + + layout = QtWidgets.QVBoxLayout(self) + + scroll_widget = QtWidgets.QScrollArea(self) + scroll_widget.setWidgetResizable(True) + content_widget = QtWidgets.QWidget(self) + scroll_widget.setWidget(content_widget) + + message_len = 0 + content_layout = QtWidgets.QVBoxLayout(content_widget) + for message in messages: + label_widget = QtWidgets.QLabel(message, content_widget) + content_layout.addWidget(label_widget) + message_len = max(message_len, len(message)) + + # guess size of scrollable area + # WARNING: 'desktop' method probably won't work in PySide6 + desktop = QtWidgets.QApplication.desktop() + max_width = desktop.availableGeometry().width() + scroll_widget.setMinimumWidth( + min(max_width, message_len * 6) + ) + layout.addWidget(scroll_widget) + + buttons = QtWidgets.QDialogButtonBox.Ok + if cancelable: + buttons |= QtWidgets.QDialogButtonBox.Cancel + + btn_box = QtWidgets.QDialogButtonBox(buttons) + btn_box.accepted.connect(self.accept) + + if cancelable: + btn_box.reject.connect(self.reject) + + btn = QtWidgets.QPushButton("Copy to clipboard") + btn.clicked.connect(self._on_copy_click) + btn_box.addButton(btn, QtWidgets.QDialogButtonBox.NoRole) + + layout.addWidget(btn_box) + + def _on_copy_click(self): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText("\n".join(self._messages)) + + +class SimplePopup(QtWidgets.QDialog): + """A Popup that moves itself to bottom right of screen on show event. + + The UI contains a message label and a red highlighted button to "show" + or perform another custom action from this pop-up. + + """ + + on_clicked = QtCore.Signal() + + def __init__(self, parent=None, *args, **kwargs): + super(SimplePopup, self).__init__(parent=parent, *args, **kwargs) + + # Set default title + self.setWindowTitle("Popup") + + self.setContentsMargins(0, 0, 0, 0) + + message_label = QtWidgets.QLabel("", self) + message_label.setStyleSheet(""" + QLabel { + font-size: 12px; + } + """) + confirm_btn = QtWidgets.QPushButton("Show", self) + confirm_btn.setSizePolicy( + QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum + ) + confirm_btn.setStyleSheet( + """QPushButton { background-color: #BB0000 }""" + ) + + # Layout + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(10, 5, 10, 10) + + # Increase spacing slightly for readability + layout.setSpacing(10) + layout.addWidget(message_label) + layout.addWidget(confirm_btn) + + # Signals + confirm_btn.clicked.connect(self._on_clicked) + + # Default size + self.resize(400, 40) + + self._message_label = message_label + self._confirm_btn = confirm_btn + + def set_message(self, message): + self._message_label.setText(message) + + def set_button_text(self, text): + self._confirm_btn.setText(text) + + def setMessage(self, message): + self.set_message(message) + + def setButtonText(self, text): + self.set_button_text(text) + + def showEvent(self, event): + # Position popup based on contents on show event + geo = self._calculate_window_geometry() + self.setGeometry(geo) + + return super(SimplePopup, self).showEvent(event) + + def _on_clicked(self): + """Callback for when the 'show' button is clicked. + + Raises the parent (if any) + + """ + + parent = self.parent() + self.close() + + # Trigger the signal + self.on_clicked.emit() + + if parent: + parent.raise_() + + def _calculate_window_geometry(self): + """Respond to status changes + + On creation, align window with screen bottom right. + + """ + + window = self + + width = window.width() + width = max(width, window.minimumWidth()) + + height = window.height() + height = max(height, window.sizeHint().height()) + + try: + screen = window.screen() + desktop_geometry = screen.availableGeometry() + except AttributeError: + # Backwards compatibility for older Qt versions + # PySide6 removed QDesktopWidget + desktop_geometry = QtWidgets.QDesktopWidget().availableGeometry() + + window_geometry = window.geometry() + + screen_width = window_geometry.width() + screen_height = window_geometry.height() + + # Calculate width and height of system tray + systray_width = window_geometry.width() - desktop_geometry.width() + systray_height = window_geometry.height() - desktop_geometry.height() + + padding = 10 + + x = screen_width - width + y = screen_height - height + + x -= systray_width + padding + y -= systray_height + padding + + return QtCore.QRect(x, y, width, height) + + +class PopupUpdateKeys(SimplePopup): + """Simple popup with checkbox.""" + + on_clicked_state = QtCore.Signal(bool) + + def __init__(self, parent=None, *args, **kwargs): + super(PopupUpdateKeys, self).__init__( + parent=parent, *args, **kwargs + ) + + layout = self.layout() + + # Insert toggle for Update keys + toggle = QtWidgets.QCheckBox("Update Keys", self) + layout.insertWidget(1, toggle) + + self.on_clicked.connect(self.emit_click_with_state) + + layout.insertStretch(1, 1) + + self._toggle_checkbox = toggle + + def is_toggle_checked(self): + return self._toggle_checkbox.isChecked() + + def emit_click_with_state(self): + """Emit the on_clicked_state signal with the toggled state""" + checked = self._toggle_checkbox.isChecked() + self.on_clicked_state.emit(checked) diff --git a/client/ayon_core/widgets/README.md b/client/ayon_core/widgets/README.md deleted file mode 100644 index 4ac935abf6..0000000000 --- a/client/ayon_core/widgets/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# Widgets - -## Splash Screen - -This widget is used for executing a monitoring progress of a process which has been executed on a different thread. - -To properly use this widget certain preparation has to be done in order to correctly execute the process and show the -splash screen. - -### Prerequisites - -In order to run a function or an operation on another thread, a `QtCore.QObject` class needs to be created with the -desired code. The class has to have a method as an entry point for the thread to execute the code. - -For utilizing the functionalities of the splash screen, certain signals need to be declared to let it know what is -happening in the thread and how is it progressing. It is also recommended to have a function to set up certain variables -which are needed in the worker's code - -For example: -```python -from qtpy import QtCore - -class ExampleWorker(QtCore.QObject): - - finished = QtCore.Signal() - failed = QtCore.Signal(str) - progress = QtCore.Signal(int) - log = QtCore.Signal(str) - stage_begin = QtCore.Signal(str) - - foo = None - bar = None - - def run(self): - # The code goes here - print("Hello world!") - self.finished.emit() - - def setup(self, - foo: str, - bar: str,): - self.foo = foo - self.bar = bar -``` - -### Creating the splash screen - -```python -import os -from qtpy import QtCore -from pathlib import Path -from ayon_core.widgets.splash_screen import SplashScreen -from ayon_core import resources - - -def exec_plugin_install( engine_path: Path, env: dict = None): - env = env or os.environ - q_thread = QtCore.QThread() - example_worker = ExampleWorker() - - q_thread.started.connect(example_worker.run) - example_worker.setup(engine_path, env) - example_worker.moveToThread(q_thread) - - splash_screen = SplashScreen("Executing process ...", - resources.get_ayon_icon_filepath()) - - # set up the splash screen with necessary events - example_worker.installing.connect(splash_screen.update_top_label_text) - example_worker.progress.connect(splash_screen.update_progress) - example_worker.log.connect(splash_screen.append_log) - example_worker.finished.connect(splash_screen.quit_and_close) - example_worker.failed.connect(splash_screen.fail) - - splash_screen.start_thread(q_thread) - splash_screen.show_ui() -``` - -In this example code, before executing the process the worker needs to be instantiated and moved onto a newly created -`QtCore.QThread` object. After this, needed signals have to be connected to the desired slots to make full use of -the splash screen. Finally, the `start_thread` and `show_ui` is called. - -**Note that when the `show_ui` function is called the thread is blocked until the splash screen quits automatically, or -it is closed by the user in case the process fails! The `start_thread` method in that case must be called before -showing the UI!** - -The most important signals are -```python -q_thread.started.connect(example_worker.run) -``` - and -```python -example_worker.finished.connect(splash_screen.quit_and_close) -``` - -These ensure that when the `start_thread` method is called (which takes as a parameter the `QtCore.QThread` object and -saves it as a reference), the `QThread` object starts and signals the worker to -start executing its own code. Once the worker is done and emits a signal that it has finished with the `quit_and_close` -slot, the splash screen quits the `QtCore.QThread` and closes itself. - -It is highly recommended to also use the `fail` slot in case an exception or other error occurs during the execution of -the worker's code (You would use in this case the `failed` signal in the `ExampleWorker`). diff --git a/client/ayon_core/widgets/message_window.py b/client/ayon_core/widgets/message_window.py deleted file mode 100644 index c207702f74..0000000000 --- a/client/ayon_core/widgets/message_window.py +++ /dev/null @@ -1,141 +0,0 @@ -import sys -import logging -from qtpy import QtWidgets, QtCore - -log = logging.getLogger(__name__) - - -class Window(QtWidgets.QWidget): - def __init__(self, parent, title, message, level): - super(Window, self).__init__() - self.parent = parent - self.title = title - self.message = message - self.level = level - - if self.level == "info": - self._info() - elif self.level == "warning": - self._warning() - elif self.level == "critical": - self._critical() - - def _info(self): - self.setWindowTitle(self.title) - rc = QtWidgets.QMessageBox.information( - self, self.title, self.message) - if rc: - self.exit() - - def _warning(self): - self.setWindowTitle(self.title) - rc = QtWidgets.QMessageBox.warning( - self, self.title, self.message) - if rc: - self.exit() - - def _critical(self): - self.setWindowTitle(self.title) - rc = QtWidgets.QMessageBox.critical( - self, self.title, self.message) - if rc: - self.exit() - - def exit(self): - self.hide() - # self.parent.exec_() - # self.parent.hide() - return - - -def message(title=None, message=None, level="info", parent=None): - """ - Produces centered dialog with specific level denoting severity - Args: - title: (string) dialog title - message: (string) message - level: (string) info|warning|critical - parent: (QtWidgets.QApplication) - - Returns: - None - """ - app = parent - if not app: - app = QtWidgets.QApplication(sys.argv) - - ex = Window(app, title, message, level) - ex.show() - - # Move widget to center of screen - try: - desktop_rect = QtWidgets.QApplication.desktop().availableGeometry(ex) - center = desktop_rect.center() - ex.move( - center.x() - (ex.width() * 0.5), - center.y() - (ex.height() * 0.5) - ) - except Exception: - # skip all possible issues that may happen feature is not crutial - log.warning("Couldn't center message.", exc_info=True) - # sys.exit(app.exec_()) - - -class ScrollMessageBox(QtWidgets.QDialog): - """ - Basic version of scrollable QMessageBox. No other existing dialog - implementation is scrollable. - Args: - icon: - title: - messages: of messages - cancelable: - True if Cancel button should be added - """ - def __init__(self, icon, title, messages, cancelable=False): - super(ScrollMessageBox, self).__init__() - self.setWindowTitle(title) - self.icon = icon - - self.setWindowFlags(QtCore.Qt.WindowTitleHint) - - layout = QtWidgets.QVBoxLayout(self) - - scroll_widget = QtWidgets.QScrollArea(self) - scroll_widget.setWidgetResizable(True) - content_widget = QtWidgets.QWidget(self) - scroll_widget.setWidget(content_widget) - - message_len = 0 - content_layout = QtWidgets.QVBoxLayout(content_widget) - for message in messages: - label_widget = QtWidgets.QLabel(message, content_widget) - content_layout.addWidget(label_widget) - message_len = max(message_len, len(message)) - - # guess size of scrollable area - desktop = QtWidgets.QApplication.desktop() - max_width = desktop.availableGeometry().width() - scroll_widget.setMinimumWidth( - min(max_width, message_len * 6) - ) - layout.addWidget(scroll_widget) - - if not cancelable: # if no specific buttons OK only - buttons = QtWidgets.QDialogButtonBox.Ok - else: - buttons = QtWidgets.QDialogButtonBox.Ok | \ - QtWidgets.QDialogButtonBox.Cancel - - btn_box = QtWidgets.QDialogButtonBox(buttons) - btn_box.accepted.connect(self.accept) - - if cancelable: - btn_box.reject.connect(self.reject) - - btn = QtWidgets.QPushButton('Copy to clipboard') - btn.clicked.connect(lambda: QtWidgets.QApplication. - clipboard().setText("\n".join(messages))) - btn_box.addButton(btn, QtWidgets.QDialogButtonBox.NoRole) - - layout.addWidget(btn_box) - self.show() diff --git a/client/ayon_core/widgets/popup.py b/client/ayon_core/widgets/popup.py deleted file mode 100644 index 225c5e18a1..0000000000 --- a/client/ayon_core/widgets/popup.py +++ /dev/null @@ -1,165 +0,0 @@ -import sys -import contextlib - -from qtpy import QtCore, QtWidgets - - -class Popup(QtWidgets.QDialog): - """A Popup that moves itself to bottom right of screen on show event. - - The UI contains a message label and a red highlighted button to "show" - or perform another custom action from this pop-up. - - """ - - on_clicked = QtCore.Signal() - - def __init__(self, parent=None, *args, **kwargs): - super(Popup, self).__init__(parent=parent, *args, **kwargs) - self.setContentsMargins(0, 0, 0, 0) - - # Layout - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(10, 5, 10, 10) - - # Increase spacing slightly for readability - layout.setSpacing(10) - - message = QtWidgets.QLabel("") - message.setStyleSheet(""" - QLabel { - font-size: 12px; - } - """) - button = QtWidgets.QPushButton("Show") - button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) - button.setStyleSheet("""QPushButton { background-color: #BB0000 }""") - - layout.addWidget(message) - layout.addWidget(button) - - # Default size - self.resize(400, 40) - - self.widgets = { - "message": message, - "button": button, - } - - # Signals - button.clicked.connect(self._on_clicked) - - # Set default title - self.setWindowTitle("Popup") - - def setMessage(self, message): - self.widgets['message'].setText(message) - - def setButtonText(self, text): - self.widgets["button"].setText(text) - - def _on_clicked(self): - """Callback for when the 'show' button is clicked. - - Raises the parent (if any) - - """ - - parent = self.parent() - self.close() - - # Trigger the signal - self.on_clicked.emit() - - if parent: - parent.raise_() - - def showEvent(self, event): - - # Position popup based on contents on show event - geo = self.calculate_window_geometry() - self.setGeometry(geo) - - return super(Popup, self).showEvent(event) - - def calculate_window_geometry(self): - """Respond to status changes - - On creation, align window with screen bottom right. - - """ - - window = self - - width = window.width() - width = max(width, window.minimumWidth()) - - height = window.height() - height = max(height, window.sizeHint().height()) - - try: - screen = window.screen() - desktop_geometry = screen.availableGeometry() - except AttributeError: - # Backwards compatibility for older Qt versions - # PySide6 removed QDesktopWidget - desktop_geometry = QtWidgets.QDesktopWidget().availableGeometry() - - window_geometry = window.geometry() - - screen_width = window_geometry.width() - screen_height = window_geometry.height() - - # Calculate width and height of system tray - systray_width = window_geometry.width() - desktop_geometry.width() - systray_height = window_geometry.height() - desktop_geometry.height() - - padding = 10 - - x = screen_width - width - y = screen_height - height - - x -= systray_width + padding - y -= systray_height + padding - - return QtCore.QRect(x, y, width, height) - - -class PopupUpdateKeys(Popup): - """Popup with Update Keys checkbox (intended for Maya)""" - - on_clicked_state = QtCore.Signal(bool) - - def __init__(self, parent=None, *args, **kwargs): - Popup.__init__(self, parent=parent, *args, **kwargs) - - layout = self.layout() - - # Insert toggle for Update keys - toggle = QtWidgets.QCheckBox("Update Keys") - layout.insertWidget(1, toggle) - self.widgets["toggle"] = toggle - - self.on_clicked.connect(self.emit_click_with_state) - - layout.insertStretch(1, 1) - - def emit_click_with_state(self): - """Emit the on_clicked_state signal with the toggled state""" - checked = self.widgets["toggle"].isChecked() - self.on_clicked_state.emit(checked) - - -@contextlib.contextmanager -def application(): - app = QtWidgets.QApplication(sys.argv) - yield - app.exec_() - - -if __name__ == "__main__": - with application(): - dialog = Popup() - dialog.setMessage("There are outdated containers in your Maya scene.") - dialog.show() From 1327bff286adb1cdbbe4b4f5b74e6909702f8774 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 7 Feb 2024 17:45:20 +0100 Subject: [PATCH 2/2] use snake-case method names --- client/ayon_core/hosts/fusion/api/lib.py | 4 ++-- client/ayon_core/hosts/fusion/api/pipeline.py | 2 +- client/ayon_core/hosts/houdini/api/lib.py | 13 ++++++------- client/ayon_core/hosts/houdini/api/pipeline.py | 2 +- client/ayon_core/hosts/max/api/lib.py | 4 ++-- client/ayon_core/hosts/maya/api/lib.py | 4 ++-- client/ayon_core/hosts/maya/api/pipeline.py | 2 +- .../hosts/substancepainter/api/pipeline.py | 2 +- .../plugins/inventory/delete_unused_assets.py | 4 ++-- 9 files changed, 18 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/hosts/fusion/api/lib.py b/client/ayon_core/hosts/fusion/api/lib.py index b8034afbb0..b31f812c1b 100644 --- a/client/ayon_core/hosts/fusion/api/lib.py +++ b/client/ayon_core/hosts/fusion/api/lib.py @@ -172,8 +172,8 @@ def _on_repair(): msg = "Comp preferences mismatches '{}'".format(asset_doc["name"]) msg += "\n" + "\n".join(invalid) - dialog.setMessage(msg) - dialog.setButtonText("Repair") + dialog.set_message(msg) + dialog.set_button_text("Repair") dialog.on_clicked.connect(_on_repair) dialog.show() dialog.raise_() diff --git a/client/ayon_core/hosts/fusion/api/pipeline.py b/client/ayon_core/hosts/fusion/api/pipeline.py index 18e26d74de..b3c42314b7 100644 --- a/client/ayon_core/hosts/fusion/api/pipeline.py +++ b/client/ayon_core/hosts/fusion/api/pipeline.py @@ -193,7 +193,7 @@ def _on_show_scene_inventory(): from ayon_core.style import load_stylesheet dialog = SimplePopup(parent=menu.menu) dialog.setWindowTitle("Fusion comp has outdated content") - dialog.setMessage("There are outdated containers in " + dialog.set_message("There are outdated containers in " "your Fusion comp.") dialog.on_clicked.connect(_on_show_scene_inventory) dialog.show() diff --git a/client/ayon_core/hosts/houdini/api/lib.py b/client/ayon_core/hosts/houdini/api/lib.py index 75bb775ee5..c0c398fbb6 100644 --- a/client/ayon_core/hosts/houdini/api/lib.py +++ b/client/ayon_core/hosts/houdini/api/lib.py @@ -5,7 +5,6 @@ import re import uuid import logging -from contextlib import contextmanager import json import six @@ -24,7 +23,7 @@ from ayon_core.pipeline.create import CreateContext from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.context_tools import get_current_project_asset -from ayon_core.tools.utils import PopupUpdateKeys +from ayon_core.tools.utils import PopupUpdateKeys, SimplePopup from ayon_core.tools.utils.host_tools import get_tool_by_name import hou @@ -212,9 +211,9 @@ def validate_fps(): dialog = PopupUpdateKeys(parent=parent) dialog.setModal(True) dialog.setWindowTitle("Houdini scene does not match project FPS") - dialog.setMessage("Scene %i FPS does not match project %i FPS" % + dialog.set_message("Scene %i FPS does not match project %i FPS" % (current_fps, fps)) - dialog.setButtonText("Fix") + dialog.set_button_text("Fix") # on_show is the Fix button clicked callback dialog.on_clicked_state.connect(lambda: set_scene_fps(fps)) @@ -950,11 +949,11 @@ def update_houdini_vars_context_dialog(): # TODO: Use better UI! parent = hou.ui.mainQtWindow() - dialog = popup.Popup(parent=parent) + dialog = SimplePopup(parent=parent) dialog.setModal(True) dialog.setWindowTitle("Houdini scene has outdated asset variables") - dialog.setMessage(message) - dialog.setButtonText("Fix") + dialog.set_message(message) + dialog.set_button_text("Fix") # on_show is the Fix button clicked callback dialog.on_clicked.connect(update_houdini_vars_context) diff --git a/client/ayon_core/hosts/houdini/api/pipeline.py b/client/ayon_core/hosts/houdini/api/pipeline.py index 1363700b8a..d93ea9acec 100644 --- a/client/ayon_core/hosts/houdini/api/pipeline.py +++ b/client/ayon_core/hosts/houdini/api/pipeline.py @@ -316,7 +316,7 @@ def _on_show_inventory(): dialog = SimplePopup(parent=parent) dialog.setWindowTitle("Houdini scene has outdated content") - dialog.setMessage("There are outdated containers in " + dialog.set_message("There are outdated containers in " "your Houdini scene.") dialog.on_clicked.connect(_on_show_inventory) dialog.show() diff --git a/client/ayon_core/hosts/max/api/lib.py b/client/ayon_core/hosts/max/api/lib.py index 88d9bde746..05c3364e4a 100644 --- a/client/ayon_core/hosts/max/api/lib.py +++ b/client/ayon_core/hosts/max/api/lib.py @@ -408,9 +408,9 @@ def check_colorspace(): from ayon_core.tools.utils import SimplePopup dialog = SimplePopup(parent=parent) dialog.setWindowTitle("Warning: Wrong OCIO Mode") - dialog.setMessage("This scene has wrong OCIO " + dialog.set_message("This scene has wrong OCIO " "Mode setting.") - dialog.setButtonText("Fix") + dialog.set_button_text("Fix") dialog.setStyleSheet(load_stylesheet()) dialog.on_clicked.connect(reset_colorspace) dialog.show() diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index cf3e49d367..7b791c3d51 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2678,12 +2678,12 @@ def validate_fps(): dialog = PopupUpdateKeys(parent=parent) dialog.setModal(True) dialog.setWindowTitle("Maya scene does not match project FPS") - dialog.setMessage( + dialog.set_message( "Scene {} FPS does not match project {} FPS".format( current_fps, expected_fps ) ) - dialog.setButtonText("Fix") + dialog.set_button_text("Fix") # Set new text for button (add optional argument for the popup?) def on_click(update): diff --git a/client/ayon_core/hosts/maya/api/pipeline.py b/client/ayon_core/hosts/maya/api/pipeline.py index 7d918b7b26..95617cb90a 100644 --- a/client/ayon_core/hosts/maya/api/pipeline.py +++ b/client/ayon_core/hosts/maya/api/pipeline.py @@ -606,7 +606,7 @@ def _on_show_inventory(): dialog = SimplePopup(parent=parent) dialog.setWindowTitle("Maya scene has outdated content") - dialog.setMessage("There are outdated containers in " + dialog.set_message("There are outdated containers in " "your Maya scene.") dialog.on_clicked.connect(_on_show_inventory) dialog.show() diff --git a/client/ayon_core/hosts/substancepainter/api/pipeline.py b/client/ayon_core/hosts/substancepainter/api/pipeline.py index 5e28620f15..06643df7e5 100644 --- a/client/ayon_core/hosts/substancepainter/api/pipeline.py +++ b/client/ayon_core/hosts/substancepainter/api/pipeline.py @@ -303,7 +303,7 @@ def _on_show_inventory(): dialog = SimplePopup(parent=parent) dialog.setWindowTitle("Substance scene has outdated content") - dialog.setMessage("There are outdated containers in " + dialog.set_message("There are outdated containers in " "your Substance scene.") dialog.on_clicked.connect(_on_show_inventory) dialog.show() diff --git a/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py b/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py index 51d1b64ec2..1f63a1697a 100644 --- a/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py +++ b/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py @@ -44,11 +44,11 @@ def _show_confirmation_dialog(self, containers): ) dialog.setFocusPolicy(QtCore.Qt.StrongFocus) dialog.setWindowTitle("Delete all unused assets") - dialog.setMessage( + dialog.set_message( "You are about to delete all the assets in the project that \n" "are not used in any level. Are you sure you want to continue?" ) - dialog.setButtonText("Delete") + dialog.set_button_text("Delete") dialog.on_clicked.connect( lambda: self._delete_unused_assets(containers)