From c608685f102d285ffcce1092ddfa8a1bd74bdd04 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 12 Feb 2024 15:06:12 +0100 Subject: [PATCH] apply changes from OpenPype PR --- client/ayon_core/tools/tray/__init__.py | 1 + client/ayon_core/tools/tray/dialogs.py | 155 +++++++++++ client/ayon_core/tools/tray/tray.py | 347 +++++++++++++++--------- 3 files changed, 378 insertions(+), 125 deletions(-) create mode 100644 client/ayon_core/tools/tray/dialogs.py diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/__init__.py index f5e558e0bb..49130e660a 100644 --- a/client/ayon_core/tools/tray/__init__.py +++ b/client/ayon_core/tools/tray/__init__.py @@ -1,5 +1,6 @@ from .tray import main + __all__ = ( "main", ) diff --git a/client/ayon_core/tools/tray/dialogs.py b/client/ayon_core/tools/tray/dialogs.py new file mode 100644 index 0000000000..67348284a1 --- /dev/null +++ b/client/ayon_core/tools/tray/dialogs.py @@ -0,0 +1,155 @@ +import os + +from qtpy import QtWidgets, QtCore, QtGui + +from ayon_core import resources, style +from ayon_core.tools.utils import paint_image_with_color + + +class PixmapLabel(QtWidgets.QLabel): + """Label resizing image to height of font.""" + def __init__(self, pixmap, parent): + super(PixmapLabel, self).__init__(parent) + self._empty_pixmap = QtGui.QPixmap(0, 0) + self._source_pixmap = pixmap + + def set_source_pixmap(self, pixmap): + """Change source image.""" + self._source_pixmap = pixmap + self._set_resized_pix() + + def _get_pix_size(self): + size = self.fontMetrics().height() * 3 + return size, size + + def _set_resized_pix(self): + if self._source_pixmap is None: + self.setPixmap(self._empty_pixmap) + return + width, height = self._get_pix_size() + self.setPixmap( + self._source_pixmap.scaled( + width, + height, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + ) + + def resizeEvent(self, event): + self._set_resized_pix() + super(PixmapLabel, self).resizeEvent(event) + + +class UpdateDialog(QtWidgets.QDialog): + restart_requested = QtCore.Signal() + ignore_requested = QtCore.Signal() + + _min_width = 400 + _min_height = 130 + + def __init__(self, parent=None): + super(UpdateDialog, self).__init__(parent) + + icon = QtGui.QIcon(resources.get_ayon_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowTitle("AYON update") + self.setWindowFlags( + self.windowFlags() + | QtCore.Qt.WindowStaysOnTopHint + ) + + self.setMinimumWidth(self._min_width) + self.setMinimumHeight(self._min_height) + + top_widget = QtWidgets.QWidget(self) + + gift_pixmap = self._get_gift_pixmap() + gift_icon_label = PixmapLabel(gift_pixmap, top_widget) + + label_widget = QtWidgets.QLabel( + ( + "Your AYON needs to update." + "

Please restart AYON launcher and all running" + " applications as soon as possible." + ), + top_widget + ) + label_widget.setWordWrap(True) + + top_layout = QtWidgets.QHBoxLayout(top_widget) + top_layout.setSpacing(10) + top_layout.addWidget(gift_icon_label, 0, QtCore.Qt.AlignCenter) + top_layout.addWidget(label_widget, 1) + + ignore_btn = QtWidgets.QPushButton("Ignore", self) + restart_btn = QtWidgets.QPushButton("Restart && Change", self) + restart_btn.setObjectName("TrayRestartButton") + + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ignore_btn, 0) + btns_layout.addWidget(restart_btn, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(top_widget, 0) + layout.addStretch(1) + layout.addLayout(btns_layout, 0) + + ignore_btn.clicked.connect(self._on_ignore) + restart_btn.clicked.connect(self._on_reset) + + self._label_widget = label_widget + self._gift_icon_label = gift_icon_label + self._ignore_btn = ignore_btn + self._restart_btn = restart_btn + + self._restart_accepted = False + self._current_is_higher = False + + self._close_silently = False + + self.setStyleSheet(style.load_stylesheet()) + + def close_silently(self): + self._close_silently = True + self.close() + + def showEvent(self, event): + super(UpdateDialog, self).showEvent(event) + self._close_silently = False + self._restart_accepted = False + + def closeEvent(self, event): + super(UpdateDialog, self).closeEvent(event) + if self._restart_accepted or self._current_is_higher: + return + + if self._close_silently: + return + + # Trigger ignore requested only if restart was not clicked and current + # version is lower + self.ignore_requested.emit() + + def _on_ignore(self): + self.reject() + + def _on_reset(self): + self._restart_accepted = True + self.restart_requested.emit() + self.accept() + + def _get_gift_pixmap(self): + image_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "images", + "gifts.png" + ) + src_image = QtGui.QImage(image_path) + color_value = style.get_objected_colors("font") + + return paint_image_with_color( + src_image, + color_value.get_qcolor() + ) diff --git a/client/ayon_core/tools/tray/tray.py b/client/ayon_core/tools/tray/tray.py index 3a70d68466..c16f1513be 100644 --- a/client/ayon_core/tools/tray/tray.py +++ b/client/ayon_core/tools/tray/tray.py @@ -1,10 +1,11 @@ -import collections import os import sys +import collections import atexit import platform +import ayon_api from qtpy import QtCore, QtGui, QtWidgets from ayon_core import resources, style @@ -12,57 +13,24 @@ from ayon_core.lib import ( Logger, get_ayon_launcher_args, run_detached_process, + is_dev_mode_enabled, + is_staging_enabled, + is_running_from_build, ) -from ayon_core.lib import is_running_from_build from ayon_core.addon import ( ITrayAction, ITrayService, TrayAddonsManager, ) -from ayon_core.settings import get_system_settings from ayon_core.tools.utils import ( WrappedCallbackItem, get_ayon_qt_app, ) from .info_widget import InfoWidget - - -# TODO PixmapLabel should be moved to 'utils' in other future PR so should be -# imported from there -class PixmapLabel(QtWidgets.QLabel): - """Label resizing image to height of font.""" - def __init__(self, pixmap, parent): - super(PixmapLabel, self).__init__(parent) - self._empty_pixmap = QtGui.QPixmap(0, 0) - self._source_pixmap = pixmap - - def set_source_pixmap(self, pixmap): - """Change source image.""" - self._source_pixmap = pixmap - self._set_resized_pix() - - def _get_pix_size(self): - size = self.fontMetrics().height() * 3 - return size, size - - def _set_resized_pix(self): - if self._source_pixmap is None: - self.setPixmap(self._empty_pixmap) - return - width, height = self._get_pix_size() - self.setPixmap( - self._source_pixmap.scaled( - width, - height, - QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation - ) - ) - - def resizeEvent(self, event): - self._set_resized_pix() - super(PixmapLabel, self).resizeEvent(event) +from .dialogs import ( + UpdateDialog, +) class TrayManager: @@ -91,9 +59,13 @@ class TrayManager: self.errors = [] - self.main_thread_timer = None + self._bundle_check_timer = None + self._outdated_dialog = None + + self._main_thread_timer = None self._main_thread_callbacks = collections.deque() self._execution_in_progress = None + self._closing = False @property def doubleclick_callback(self): @@ -107,29 +79,25 @@ class TrayManager: if callback: self.execute_in_main_thread(callback) - def _restart_and_install(self): - self.restart(use_expected_version=True) + def show_tray_message(self, title, message, icon=None, msecs=None): + """Show tray message. - def execute_in_main_thread(self, callback, *args, **kwargs): - if isinstance(callback, WrappedCallbackItem): - item = callback - else: - item = WrappedCallbackItem(callback, *args, **kwargs) + Args: + title (str): Title of message. + message (str): Content of message. + icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is + Information icon, may differ by Qt version. + msecs (int): Duration of message visibility in milliseconds. + Default is 10000 msecs, may differ by Qt version. + """ + args = [title, message] + kwargs = {} + if icon: + kwargs["icon"] = icon + if msecs: + kwargs["msecs"] = msecs - self._main_thread_callbacks.append(item) - - return item - - def _main_thread_execution(self): - if self._execution_in_progress: - return - self._execution_in_progress = True - for _ in range(len(self._main_thread_callbacks)): - if self._main_thread_callbacks: - item = self._main_thread_callbacks.popleft() - item.execute() - - self._execution_in_progress = False + self.tray_widget.showMessage(*args, **kwargs) def initialize_addons(self): """Add addons to tray.""" @@ -140,7 +108,9 @@ class TrayManager: self.tray_widget.menu.addMenu(admin_submenu) # Add services if they are - services_submenu = ITrayService.services_submenu(self.tray_widget.menu) + services_submenu = ITrayService.services_submenu( + self.tray_widget.menu + ) self.tray_widget.menu.addMenu(services_submenu) # Add separator @@ -165,39 +135,175 @@ class TrayManager: main_thread_timer.timeout.connect(self._main_thread_execution) main_thread_timer.start() - self.main_thread_timer = main_thread_timer + self._main_thread_timer = main_thread_timer + + version_check_timer = QtCore.QTimer() + if self._version_check_interval > 0: + version_check_timer.timeout.connect(self._on_bundle_check_timer) + version_check_timer.setInterval(self._version_check_interval) + version_check_timer.start() + self._bundle_check_timer = version_check_timer + + # For storing missing settings dialog + self._settings_validation_dialog = None self.execute_in_main_thread(self._startup_validations) + def restart(self): + """Restart Tray tool. + + First creates new process with same argument and close current tray. + """ + + self._closing = True + + args = get_ayon_launcher_args() + + # Create a copy of sys.argv + additional_args = list(sys.argv) + # Remove first argument from 'sys.argv' + # - when running from code the first argument is 'start.py' + # - when running from build the first argument is executable + additional_args.pop(0) + additional_args = [ + arg + for arg in additional_args + if arg not in {"--use-staging", "--use-dev"} + ] + + if is_dev_mode_enabled(): + additional_args.append("--use-dev") + elif is_staging_enabled(): + additional_args.append("--use-staging") + + args.extend(additional_args) + + envs = dict(os.environ.items()) + for key in { + "AYON_BUNDLE_NAME", + }: + envs.pop(key, None) + + run_detached_process(args, env=envs) + self.exit() + + def exit(self): + self._closing = True + self.tray_widget.exit() + + def on_exit(self): + self._addons_manager.on_exit() + + def execute_in_main_thread(self, callback, *args, **kwargs): + if isinstance(callback, WrappedCallbackItem): + item = callback + else: + item = WrappedCallbackItem(callback, *args, **kwargs) + + self._main_thread_callbacks.append(item) + + return item + + def _on_bundle_check_timer(self): + try: + bundles = ayon_api.get_bundles() + user = ayon_api.get_user() + # This is a workaround for bug in ayon-python-api + if user.get("code") == 401: + raise Exception("Unauthorized") + except Exception: + self._revalidate_ayon_auth() + if self._closing: + return + + try: + bundles = ayon_api.get_bundles() + except Exception: + return + + if is_dev_mode_enabled(): + return + + bundle_type = ( + "stagingBundle" + if is_staging_enabled() + else "productionBundle" + ) + + expected_bundle = bundles.get(bundle_type) + current_bundle = os.environ.get("AYON_BUNDLE_NAME") + is_expected = expected_bundle == current_bundle + if is_expected or expected_bundle is None: + self._restart_action.setVisible(False) + if ( + self._outdated_dialog is not None + and self._outdated_dialog.isVisible() + ): + self._outdated_dialog.close_silently() + return + + self._restart_action.setVisible(True) + + if self._outdated_dialog is None: + self._outdated_dialog = UpdateDialog() + self._outdated_dialog.restart_requested.connect( + self._restart_and_install + ) + self._outdated_dialog.ignore_requested.connect( + self._outdated_bundle_ignored + ) + + self._outdated_dialog.show() + self._outdated_dialog.raise_() + self._outdated_dialog.activateWindow() + + def _revalidate_ayon_auth(self): + result = self._show_ayon_login(restart_on_token_change=False) + if self._closing: + return False + + if not result.new_token: + self.exit() + return False + return True + + def _restart_and_install(self): + self.restart() + + def _outdated_bundle_ignored(self): + self.show_tray_message( + "AYON update ignored", + ( + "Please restart AYON launcher as soon as possible" + " to propagate updates." + ) + ) + + def _main_thread_execution(self): + if self._execution_in_progress: + return + self._execution_in_progress = True + for _ in range(len(self._main_thread_callbacks)): + if self._main_thread_callbacks: + item = self._main_thread_callbacks.popleft() + try: + item.execute() + except BaseException: + self.log.erorr( + "Main thread execution failed", exc_info=True + ) + + self._execution_in_progress = False + def _startup_validations(self): """Run possible startup validations.""" - pass - - def show_tray_message(self, title, message, icon=None, msecs=None): - """Show tray message. - - Args: - title (str): Title of message. - message (str): Content of message. - icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is - Information icon, may differ by Qt version. - msecs (int): Duration of message visibility in milliseconds. - Default is 10000 msecs, may differ by Qt version. - """ - args = [title, message] - kwargs = {} - if icon: - kwargs["icon"] = icon - if msecs: - kwargs["msecs"] = msecs - - self.tray_widget.showMessage(*args, **kwargs) + # Trigger bundle validation on start + self._bundle_check_timer.timeout.emit() def _add_version_item(self): login_action = QtWidgets.QAction("Login", self.tray_widget) login_action.triggered.connect(self._on_ayon_login) self.tray_widget.menu.addAction(login_action) - version_string = os.getenv("AYON_VERSION", "AYON Info") version_action = QtWidgets.QAction(version_string, self.tray_widget) @@ -216,16 +322,24 @@ class TrayManager: self._restart_action = restart_action def _on_ayon_login(self): - self.execute_in_main_thread(self._show_ayon_login) + self.execute_in_main_thread( + self._show_ayon_login, + restart_on_token_change=True + ) - def _show_ayon_login(self): + def _show_ayon_login(self, restart_on_token_change): from ayon_common.connection.credentials import change_user_ui result = change_user_ui() if result.shutdown: self.exit() + return result - elif result.restart or result.token_changed: + restart = result.restart + if restart_on_token_change and result.token_changed: + restart = True + + if restart: # Remove environment variables from current connection # - keep develop, staging, headless values for key in { @@ -235,23 +349,13 @@ class TrayManager: }: os.environ.pop(key, None) self.restart() + return result def _on_restart_action(self): - self.restart(use_expected_version=True) + self.restart() - def restart(self, use_expected_version=False, reset_version=False): - """Restart Tray tool. - - First creates new process with same argument and close current tray. - - Args: - use_expected_version(bool): OpenPype version is set to expected - version. - reset_version(bool): OpenPype version is cleaned up so igniters - logic will decide which version will be used. - """ + def _restart_ayon(self): args = get_ayon_launcher_args() - envs = dict(os.environ.items()) # Create a copy of sys.argv additional_args = list(sys.argv) @@ -259,35 +363,28 @@ class TrayManager: # - when running from code the first argument is 'start.py' # - when running from build the first argument is executable additional_args.pop(0) + additional_args = [ + arg + for arg in additional_args + if arg not in {"--use-staging", "--use-dev"} + ] - cleanup_additional_args = False - if use_expected_version: - cleanup_additional_args = True - reset_version = True - - # Pop OPENPYPE_VERSION - if reset_version: - cleanup_additional_args = True - envs.pop("OPENPYPE_VERSION", None) - - if cleanup_additional_args: - _additional_args = [] - for arg in additional_args: - if arg == "--use-staging" or arg.startswith("--use-version"): - continue - _additional_args.append(arg) - additional_args = _additional_args + if is_dev_mode_enabled(): + additional_args.append("--use-dev") + elif is_staging_enabled(): + additional_args.append("--use-staging") args.extend(additional_args) + + envs = dict(os.environ.items()) + for key in { + "AYON_BUNDLE_NAME", + }: + envs.pop(key, None) + run_detached_process(args, env=envs) self.exit() - def exit(self): - self.tray_widget.exit() - - def on_exit(self): - self._addons_manager.on_exit() - def _on_version_action(self): if self._info_widget is None: self._info_widget = InfoWidget()