diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 364a84cb7b..ed6b82ef52 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -370,67 +370,11 @@ def _load_ayon_addons(log): return all_addon_modules -def _load_addons_in_core(log): - # Add current directory at first place - # - has small differences in import logic - addon_modules = [] - modules_dir = os.path.join(AYON_CORE_ROOT, "modules") - if not os.path.exists(modules_dir): - log.warning( - f"Could not find path when loading AYON addons \"{modules_dir}\"" - ) - return addon_modules - - ignored_filenames = IGNORED_FILENAMES | IGNORED_DEFAULT_FILENAMES - for filename in os.listdir(modules_dir): - # Ignore filenames - if filename in ignored_filenames: - continue - - fullpath = os.path.join(modules_dir, filename) - basename, ext = os.path.splitext(filename) - - # Validations - if os.path.isdir(fullpath): - # Check existence of init file - init_path = os.path.join(fullpath, "__init__.py") - if not os.path.exists(init_path): - log.debug(( - "Addon directory does not contain __init__.py" - f" file {fullpath}" - )) - continue - - elif ext != ".py": - continue - - # TODO add more logic how to define if folder is addon or not - # - check manifest and content of manifest - try: - # Don't import dynamically current directory modules - import_str = f"ayon_core.modules.{basename}" - default_module = __import__(import_str, fromlist=("", )) - addon_modules.append(default_module) - - except Exception: - log.error( - f"Failed to import in-core addon '{basename}'.", - exc_info=True - ) - return addon_modules - - def _load_addons(): log = Logger.get_logger("AddonsLoader") - addon_modules = _load_ayon_addons(log) - # All addon in 'modules' folder are tray actions and should be moved - # to tray tool. - # TODO remove - addon_modules.extend(_load_addons_in_core(log)) - # Store modules to local cache - _LoadCache.addon_modules = addon_modules + _LoadCache.addon_modules = _load_ayon_addons(log) class AYONAddon(ABC): diff --git a/client/ayon_core/addon/interfaces.py b/client/ayon_core/addon/interfaces.py index b273e7839b..2616913dc0 100644 --- a/client/ayon_core/addon/interfaces.py +++ b/client/ayon_core/addon/interfaces.py @@ -125,6 +125,7 @@ class ITrayAddon(AYONInterface): tray_initialized = False _tray_manager = None + _admin_submenu = None @abstractmethod def tray_init(self): @@ -198,6 +199,27 @@ class ITrayAddon(AYONInterface): if hasattr(self.manager, "add_doubleclick_callback"): self.manager.add_doubleclick_callback(self, callback) + @staticmethod + def admin_submenu(tray_menu): + if ITrayAddon._admin_submenu is None: + from qtpy import QtWidgets + + admin_submenu = QtWidgets.QMenu("Admin", tray_menu) + admin_submenu.menuAction().setVisible(False) + ITrayAddon._admin_submenu = admin_submenu + return ITrayAddon._admin_submenu + + @staticmethod + def add_action_to_admin_submenu(label, tray_menu): + from qtpy import QtWidgets + + menu = ITrayAddon.admin_submenu(tray_menu) + action = QtWidgets.QAction(label, menu) + menu.addAction(action) + if not menu.menuAction().isVisible(): + menu.menuAction().setVisible(True) + return action + class ITrayAction(ITrayAddon): """Implementation of Tray action. @@ -211,7 +233,6 @@ class ITrayAction(ITrayAddon): """ admin_action = False - _admin_submenu = None _action_item = None @property @@ -229,12 +250,7 @@ class ITrayAction(ITrayAddon): from qtpy import QtWidgets if self.admin_action: - menu = self.admin_submenu(tray_menu) - action = QtWidgets.QAction(self.label, menu) - menu.addAction(action) - if not menu.menuAction().isVisible(): - menu.menuAction().setVisible(True) - + action = self.add_action_to_admin_submenu(self.label, tray_menu) else: action = QtWidgets.QAction(self.label, tray_menu) tray_menu.addAction(action) @@ -248,16 +264,6 @@ class ITrayAction(ITrayAddon): def tray_exit(self): return - @staticmethod - def admin_submenu(tray_menu): - if ITrayAction._admin_submenu is None: - from qtpy import QtWidgets - - admin_submenu = QtWidgets.QMenu("Admin", tray_menu) - admin_submenu.menuAction().setVisible(False) - ITrayAction._admin_submenu = admin_submenu - return ITrayAction._admin_submenu - class ITrayService(ITrayAddon): # Module's property diff --git a/client/ayon_core/modules/__init__.py b/client/ayon_core/modules/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/modules/launcher_action.py b/client/ayon_core/modules/launcher_action.py deleted file mode 100644 index 344b0bc389..0000000000 --- a/client/ayon_core/modules/launcher_action.py +++ /dev/null @@ -1,60 +0,0 @@ -import os - -from ayon_core import AYON_CORE_ROOT -from ayon_core.addon import AYONAddon, ITrayAction - - -class LauncherAction(AYONAddon, ITrayAction): - label = "Launcher" - name = "launcher_tool" - version = "1.0.0" - - def initialize(self, settings): - - # Tray attributes - self._window = None - - def tray_init(self): - self._create_window() - - self.add_doubleclick_callback(self._show_launcher) - - def tray_start(self): - return - - def connect_with_addons(self, enabled_modules): - # Register actions - if not self.tray_initialized: - return - - from ayon_core.pipeline.actions import register_launcher_action_path - - actions_dir = os.path.join(AYON_CORE_ROOT, "plugins", "actions") - if os.path.exists(actions_dir): - register_launcher_action_path(actions_dir) - - actions_paths = self.manager.collect_plugin_paths()["actions"] - for path in actions_paths: - if path and os.path.exists(path): - register_launcher_action_path(path) - - def on_action_trigger(self): - """Implementation for ITrayAction interface. - - Show launcher tool on action trigger. - """ - - self._show_launcher() - - def _create_window(self): - if self._window: - return - from ayon_core.tools.launcher.ui import LauncherWindow - self._window = LauncherWindow() - - def _show_launcher(self): - if self._window is None: - return - self._window.show() - self._window.raise_() - self._window.activateWindow() diff --git a/client/ayon_core/modules/loader_action.py b/client/ayon_core/modules/loader_action.py deleted file mode 100644 index a58d7fd456..0000000000 --- a/client/ayon_core/modules/loader_action.py +++ /dev/null @@ -1,68 +0,0 @@ -from ayon_core.addon import AYONAddon, ITrayAddon - - -class LoaderAddon(AYONAddon, ITrayAddon): - name = "loader_tool" - version = "1.0.0" - - def initialize(self, settings): - # Tray attributes - self._loader_imported = None - self._loader_window = None - - def tray_init(self): - # Add library tool - self._loader_imported = False - try: - from ayon_core.tools.loader.ui import LoaderWindow # noqa F401 - - self._loader_imported = True - except Exception: - self.log.warning( - "Couldn't load Loader tool for tray.", - exc_info=True - ) - - # Definition of Tray menu - def tray_menu(self, tray_menu): - if not self._loader_imported: - return - - from qtpy import QtWidgets - # Actions - action_loader = QtWidgets.QAction( - "Loader", tray_menu - ) - - action_loader.triggered.connect(self.show_loader) - - tray_menu.addAction(action_loader) - - def tray_start(self, *_a, **_kw): - return - - def tray_exit(self, *_a, **_kw): - return - - def show_loader(self): - if self._loader_window is None: - from ayon_core.pipeline import install_ayon_plugins - - self._init_loader() - - install_ayon_plugins() - - self._loader_window.show() - - # Raise and activate the window - # for MacOS - self._loader_window.raise_() - # for Windows - self._loader_window.activateWindow() - - def _init_loader(self): - from ayon_core.tools.loader.ui import LoaderWindow - - libraryloader = LoaderWindow() - - self._loader_window = libraryloader diff --git a/client/ayon_core/modules/python_console_interpreter/__init__.py b/client/ayon_core/modules/python_console_interpreter/__init__.py deleted file mode 100644 index 8d5c23bdba..0000000000 --- a/client/ayon_core/modules/python_console_interpreter/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .addon import ( - PythonInterpreterAction -) - - -__all__ = ( - "PythonInterpreterAction", -) diff --git a/client/ayon_core/modules/python_console_interpreter/addon.py b/client/ayon_core/modules/python_console_interpreter/addon.py deleted file mode 100644 index b0dce2585e..0000000000 --- a/client/ayon_core/modules/python_console_interpreter/addon.py +++ /dev/null @@ -1,42 +0,0 @@ -from ayon_core.addon import AYONAddon, ITrayAction - - -class PythonInterpreterAction(AYONAddon, ITrayAction): - label = "Console" - name = "python_interpreter" - version = "1.0.0" - admin_action = True - - def initialize(self, settings): - self._interpreter_window = None - - def tray_init(self): - self.create_interpreter_window() - - def tray_exit(self): - if self._interpreter_window is not None: - self._interpreter_window.save_registry() - - def create_interpreter_window(self): - """Initializa Settings Qt window.""" - if self._interpreter_window: - return - - from ayon_core.modules.python_console_interpreter.window import ( - PythonInterpreterWidget - ) - - self._interpreter_window = PythonInterpreterWidget() - - def on_action_trigger(self): - self.show_interpreter_window() - - def show_interpreter_window(self): - self.create_interpreter_window() - - if self._interpreter_window.isVisible(): - self._interpreter_window.activateWindow() - self._interpreter_window.raise_() - return - - self._interpreter_window.show() diff --git a/client/ayon_core/modules/python_console_interpreter/window/__init__.py b/client/ayon_core/modules/python_console_interpreter/window/__init__.py deleted file mode 100644 index 92fd6f1df2..0000000000 --- a/client/ayon_core/modules/python_console_interpreter/window/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .widgets import ( - PythonInterpreterWidget -) - - -__all__ = ( - "PythonInterpreterWidget", -) diff --git a/client/ayon_core/modules/python_console_interpreter/window/widgets.py b/client/ayon_core/modules/python_console_interpreter/window/widgets.py deleted file mode 100644 index 628a2e72ff..0000000000 --- a/client/ayon_core/modules/python_console_interpreter/window/widgets.py +++ /dev/null @@ -1,660 +0,0 @@ -import os -import re -import sys -import collections -from code import InteractiveInterpreter - -import appdirs -from qtpy import QtCore, QtWidgets, QtGui - -from ayon_core import resources -from ayon_core.style import load_stylesheet -from ayon_core.lib import JSONSettingRegistry - - -ayon_art = r""" - - ▄██▄ - ▄███▄ ▀██▄ ▀██▀ ▄██▀ ▄██▀▀▀██▄ ▀███▄ █▄ - ▄▄ ▀██▄ ▀██▄ ▄██▀ ██▀ ▀██▄ ▄ ▀██▄ ███ - ▄██▀ ██▄ ▀ ▄▄ ▀ ██ ▄██ ███ ▀██▄ ███ - ▄██▀ ▀██▄ ██ ▀██▄ ▄██▀ ███ ▀██ ▀█▀ - ▄██▀ ▀██▄ ▀█ ▀██▄▄▄▄██▀ █▀ ▀██▄ - - · · - =[ by YNPUT ]:[ http://ayon.ynput.io ]= - · · - -""" - - -class PythonInterpreterRegistry(JSONSettingRegistry): - """Class handling OpenPype general settings registry. - - Attributes: - vendor (str): Name used for path construction. - product (str): Additional name used for path construction. - - """ - - def __init__(self): - self.vendor = "Ynput" - self.product = "AYON" - name = "python_interpreter_tool" - path = appdirs.user_data_dir(self.product, self.vendor) - super(PythonInterpreterRegistry, self).__init__(name, path) - - -class StdOEWrap: - def __init__(self): - self._origin_stdout_write = None - self._origin_stderr_write = None - self._listening = False - self.lines = collections.deque() - - if not sys.stdout: - sys.stdout = open(os.devnull, "w") - - if not sys.stderr: - sys.stderr = open(os.devnull, "w") - - if self._origin_stdout_write is None: - self._origin_stdout_write = sys.stdout.write - - if self._origin_stderr_write is None: - self._origin_stderr_write = sys.stderr.write - - self._listening = True - sys.stdout.write = self._stdout_listener - sys.stderr.write = self._stderr_listener - - def stop_listen(self): - self._listening = False - - def _stdout_listener(self, text): - if self._listening: - self.lines.append(text) - if self._origin_stdout_write is not None: - self._origin_stdout_write(text) - - def _stderr_listener(self, text): - if self._listening: - self.lines.append(text) - if self._origin_stderr_write is not None: - self._origin_stderr_write(text) - - -class PythonCodeEditor(QtWidgets.QPlainTextEdit): - execute_requested = QtCore.Signal() - - def __init__(self, parent): - super(PythonCodeEditor, self).__init__(parent) - - self.setObjectName("PythonCodeEditor") - - self._indent = 4 - - def _tab_shift_right(self): - cursor = self.textCursor() - selected_text = cursor.selectedText() - if not selected_text: - cursor.insertText(" " * self._indent) - return - - sel_start = cursor.selectionStart() - sel_end = cursor.selectionEnd() - cursor.setPosition(sel_end) - end_line = cursor.blockNumber() - cursor.setPosition(sel_start) - while True: - cursor.movePosition(QtGui.QTextCursor.StartOfLine) - text = cursor.block().text() - spaces = len(text) - len(text.lstrip(" ")) - new_spaces = spaces % self._indent - if not new_spaces: - new_spaces = self._indent - - cursor.insertText(" " * new_spaces) - if cursor.blockNumber() == end_line: - break - - cursor.movePosition(QtGui.QTextCursor.NextBlock) - - def _tab_shift_left(self): - tmp_cursor = self.textCursor() - sel_start = tmp_cursor.selectionStart() - sel_end = tmp_cursor.selectionEnd() - - cursor = QtGui.QTextCursor(self.document()) - cursor.setPosition(sel_end) - end_line = cursor.blockNumber() - cursor.setPosition(sel_start) - while True: - cursor.movePosition(QtGui.QTextCursor.StartOfLine) - text = cursor.block().text() - spaces = len(text) - len(text.lstrip(" ")) - if spaces: - spaces_to_remove = (spaces % self._indent) or self._indent - if spaces_to_remove > spaces: - spaces_to_remove = spaces - - cursor.setPosition( - cursor.position() + spaces_to_remove, - QtGui.QTextCursor.KeepAnchor - ) - cursor.removeSelectedText() - - if cursor.blockNumber() == end_line: - break - - cursor.movePosition(QtGui.QTextCursor.NextBlock) - - def keyPressEvent(self, event): - if event.key() == QtCore.Qt.Key_Backtab: - self._tab_shift_left() - event.accept() - return - - if event.key() == QtCore.Qt.Key_Tab: - if event.modifiers() == QtCore.Qt.NoModifier: - self._tab_shift_right() - event.accept() - return - - if ( - event.key() == QtCore.Qt.Key_Return - and event.modifiers() == QtCore.Qt.ControlModifier - ): - self.execute_requested.emit() - event.accept() - return - - super(PythonCodeEditor, self).keyPressEvent(event) - - -class PythonTabWidget(QtWidgets.QWidget): - add_tab_requested = QtCore.Signal() - before_execute = QtCore.Signal(str) - - def __init__(self, parent): - super(PythonTabWidget, self).__init__(parent) - - code_input = PythonCodeEditor(self) - - self.setFocusProxy(code_input) - - add_tab_btn = QtWidgets.QPushButton("Add tab...", self) - add_tab_btn.setToolTip("Add new tab") - - execute_btn = QtWidgets.QPushButton("Execute", self) - execute_btn.setToolTip("Execute command (Ctrl + Enter)") - - btns_layout = QtWidgets.QHBoxLayout() - btns_layout.setContentsMargins(0, 0, 0, 0) - btns_layout.addWidget(add_tab_btn) - btns_layout.addStretch(1) - btns_layout.addWidget(execute_btn) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(code_input, 1) - layout.addLayout(btns_layout, 0) - - add_tab_btn.clicked.connect(self._on_add_tab_clicked) - execute_btn.clicked.connect(self._on_execute_clicked) - code_input.execute_requested.connect(self.execute) - - self._code_input = code_input - self._interpreter = InteractiveInterpreter() - - def _on_add_tab_clicked(self): - self.add_tab_requested.emit() - - def _on_execute_clicked(self): - self.execute() - - def get_code(self): - return self._code_input.toPlainText() - - def set_code(self, code_text): - self._code_input.setPlainText(code_text) - - def execute(self): - code_text = self._code_input.toPlainText() - self.before_execute.emit(code_text) - self._interpreter.runcode(code_text) - - -class TabNameDialog(QtWidgets.QDialog): - default_width = 330 - default_height = 85 - - def __init__(self, parent): - super(TabNameDialog, self).__init__(parent) - - self.setWindowTitle("Enter tab name") - - name_label = QtWidgets.QLabel("Tab name:", self) - name_input = QtWidgets.QLineEdit(self) - - inputs_layout = QtWidgets.QHBoxLayout() - inputs_layout.addWidget(name_label) - inputs_layout.addWidget(name_input) - - ok_btn = QtWidgets.QPushButton("Ok", self) - cancel_btn = QtWidgets.QPushButton("Cancel", self) - btns_layout = QtWidgets.QHBoxLayout() - btns_layout.addStretch(1) - btns_layout.addWidget(ok_btn) - btns_layout.addWidget(cancel_btn) - - layout = QtWidgets.QVBoxLayout(self) - layout.addLayout(inputs_layout) - layout.addStretch(1) - layout.addLayout(btns_layout) - - ok_btn.clicked.connect(self._on_ok_clicked) - cancel_btn.clicked.connect(self._on_cancel_clicked) - - self._name_input = name_input - self._ok_btn = ok_btn - self._cancel_btn = cancel_btn - - self._result = None - - self.resize(self.default_width, self.default_height) - - def set_tab_name(self, name): - self._name_input.setText(name) - - def result(self): - return self._result - - def showEvent(self, event): - super(TabNameDialog, self).showEvent(event) - btns_width = max( - self._ok_btn.width(), - self._cancel_btn.width() - ) - - self._ok_btn.setMinimumWidth(btns_width) - self._cancel_btn.setMinimumWidth(btns_width) - - def _on_ok_clicked(self): - self._result = self._name_input.text() - self.accept() - - def _on_cancel_clicked(self): - self._result = None - self.reject() - - -class OutputTextWidget(QtWidgets.QTextEdit): - v_max_offset = 4 - - def vertical_scroll_at_max(self): - v_scroll = self.verticalScrollBar() - return v_scroll.value() > v_scroll.maximum() - self.v_max_offset - - def scroll_to_bottom(self): - v_scroll = self.verticalScrollBar() - return v_scroll.setValue(v_scroll.maximum()) - - -class EnhancedTabBar(QtWidgets.QTabBar): - double_clicked = QtCore.Signal(QtCore.QPoint) - right_clicked = QtCore.Signal(QtCore.QPoint) - mid_clicked = QtCore.Signal(QtCore.QPoint) - - def __init__(self, parent): - super(EnhancedTabBar, self).__init__(parent) - - self.setDrawBase(False) - - def mouseDoubleClickEvent(self, event): - self.double_clicked.emit(event.globalPos()) - event.accept() - - def mouseReleaseEvent(self, event): - if event.button() == QtCore.Qt.RightButton: - self.right_clicked.emit(event.globalPos()) - event.accept() - return - - elif event.button() == QtCore.Qt.MidButton: - self.mid_clicked.emit(event.globalPos()) - event.accept() - - else: - super(EnhancedTabBar, self).mouseReleaseEvent(event) - - -class PythonInterpreterWidget(QtWidgets.QWidget): - default_width = 1000 - default_height = 600 - - def __init__(self, allow_save_registry=True, parent=None): - super(PythonInterpreterWidget, self).__init__(parent) - - self.setWindowTitle("AYON Console") - self.setWindowIcon(QtGui.QIcon(resources.get_ayon_icon_filepath())) - - self.ansi_escape = re.compile( - r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]" - ) - - self._tabs = [] - - self._stdout_err_wrapper = StdOEWrap() - - output_widget = OutputTextWidget(self) - output_widget.setObjectName("PythonInterpreterOutput") - output_widget.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) - output_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - - tab_widget = QtWidgets.QTabWidget(self) - tab_bar = EnhancedTabBar(tab_widget) - tab_widget.setTabBar(tab_bar) - tab_widget.setTabsClosable(False) - tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - - widgets_splitter = QtWidgets.QSplitter(self) - widgets_splitter.setOrientation(QtCore.Qt.Vertical) - widgets_splitter.addWidget(output_widget) - widgets_splitter.addWidget(tab_widget) - widgets_splitter.setStretchFactor(0, 1) - widgets_splitter.setStretchFactor(1, 1) - height = int(self.default_height / 2) - widgets_splitter.setSizes([height, self.default_height - height]) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(widgets_splitter) - - line_check_timer = QtCore.QTimer() - line_check_timer.setInterval(200) - - line_check_timer.timeout.connect(self._on_timer_timeout) - tab_bar.right_clicked.connect(self._on_tab_right_click) - tab_bar.double_clicked.connect(self._on_tab_double_click) - tab_bar.mid_clicked.connect(self._on_tab_mid_click) - tab_widget.tabCloseRequested.connect(self._on_tab_close_req) - - self._widgets_splitter = widgets_splitter - self._output_widget = output_widget - self._tab_widget = tab_widget - self._line_check_timer = line_check_timer - - self._append_lines([ayon_art]) - - self._first_show = True - self._splitter_size_ratio = None - self._allow_save_registry = allow_save_registry - self._registry_saved = True - - self._init_from_registry() - - if self._tab_widget.count() < 1: - self.add_tab("Python") - - def _init_from_registry(self): - setting_registry = PythonInterpreterRegistry() - width = None - height = None - try: - width = setting_registry.get_item("width") - height = setting_registry.get_item("height") - - except ValueError: - pass - - if width is None or width < 200: - width = self.default_width - - if height is None or height < 200: - height = self.default_height - - self.resize(width, height) - - try: - self._splitter_size_ratio = ( - setting_registry.get_item("splitter_sizes") - ) - - except ValueError: - pass - - try: - tab_defs = setting_registry.get_item("tabs") or [] - for tab_def in tab_defs: - widget = self.add_tab(tab_def["name"]) - widget.set_code(tab_def["code"]) - - except ValueError: - pass - - def save_registry(self): - # Window was not showed - if not self._allow_save_registry or self._registry_saved: - return - - self._registry_saved = True - setting_registry = PythonInterpreterRegistry() - - setting_registry.set_item("width", self.width()) - setting_registry.set_item("height", self.height()) - - setting_registry.set_item( - "splitter_sizes", self._widgets_splitter.sizes() - ) - - tabs = [] - for tab_idx in range(self._tab_widget.count()): - widget = self._tab_widget.widget(tab_idx) - tab_code = widget.get_code() - tab_name = self._tab_widget.tabText(tab_idx) - tabs.append({ - "name": tab_name, - "code": tab_code - }) - - setting_registry.set_item("tabs", tabs) - - def _on_tab_right_click(self, global_point): - point = self._tab_widget.mapFromGlobal(global_point) - tab_bar = self._tab_widget.tabBar() - tab_idx = tab_bar.tabAt(point) - last_index = tab_bar.count() - 1 - if tab_idx < 0 or tab_idx > last_index: - return - - menu = QtWidgets.QMenu(self._tab_widget) - - add_tab_action = QtWidgets.QAction("Add tab...", menu) - add_tab_action.setToolTip("Add new tab") - - rename_tab_action = QtWidgets.QAction("Rename...", menu) - rename_tab_action.setToolTip("Rename tab") - - duplicate_tab_action = QtWidgets.QAction("Duplicate...", menu) - duplicate_tab_action.setToolTip("Duplicate code to new tab") - - close_tab_action = QtWidgets.QAction("Close", menu) - close_tab_action.setToolTip("Close tab and lose content") - close_tab_action.setEnabled(self._tab_widget.tabsClosable()) - - menu.addAction(add_tab_action) - menu.addAction(rename_tab_action) - menu.addAction(duplicate_tab_action) - menu.addAction(close_tab_action) - - result = menu.exec_(global_point) - if result is None: - return - - if result is rename_tab_action: - self._rename_tab_req(tab_idx) - - elif result is add_tab_action: - self._on_add_requested() - - elif result is duplicate_tab_action: - self._duplicate_requested(tab_idx) - - elif result is close_tab_action: - self._on_tab_close_req(tab_idx) - - def _rename_tab_req(self, tab_idx): - dialog = TabNameDialog(self) - dialog.set_tab_name(self._tab_widget.tabText(tab_idx)) - dialog.exec_() - tab_name = dialog.result() - if tab_name: - self._tab_widget.setTabText(tab_idx, tab_name) - - def _duplicate_requested(self, tab_idx=None): - if tab_idx is None: - tab_idx = self._tab_widget.currentIndex() - - src_widget = self._tab_widget.widget(tab_idx) - dst_widget = self._add_tab() - if dst_widget is None: - return - dst_widget.set_code(src_widget.get_code()) - - def _on_tab_mid_click(self, global_point): - point = self._tab_widget.mapFromGlobal(global_point) - tab_bar = self._tab_widget.tabBar() - tab_idx = tab_bar.tabAt(point) - last_index = tab_bar.count() - 1 - if tab_idx < 0 or tab_idx > last_index: - return - - self._on_tab_close_req(tab_idx) - - def _on_tab_double_click(self, global_point): - point = self._tab_widget.mapFromGlobal(global_point) - tab_bar = self._tab_widget.tabBar() - tab_idx = tab_bar.tabAt(point) - last_index = tab_bar.count() - 1 - if tab_idx < 0 or tab_idx > last_index: - return - - self._rename_tab_req(tab_idx) - - def _on_tab_close_req(self, tab_index): - if self._tab_widget.count() == 1: - return - - widget = self._tab_widget.widget(tab_index) - if widget in self._tabs: - self._tabs.remove(widget) - self._tab_widget.removeTab(tab_index) - - if self._tab_widget.count() == 1: - self._tab_widget.setTabsClosable(False) - - def _append_lines(self, lines): - at_max = self._output_widget.vertical_scroll_at_max() - tmp_cursor = QtGui.QTextCursor(self._output_widget.document()) - tmp_cursor.movePosition(QtGui.QTextCursor.End) - for line in lines: - tmp_cursor.insertText(line) - - if at_max: - self._output_widget.scroll_to_bottom() - - def _on_timer_timeout(self): - if self._stdout_err_wrapper.lines: - lines = [] - while self._stdout_err_wrapper.lines: - line = self._stdout_err_wrapper.lines.popleft() - lines.append(self.ansi_escape.sub("", line)) - self._append_lines(lines) - - def _on_add_requested(self): - self._add_tab() - - def _add_tab(self): - dialog = TabNameDialog(self) - dialog.exec_() - tab_name = dialog.result() - if tab_name: - return self.add_tab(tab_name) - - return None - - def _on_before_execute(self, code_text): - at_max = self._output_widget.vertical_scroll_at_max() - document = self._output_widget.document() - tmp_cursor = QtGui.QTextCursor(document) - tmp_cursor.movePosition(QtGui.QTextCursor.End) - tmp_cursor.insertText("{}\nExecuting command:\n".format(20 * "-")) - - code_block_format = QtGui.QTextFrameFormat() - code_block_format.setBackground(QtGui.QColor(27, 27, 27)) - code_block_format.setPadding(4) - - tmp_cursor.insertFrame(code_block_format) - char_format = tmp_cursor.charFormat() - char_format.setForeground( - QtGui.QBrush(QtGui.QColor(114, 224, 198)) - ) - tmp_cursor.setCharFormat(char_format) - tmp_cursor.insertText(code_text) - - # Create new cursor - tmp_cursor = QtGui.QTextCursor(document) - tmp_cursor.movePosition(QtGui.QTextCursor.End) - tmp_cursor.insertText("{}\n".format(20 * "-")) - - if at_max: - self._output_widget.scroll_to_bottom() - - def add_tab(self, tab_name, index=None): - widget = PythonTabWidget(self) - widget.before_execute.connect(self._on_before_execute) - widget.add_tab_requested.connect(self._on_add_requested) - if index is None: - if self._tab_widget.count() > 0: - index = self._tab_widget.currentIndex() + 1 - else: - index = 0 - - self._tabs.append(widget) - self._tab_widget.insertTab(index, widget, tab_name) - self._tab_widget.setCurrentIndex(index) - - if self._tab_widget.count() > 1: - self._tab_widget.setTabsClosable(True) - widget.setFocus() - return widget - - def showEvent(self, event): - self._line_check_timer.start() - self._registry_saved = False - super(PythonInterpreterWidget, self).showEvent(event) - # First show setup - if self._first_show: - self._first_show = False - self._on_first_show() - - self._output_widget.scroll_to_bottom() - - def _on_first_show(self): - # Change stylesheet - self.setStyleSheet(load_stylesheet()) - # Check if splitter size ratio is set - # - first store value to local variable and then unset it - splitter_size_ratio = self._splitter_size_ratio - self._splitter_size_ratio = None - # Skip if is not set - if not splitter_size_ratio: - return - - # Skip if number of size items does not match to splitter - splitters_count = len(self._widgets_splitter.sizes()) - if len(splitter_size_ratio) == splitters_count: - self._widgets_splitter.setSizes(splitter_size_ratio) - - def closeEvent(self, event): - self.save_registry() - super(PythonInterpreterWidget, self).closeEvent(event) - self._line_check_timer.stop() diff --git a/client/ayon_core/pipeline/tempdir.py b/client/ayon_core/pipeline/tempdir.py index fe057b7fc7..38b03f5c85 100644 --- a/client/ayon_core/pipeline/tempdir.py +++ b/client/ayon_core/pipeline/tempdir.py @@ -5,6 +5,7 @@ Temporary folder operations import os import tempfile from pathlib import Path +import warnings from ayon_core.lib import StringTemplate from ayon_core.pipeline import Anatomy @@ -70,6 +71,21 @@ def _create_local_staging_dir(prefix, suffix, dirpath=None): ) +def create_custom_tempdir(project_name, anatomy=None): + """Backward compatibility deprecated since 2024/12/09. + """ + warnings.warn( + "Used deprecated 'create_custom_tempdir' " + "use 'ayon_core.pipeline.tempdir.get_temp_dir' instead.", + DeprecationWarning, + ) + + if anatomy is None: + anatomy = Anatomy(project_name) + + return _create_custom_tempdir(project_name, anatomy) + + def _create_custom_tempdir(project_name, anatomy): """ Create custom tempdir diff --git a/client/ayon_core/pipeline/workfile/path_resolving.py b/client/ayon_core/pipeline/workfile/path_resolving.py index 47d6f4ddfa..61c6e5b876 100644 --- a/client/ayon_core/pipeline/workfile/path_resolving.py +++ b/client/ayon_core/pipeline/workfile/path_resolving.py @@ -2,6 +2,7 @@ import os import re import copy import platform +from typing import Optional, Dict, Any import ayon_api @@ -16,12 +17,12 @@ from ayon_core.pipeline.template_data import get_template_data def get_workfile_template_key_from_context( - project_name, - folder_path, - task_name, - host_name, - project_settings=None -): + project_name: str, + folder_path: str, + task_name: str, + host_name: str, + project_settings: Optional[Dict[str, Any]] = None, +) -> str: """Helper function to get template key for workfile template. Do the same as `get_workfile_template_key` but returns value for "session @@ -34,15 +35,23 @@ def get_workfile_template_key_from_context( host_name (str): Host name. project_settings (Dict[str, Any]): Project settings for passed 'project_name'. Not required at all but makes function faster. - """ + Returns: + str: Workfile template name. + + """ folder_entity = ayon_api.get_folder_by_path( - project_name, folder_path, fields={"id"} + project_name, + folder_path, + fields={"id"}, ) task_entity = ayon_api.get_task_by_name( - project_name, folder_entity["id"], task_name + project_name, + folder_entity["id"], + task_name, + fields={"taskType"}, ) - task_type = task_entity.get("type") + task_type = task_entity.get("taskType") return get_workfile_template_key( project_name, task_type, host_name, project_settings diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index c8d2086865..712ae7a886 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -71,15 +71,16 @@ class ExtractOTIOReview( # TODO: convert resulting image sequence to mp4 # get otio clip and other time info from instance clip - # TODO: what if handles are different in `versionData`? - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] otio_review_clips = instance.data.get("otioReviewClips") if otio_review_clips is None: self.log.info(f"Instance `{instance}` has no otioReviewClips") return + # TODO: what if handles are different in `versionData`? + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] + # add plugin wide attributes self.representation_files = [] self.used_frames = [] diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 37bbac8898..8ae18f4abf 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -37,7 +37,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "substancepainter", "nuke", "aftereffects", - "unreal" + "unreal", + "houdini" ] enabled = False diff --git a/client/ayon_core/tools/attribute_defs/files_widget.py b/client/ayon_core/tools/attribute_defs/files_widget.py index 46399c5fce..8a40b3ff38 100644 --- a/client/ayon_core/tools/attribute_defs/files_widget.py +++ b/client/ayon_core/tools/attribute_defs/files_widget.py @@ -254,7 +254,7 @@ class FilesModel(QtGui.QStandardItemModel): """Make sure that removed items are removed from items mapping. Connected with '_on_insert'. When user drag item and drop it to same - view the item is actually removed and creted again but it happens in + view the item is actually removed and created again but it happens in inner calls of Qt. """ @@ -841,7 +841,7 @@ class FilesWidget(QtWidgets.QFrame): self._multivalue ) widget.context_menu_requested.connect( - self._on_context_menu_requested + self._on_item_context_menu_request ) self._files_view.setIndexWidget(index, widget) self._files_proxy_model.setData( @@ -859,7 +859,7 @@ class FilesWidget(QtWidgets.QFrame): for row in range(self._files_proxy_model.rowCount()): index = self._files_proxy_model.index(row, 0) item_id = index.data(ITEM_ID_ROLE) - available_item_ids.add(index.data(ITEM_ID_ROLE)) + available_item_ids.add(item_id) widget_ids = set(self._widgets_by_id.keys()) for item_id in available_item_ids: @@ -923,6 +923,9 @@ class FilesWidget(QtWidgets.QFrame): if menu.actions(): menu.popup(pos) + def _on_item_context_menu_request(self, pos): + self._on_context_menu_requested(pos, True) + def dragEnterEvent(self, event): if self._multivalue: return diff --git a/client/ayon_core/tools/console_interpreter/__init__.py b/client/ayon_core/tools/console_interpreter/__init__.py new file mode 100644 index 0000000000..0333fe80a0 --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/__init__.py @@ -0,0 +1,8 @@ +from .abstract import AbstractInterpreterController +from .control import InterpreterController + + +__all__ = ( + "AbstractInterpreterController", + "InterpreterController", +) diff --git a/client/ayon_core/tools/console_interpreter/abstract.py b/client/ayon_core/tools/console_interpreter/abstract.py new file mode 100644 index 0000000000..a945e6e498 --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/abstract.py @@ -0,0 +1,33 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import List, Dict, Optional + + +@dataclass +class TabItem: + name: str + code: str + + +@dataclass +class InterpreterConfig: + width: Optional[int] + height: Optional[int] + splitter_sizes: List[int] = field(default_factory=list) + tabs: List[TabItem] = field(default_factory=list) + + +class AbstractInterpreterController(ABC): + @abstractmethod + def get_config(self) -> InterpreterConfig: + pass + + @abstractmethod + def save_config( + self, + width: int, + height: int, + splitter_sizes: List[int], + tabs: List[Dict[str, str]], + ): + pass diff --git a/client/ayon_core/tools/console_interpreter/control.py b/client/ayon_core/tools/console_interpreter/control.py new file mode 100644 index 0000000000..b931b6252c --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/control.py @@ -0,0 +1,63 @@ +from typing import List, Dict + +from ayon_core.lib import JSONSettingRegistry +from ayon_core.lib.local_settings import get_launcher_local_dir + +from .abstract import ( + AbstractInterpreterController, + TabItem, + InterpreterConfig, +) + + +class InterpreterController(AbstractInterpreterController): + def __init__(self): + self._registry = JSONSettingRegistry( + "python_interpreter_tool", + get_launcher_local_dir(), + ) + + def get_config(self): + width = None + height = None + splitter_sizes = [] + tabs = [] + try: + width = self._registry.get_item("width") + height = self._registry.get_item("height") + + except (ValueError, KeyError): + pass + + try: + splitter_sizes = self._registry.get_item("splitter_sizes") + except (ValueError, KeyError): + pass + + try: + tab_defs = self._registry.get_item("tabs") or [] + for tab_def in tab_defs: + tab_name = tab_def.get("name") + if not tab_name: + continue + code = tab_def.get("code") or "" + tabs.append(TabItem(tab_name, code)) + + except (ValueError, KeyError): + pass + + return InterpreterConfig( + width, height, splitter_sizes, tabs + ) + + def save_config( + self, + width: int, + height: int, + splitter_sizes: List[int], + tabs: List[Dict[str, str]], + ): + self._registry.set_item("width", width) + self._registry.set_item("height", height) + self._registry.set_item("splitter_sizes", splitter_sizes) + self._registry.set_item("tabs", tabs) diff --git a/client/ayon_core/tools/console_interpreter/ui/__init__.py b/client/ayon_core/tools/console_interpreter/ui/__init__.py new file mode 100644 index 0000000000..05b166892c --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/ui/__init__.py @@ -0,0 +1,8 @@ +from .window import ( + ConsoleInterpreterWindow +) + + +__all__ = ( + "ConsoleInterpreterWindow", +) diff --git a/client/ayon_core/tools/console_interpreter/ui/utils.py b/client/ayon_core/tools/console_interpreter/ui/utils.py new file mode 100644 index 0000000000..427483215d --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/ui/utils.py @@ -0,0 +1,42 @@ +import os +import sys +import collections + + +class StdOEWrap: + def __init__(self): + self._origin_stdout_write = None + self._origin_stderr_write = None + self._listening = False + self.lines = collections.deque() + + if not sys.stdout: + sys.stdout = open(os.devnull, "w") + + if not sys.stderr: + sys.stderr = open(os.devnull, "w") + + if self._origin_stdout_write is None: + self._origin_stdout_write = sys.stdout.write + + if self._origin_stderr_write is None: + self._origin_stderr_write = sys.stderr.write + + self._listening = True + sys.stdout.write = self._stdout_listener + sys.stderr.write = self._stderr_listener + + def stop_listen(self): + self._listening = False + + def _stdout_listener(self, text): + if self._listening: + self.lines.append(text) + if self._origin_stdout_write is not None: + self._origin_stdout_write(text) + + def _stderr_listener(self, text): + if self._listening: + self.lines.append(text) + if self._origin_stderr_write is not None: + self._origin_stderr_write(text) diff --git a/client/ayon_core/tools/console_interpreter/ui/widgets.py b/client/ayon_core/tools/console_interpreter/ui/widgets.py new file mode 100644 index 0000000000..2b9361666e --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/ui/widgets.py @@ -0,0 +1,251 @@ +from code import InteractiveInterpreter + +from qtpy import QtCore, QtWidgets, QtGui + + +class PythonCodeEditor(QtWidgets.QPlainTextEdit): + execute_requested = QtCore.Signal() + + def __init__(self, parent): + super().__init__(parent) + + self.setObjectName("PythonCodeEditor") + + self._indent = 4 + + def _tab_shift_right(self): + cursor = self.textCursor() + selected_text = cursor.selectedText() + if not selected_text: + cursor.insertText(" " * self._indent) + return + + sel_start = cursor.selectionStart() + sel_end = cursor.selectionEnd() + cursor.setPosition(sel_end) + end_line = cursor.blockNumber() + cursor.setPosition(sel_start) + while True: + cursor.movePosition(QtGui.QTextCursor.StartOfLine) + text = cursor.block().text() + spaces = len(text) - len(text.lstrip(" ")) + new_spaces = spaces % self._indent + if not new_spaces: + new_spaces = self._indent + + cursor.insertText(" " * new_spaces) + if cursor.blockNumber() == end_line: + break + + cursor.movePosition(QtGui.QTextCursor.NextBlock) + + def _tab_shift_left(self): + tmp_cursor = self.textCursor() + sel_start = tmp_cursor.selectionStart() + sel_end = tmp_cursor.selectionEnd() + + cursor = QtGui.QTextCursor(self.document()) + cursor.setPosition(sel_end) + end_line = cursor.blockNumber() + cursor.setPosition(sel_start) + while True: + cursor.movePosition(QtGui.QTextCursor.StartOfLine) + text = cursor.block().text() + spaces = len(text) - len(text.lstrip(" ")) + if spaces: + spaces_to_remove = (spaces % self._indent) or self._indent + if spaces_to_remove > spaces: + spaces_to_remove = spaces + + cursor.setPosition( + cursor.position() + spaces_to_remove, + QtGui.QTextCursor.KeepAnchor + ) + cursor.removeSelectedText() + + if cursor.blockNumber() == end_line: + break + + cursor.movePosition(QtGui.QTextCursor.NextBlock) + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Backtab: + self._tab_shift_left() + event.accept() + return + + if event.key() == QtCore.Qt.Key_Tab: + if event.modifiers() == QtCore.Qt.NoModifier: + self._tab_shift_right() + event.accept() + return + + if ( + event.key() == QtCore.Qt.Key_Return + and event.modifiers() == QtCore.Qt.ControlModifier + ): + self.execute_requested.emit() + event.accept() + return + + super().keyPressEvent(event) + + +class PythonTabWidget(QtWidgets.QWidget): + add_tab_requested = QtCore.Signal() + before_execute = QtCore.Signal(str) + + def __init__(self, parent): + super().__init__(parent) + + code_input = PythonCodeEditor(self) + + self.setFocusProxy(code_input) + + add_tab_btn = QtWidgets.QPushButton("Add tab...", self) + add_tab_btn.setDefault(False) + add_tab_btn.setToolTip("Add new tab") + + execute_btn = QtWidgets.QPushButton("Execute", self) + execute_btn.setDefault(False) + execute_btn.setToolTip("Execute command (Ctrl + Enter)") + + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addWidget(add_tab_btn) + btns_layout.addStretch(1) + btns_layout.addWidget(execute_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(code_input, 1) + layout.addLayout(btns_layout, 0) + + add_tab_btn.clicked.connect(self._on_add_tab_clicked) + execute_btn.clicked.connect(self._on_execute_clicked) + code_input.execute_requested.connect(self.execute) + + self._code_input = code_input + self._interpreter = InteractiveInterpreter() + + def _on_add_tab_clicked(self): + self.add_tab_requested.emit() + + def _on_execute_clicked(self): + self.execute() + + def get_code(self): + return self._code_input.toPlainText() + + def set_code(self, code_text): + self._code_input.setPlainText(code_text) + + def execute(self): + code_text = self._code_input.toPlainText() + self.before_execute.emit(code_text) + self._interpreter.runcode(code_text) + + +class TabNameDialog(QtWidgets.QDialog): + default_width = 330 + default_height = 85 + + def __init__(self, parent): + super().__init__(parent) + + self.setWindowTitle("Enter tab name") + + name_label = QtWidgets.QLabel("Tab name:", self) + name_input = QtWidgets.QLineEdit(self) + + inputs_layout = QtWidgets.QHBoxLayout() + inputs_layout.addWidget(name_label) + inputs_layout.addWidget(name_input) + + ok_btn = QtWidgets.QPushButton("Ok", self) + cancel_btn = QtWidgets.QPushButton("Cancel", self) + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn) + btns_layout.addWidget(cancel_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.addLayout(inputs_layout) + layout.addStretch(1) + layout.addLayout(btns_layout) + + ok_btn.clicked.connect(self._on_ok_clicked) + cancel_btn.clicked.connect(self._on_cancel_clicked) + + self._name_input = name_input + self._ok_btn = ok_btn + self._cancel_btn = cancel_btn + + self._result = None + + self.resize(self.default_width, self.default_height) + + def set_tab_name(self, name): + self._name_input.setText(name) + + def result(self): + return self._result + + def showEvent(self, event): + super().showEvent(event) + btns_width = max( + self._ok_btn.width(), + self._cancel_btn.width() + ) + + self._ok_btn.setMinimumWidth(btns_width) + self._cancel_btn.setMinimumWidth(btns_width) + + def _on_ok_clicked(self): + self._result = self._name_input.text() + self.accept() + + def _on_cancel_clicked(self): + self._result = None + self.reject() + + +class OutputTextWidget(QtWidgets.QTextEdit): + v_max_offset = 4 + + def vertical_scroll_at_max(self): + v_scroll = self.verticalScrollBar() + return v_scroll.value() > v_scroll.maximum() - self.v_max_offset + + def scroll_to_bottom(self): + v_scroll = self.verticalScrollBar() + return v_scroll.setValue(v_scroll.maximum()) + + +class EnhancedTabBar(QtWidgets.QTabBar): + double_clicked = QtCore.Signal(QtCore.QPoint) + right_clicked = QtCore.Signal(QtCore.QPoint) + mid_clicked = QtCore.Signal(QtCore.QPoint) + + def __init__(self, parent): + super().__init__(parent) + + self.setDrawBase(False) + + def mouseDoubleClickEvent(self, event): + self.double_clicked.emit(event.globalPos()) + event.accept() + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + self.right_clicked.emit(event.globalPos()) + event.accept() + return + + elif event.button() == QtCore.Qt.MidButton: + self.mid_clicked.emit(event.globalPos()) + event.accept() + + else: + super().mouseReleaseEvent(event) + diff --git a/client/ayon_core/tools/console_interpreter/ui/window.py b/client/ayon_core/tools/console_interpreter/ui/window.py new file mode 100644 index 0000000000..a5065f96f9 --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/ui/window.py @@ -0,0 +1,324 @@ +import re +from typing import Optional + +from qtpy import QtWidgets, QtGui, QtCore + +from ayon_core import resources +from ayon_core.style import load_stylesheet +from ayon_core.tools.console_interpreter import ( + AbstractInterpreterController, + InterpreterController, +) + +from .utils import StdOEWrap +from .widgets import ( + PythonTabWidget, + OutputTextWidget, + EnhancedTabBar, + TabNameDialog, +) + +ANSI_ESCAPE = re.compile( + r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]" +) +AYON_ART = r""" + + ▄██▄ + ▄███▄ ▀██▄ ▀██▀ ▄██▀ ▄██▀▀▀██▄ ▀███▄ █▄ + ▄▄ ▀██▄ ▀██▄ ▄██▀ ██▀ ▀██▄ ▄ ▀██▄ ███ + ▄██▀ ██▄ ▀ ▄▄ ▀ ██ ▄██ ███ ▀██▄ ███ + ▄██▀ ▀██▄ ██ ▀██▄ ▄██▀ ███ ▀██ ▀█▀ + ▄██▀ ▀██▄ ▀█ ▀██▄▄▄▄██▀ █▀ ▀██▄ + + · · - =[ by YNPUT ]:[ http://ayon.ynput.io ]= - · · + +""" + + +class ConsoleInterpreterWindow(QtWidgets.QWidget): + default_width = 1000 + default_height = 600 + + def __init__( + self, + controller: Optional[AbstractInterpreterController] = None, + parent: Optional[QtWidgets.QWidget] = None, + ): + super().__init__(parent) + + self.setWindowTitle("AYON Console") + self.setWindowIcon(QtGui.QIcon(resources.get_ayon_icon_filepath())) + + if controller is None: + controller = InterpreterController() + + output_widget = OutputTextWidget(self) + output_widget.setObjectName("PythonInterpreterOutput") + output_widget.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) + output_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + + tab_widget = QtWidgets.QTabWidget(self) + tab_bar = EnhancedTabBar(tab_widget) + tab_widget.setTabBar(tab_bar) + tab_widget.setTabsClosable(False) + tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + widgets_splitter = QtWidgets.QSplitter(self) + widgets_splitter.setOrientation(QtCore.Qt.Vertical) + widgets_splitter.addWidget(output_widget) + widgets_splitter.addWidget(tab_widget) + widgets_splitter.setStretchFactor(0, 1) + widgets_splitter.setStretchFactor(1, 1) + height = int(self.default_height / 2) + widgets_splitter.setSizes([height, self.default_height - height]) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(widgets_splitter) + + line_check_timer = QtCore.QTimer() + line_check_timer.setInterval(200) + + line_check_timer.timeout.connect(self._on_timer_timeout) + tab_bar.right_clicked.connect(self._on_tab_right_click) + tab_bar.double_clicked.connect(self._on_tab_double_click) + tab_bar.mid_clicked.connect(self._on_tab_mid_click) + tab_widget.tabCloseRequested.connect(self._on_tab_close_req) + + self._tabs = [] + + self._stdout_err_wrapper = StdOEWrap() + + self._widgets_splitter = widgets_splitter + self._output_widget = output_widget + self._tab_widget = tab_widget + self._line_check_timer = line_check_timer + + self._append_lines([AYON_ART]) + + self._first_show = True + self._controller = controller + + def showEvent(self, event): + self._line_check_timer.start() + super().showEvent(event) + # First show setup + if self._first_show: + self._first_show = False + self._on_first_show() + + if self._tab_widget.count() < 1: + self.add_tab("Python") + + self._output_widget.scroll_to_bottom() + + def closeEvent(self, event): + self._save_registry() + super().closeEvent(event) + self._line_check_timer.stop() + + def add_tab(self, tab_name, index=None): + widget = PythonTabWidget(self) + widget.before_execute.connect(self._on_before_execute) + widget.add_tab_requested.connect(self._on_add_requested) + if index is None: + if self._tab_widget.count() > 0: + index = self._tab_widget.currentIndex() + 1 + else: + index = 0 + + self._tabs.append(widget) + self._tab_widget.insertTab(index, widget, tab_name) + self._tab_widget.setCurrentIndex(index) + + if self._tab_widget.count() > 1: + self._tab_widget.setTabsClosable(True) + widget.setFocus() + return widget + + def _on_first_show(self): + config = self._controller.get_config() + width = config.width + height = config.height + if width is None or width < 200: + width = self.default_width + if height is None or height < 200: + height = self.default_height + + for tab_item in config.tabs: + widget = self.add_tab(tab_item.name) + widget.set_code(tab_item.code) + + self.resize(width, height) + # Change stylesheet + self.setStyleSheet(load_stylesheet()) + # Check if splitter sizes are set + splitters_count = len(self._widgets_splitter.sizes()) + if len(config.splitter_sizes) == splitters_count: + self._widgets_splitter.setSizes(config.splitter_sizes) + + def _save_registry(self): + tabs = [] + for tab_idx in range(self._tab_widget.count()): + widget = self._tab_widget.widget(tab_idx) + tabs.append({ + "name": self._tab_widget.tabText(tab_idx), + "code": widget.get_code() + }) + + self._controller.save_config( + self.width(), + self.height(), + self._widgets_splitter.sizes(), + tabs + ) + + def _on_tab_right_click(self, global_point): + point = self._tab_widget.mapFromGlobal(global_point) + tab_bar = self._tab_widget.tabBar() + tab_idx = tab_bar.tabAt(point) + last_index = tab_bar.count() - 1 + if tab_idx < 0 or tab_idx > last_index: + return + + menu = QtWidgets.QMenu(self._tab_widget) + + add_tab_action = QtWidgets.QAction("Add tab...", menu) + add_tab_action.setToolTip("Add new tab") + + rename_tab_action = QtWidgets.QAction("Rename...", menu) + rename_tab_action.setToolTip("Rename tab") + + duplicate_tab_action = QtWidgets.QAction("Duplicate...", menu) + duplicate_tab_action.setToolTip("Duplicate code to new tab") + + close_tab_action = QtWidgets.QAction("Close", menu) + close_tab_action.setToolTip("Close tab and lose content") + close_tab_action.setEnabled(self._tab_widget.tabsClosable()) + + menu.addAction(add_tab_action) + menu.addAction(rename_tab_action) + menu.addAction(duplicate_tab_action) + menu.addAction(close_tab_action) + + result = menu.exec_(global_point) + if result is None: + return + + if result is rename_tab_action: + self._rename_tab_req(tab_idx) + + elif result is add_tab_action: + self._on_add_requested() + + elif result is duplicate_tab_action: + self._duplicate_requested(tab_idx) + + elif result is close_tab_action: + self._on_tab_close_req(tab_idx) + + def _rename_tab_req(self, tab_idx): + dialog = TabNameDialog(self) + dialog.set_tab_name(self._tab_widget.tabText(tab_idx)) + dialog.exec_() + tab_name = dialog.result() + if tab_name: + self._tab_widget.setTabText(tab_idx, tab_name) + + def _duplicate_requested(self, tab_idx=None): + if tab_idx is None: + tab_idx = self._tab_widget.currentIndex() + + src_widget = self._tab_widget.widget(tab_idx) + dst_widget = self._add_tab() + if dst_widget is None: + return + dst_widget.set_code(src_widget.get_code()) + + def _on_tab_mid_click(self, global_point): + point = self._tab_widget.mapFromGlobal(global_point) + tab_bar = self._tab_widget.tabBar() + tab_idx = tab_bar.tabAt(point) + last_index = tab_bar.count() - 1 + if tab_idx < 0 or tab_idx > last_index: + return + + self._on_tab_close_req(tab_idx) + + def _on_tab_double_click(self, global_point): + point = self._tab_widget.mapFromGlobal(global_point) + tab_bar = self._tab_widget.tabBar() + tab_idx = tab_bar.tabAt(point) + last_index = tab_bar.count() - 1 + if tab_idx < 0 or tab_idx > last_index: + return + + self._rename_tab_req(tab_idx) + + def _on_tab_close_req(self, tab_index): + if self._tab_widget.count() == 1: + return + + widget = self._tab_widget.widget(tab_index) + if widget in self._tabs: + self._tabs.remove(widget) + self._tab_widget.removeTab(tab_index) + + if self._tab_widget.count() == 1: + self._tab_widget.setTabsClosable(False) + + def _append_lines(self, lines): + at_max = self._output_widget.vertical_scroll_at_max() + tmp_cursor = QtGui.QTextCursor(self._output_widget.document()) + tmp_cursor.movePosition(QtGui.QTextCursor.End) + for line in lines: + tmp_cursor.insertText(line) + + if at_max: + self._output_widget.scroll_to_bottom() + + def _on_timer_timeout(self): + if self._stdout_err_wrapper.lines: + lines = [] + while self._stdout_err_wrapper.lines: + line = self._stdout_err_wrapper.lines.popleft() + lines.append(ANSI_ESCAPE.sub("", line)) + self._append_lines(lines) + + def _on_add_requested(self): + self._add_tab() + + def _add_tab(self): + dialog = TabNameDialog(self) + dialog.exec_() + tab_name = dialog.result() + if tab_name: + return self.add_tab(tab_name) + + return None + + def _on_before_execute(self, code_text): + at_max = self._output_widget.vertical_scroll_at_max() + document = self._output_widget.document() + tmp_cursor = QtGui.QTextCursor(document) + tmp_cursor.movePosition(QtGui.QTextCursor.End) + tmp_cursor.insertText("{}\nExecuting command:\n".format(20 * "-")) + + code_block_format = QtGui.QTextFrameFormat() + code_block_format.setBackground(QtGui.QColor(27, 27, 27)) + code_block_format.setPadding(4) + + tmp_cursor.insertFrame(code_block_format) + char_format = tmp_cursor.charFormat() + char_format.setForeground( + QtGui.QBrush(QtGui.QColor(114, 224, 198)) + ) + tmp_cursor.setCharFormat(char_format) + tmp_cursor.insertText(code_text) + + # Create new cursor + tmp_cursor = QtGui.QTextCursor(document) + tmp_cursor.movePosition(QtGui.QTextCursor.End) + tmp_cursor.insertText("{}\n".format(20 * "-")) + + if at_max: + self._output_widget.scroll_to_bottom() diff --git a/client/ayon_core/tools/publisher/publish_report_viewer/window.py b/client/ayon_core/tools/publisher/publish_report_viewer/window.py index 6921c5d162..77db65588a 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/window.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/window.py @@ -484,6 +484,34 @@ class LoadedFilesView(QtWidgets.QTreeView): self._time_delegate = time_delegate self._remove_btn = remove_btn + def showEvent(self, event): + super().showEvent(event) + self._model.refresh() + header = self.header() + header.resizeSections(QtWidgets.QHeaderView.ResizeToContents) + self._update_remove_btn() + + def resizeEvent(self, event): + super().resizeEvent(event) + self._update_remove_btn() + + def add_filepaths(self, filepaths): + self._model.add_filepaths(filepaths) + self._fill_selection() + + def remove_item_by_id(self, item_id): + self._model.remove_item_by_id(item_id) + self._fill_selection() + + def get_current_report(self): + index = self.currentIndex() + item_id = index.data(ITEM_ID_ROLE) + return self._model.get_report_by_id(item_id) + + def refresh(self): + self._model.refresh() + self._fill_selection() + def _update_remove_btn(self): viewport = self.viewport() height = viewport.height() + self.header().height() @@ -496,28 +524,9 @@ class LoadedFilesView(QtWidgets.QTreeView): header.resizeSections(QtWidgets.QHeaderView.ResizeToContents) self._update_remove_btn() - def resizeEvent(self, event): - super().resizeEvent(event) - self._update_remove_btn() - - def showEvent(self, event): - super().showEvent(event) - self._model.refresh() - header = self.header() - header.resizeSections(QtWidgets.QHeaderView.ResizeToContents) - self._update_remove_btn() - def _on_selection_change(self): self.selection_changed.emit() - def add_filepaths(self, filepaths): - self._model.add_filepaths(filepaths) - self._fill_selection() - - def remove_item_by_id(self, item_id): - self._model.remove_item_by_id(item_id) - self._fill_selection() - def _on_remove_clicked(self): index = self.currentIndex() item_id = index.data(ITEM_ID_ROLE) @@ -533,11 +542,6 @@ class LoadedFilesView(QtWidgets.QTreeView): if index.isValid(): self.setCurrentIndex(index) - def get_current_report(self): - index = self.currentIndex() - item_id = index.data(ITEM_ID_ROLE) - return self._model.get_report_by_id(item_id) - class LoadedFilesWidget(QtWidgets.QWidget): report_changed = QtCore.Signal() @@ -577,15 +581,18 @@ class LoadedFilesWidget(QtWidgets.QWidget): self._add_filepaths(filepaths) event.accept() + def refresh(self): + self._view.refresh() + + def get_current_report(self): + return self._view.get_current_report() + def _on_report_change(self): self.report_changed.emit() def _add_filepaths(self, filepaths): self._view.add_filepaths(filepaths) - def get_current_report(self): - return self._view.get_current_report() - class PublishReportViewerWindow(QtWidgets.QWidget): default_width = 1200 @@ -624,9 +631,12 @@ class PublishReportViewerWindow(QtWidgets.QWidget): self.resize(self.default_width, self.default_height) self.setStyleSheet(style.load_stylesheet()) - def _on_report_change(self): - report = self._loaded_files_widget.get_current_report() - self.set_report(report) + def refresh(self): + self._loaded_files_widget.refresh() def set_report(self, report_data): self._main_widget.set_report(report_data) + + def _on_report_change(self): + report = self._loaded_files_widget.get_current_report() + self.set_report(report) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 58447a8389..fb080d158b 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -321,7 +321,7 @@ class PushToContextController: return False if ( - not self._user_values.new_folder_name + self._user_values.new_folder_name is None and not self._selection_model.get_selected_folder_id() ): return False diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index ba603699bc..6bd4279219 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -26,7 +26,7 @@ from ayon_core.pipeline import Anatomy from ayon_core.pipeline.version_start import get_versioning_start from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.publish import get_publish_template_name -from ayon_core.pipeline.create import get_product_name +from ayon_core.pipeline.create import get_product_name, TaskNotSetError UNKNOWN = object() @@ -823,15 +823,25 @@ class ProjectPushItemProcess: task_name = task_info["name"] task_type = task_info["taskType"] - product_name = get_product_name( - self._item.dst_project_name, - task_name, - task_type, - self.host_name, - product_type, - self._item.variant, - project_settings=self._project_settings - ) + try: + product_name = get_product_name( + self._item.dst_project_name, + task_name, + task_type, + self.host_name, + product_type, + self._item.variant, + project_settings=self._project_settings + ) + except TaskNotSetError: + self._status.set_failed( + "Target product name template requires task name. To continue" + " you have to select target task or change settings" + " ayon+settings://core/tools/creator/product_name_profiles" + f"?project={self._item.dst_project_name}." + ) + raise PushToProjectError(self._status.fail_reason) + self._log_info( f"Push will be integrating to product with name '{product_name}'" ) diff --git a/client/ayon_core/tools/push_to_project/models/user_values.py b/client/ayon_core/tools/push_to_project/models/user_values.py index edef2fe4fb..e52cb2917c 100644 --- a/client/ayon_core/tools/push_to_project/models/user_values.py +++ b/client/ayon_core/tools/push_to_project/models/user_values.py @@ -84,8 +84,11 @@ class UserPublishValuesModel: return self._new_folder_name = folder_name - is_valid = True - if folder_name: + if folder_name is None: + is_valid = True + elif not folder_name: + is_valid = False + else: is_valid = ( self.folder_name_regex.match(folder_name) is not None ) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 4d64509afd..a69c512fcd 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -8,12 +8,69 @@ from ayon_core.tools.utils import ( ProjectsCombobox, FoldersWidget, TasksWidget, + NiceCheckbox, ) from ayon_core.tools.push_to_project.control import ( PushToContextController, ) +class ErrorDetailDialog(QtWidgets.QDialog): + def __init__(self, parent): + super().__init__(parent) + + self.setWindowTitle("Error detail") + self.setWindowIcon(QtGui.QIcon(get_app_icon_path())) + + title_label = QtWidgets.QLabel(self) + + sep_1 = SeparatorWidget(parent=self) + + detail_widget = QtWidgets.QTextBrowser(self) + detail_widget.setReadOnly(True) + detail_widget.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + + sep_2 = SeparatorWidget(parent=self) + + btns_widget = QtWidgets.QWidget(self) + + copy_btn = QtWidgets.QPushButton("Copy", btns_widget) + close_btn = QtWidgets.QPushButton("Close", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(copy_btn, 0) + btns_layout.addWidget(close_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(10, 10, 10, 10) + main_layout.addWidget(title_label, 0) + main_layout.addWidget(sep_1, 0) + main_layout.addWidget(detail_widget, 1) + main_layout.addWidget(sep_2, 0) + main_layout.addWidget(btns_widget, 0) + + copy_btn.clicked.connect(self._on_copy_click) + close_btn.clicked.connect(self._on_close_click) + + self._title_label = title_label + self._detail_widget = detail_widget + + def set_detail(self, title, detail): + self._title_label.setText(title) + self._detail_widget.setText(detail) + + def _on_copy_click(self): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(self._detail_widget.toPlainText()) + + def _on_close_click(self): + self.close() + + class PushToContextSelectWindow(QtWidgets.QWidget): def __init__(self, controller=None): super(PushToContextSelectWindow, self).__init__() @@ -66,9 +123,12 @@ class PushToContextSelectWindow(QtWidgets.QWidget): # --- Inputs widget --- inputs_widget = QtWidgets.QWidget(main_splitter) + new_folder_checkbox = NiceCheckbox(True, parent=inputs_widget) + folder_name_input = PlaceholderLineEdit(inputs_widget) folder_name_input.setPlaceholderText("< Name of new folder >") folder_name_input.setObjectName("ValidatedLineEdit") + folder_name_input.setEnabled(new_folder_checkbox.isChecked()) variant_input = PlaceholderLineEdit(inputs_widget) variant_input.setPlaceholderText("< Variant >") @@ -79,6 +139,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): inputs_layout = QtWidgets.QFormLayout(inputs_widget) inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.addRow("Create new folder", new_folder_checkbox) inputs_layout.addRow("New folder name", folder_name_input) inputs_layout.addRow("Variant", variant_input) inputs_layout.addRow("Comment", comment_input) @@ -113,6 +174,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget): overlay_label = QtWidgets.QLabel(overlay_widget) overlay_label.setAlignment(QtCore.Qt.AlignCenter) + overlay_label.setWordWrap(True) + overlay_label.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) overlay_btns_widget = QtWidgets.QWidget(overlay_widget) overlay_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) @@ -121,13 +186,28 @@ class PushToContextSelectWindow(QtWidgets.QWidget): overlay_try_btn = QtWidgets.QPushButton( "Try again", overlay_btns_widget ) + overlay_try_btn.setToolTip( + "Hide overlay and modify submit information." + ) + + show_detail_btn = QtWidgets.QPushButton( + "Show error detail", overlay_btns_widget + ) + show_detail_btn.setToolTip( + "Show error detail dialog to copy full error." + ) + overlay_close_btn = QtWidgets.QPushButton( "Close", overlay_btns_widget ) + overlay_close_btn.setToolTip("Discard changes and close window.") overlay_btns_layout = QtWidgets.QHBoxLayout(overlay_btns_widget) + overlay_btns_layout.setContentsMargins(0, 0, 0, 0) + overlay_btns_layout.setSpacing(10) overlay_btns_layout.addStretch(1) overlay_btns_layout.addWidget(overlay_try_btn, 0) + overlay_btns_layout.addWidget(show_detail_btn, 0) overlay_btns_layout.addWidget(overlay_close_btn, 0) overlay_btns_layout.addStretch(1) @@ -156,12 +236,14 @@ class PushToContextSelectWindow(QtWidgets.QWidget): main_thread_timer.timeout.connect(self._on_main_thread_timer) show_timer.timeout.connect(self._on_show_timer) user_input_changed_timer.timeout.connect(self._on_user_input_timer) + new_folder_checkbox.stateChanged.connect(self._on_new_folder_check) folder_name_input.textChanged.connect(self._on_new_folder_change) variant_input.textChanged.connect(self._on_variant_change) comment_input.textChanged.connect(self._on_comment_change) publish_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) + show_detail_btn.clicked.connect(self._on_show_detail_click) overlay_close_btn.clicked.connect(self._on_close_click) overlay_try_btn.clicked.connect(self._on_try_again_click) @@ -203,23 +285,28 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._tasks_widget = tasks_widget self._variant_input = variant_input + self._new_folder_checkbox = new_folder_checkbox self._folder_name_input = folder_name_input self._comment_input = comment_input self._publish_btn = publish_btn self._overlay_widget = overlay_widget + self._show_detail_btn = show_detail_btn self._overlay_close_btn = overlay_close_btn self._overlay_try_btn = overlay_try_btn self._overlay_label = overlay_label + self._error_detail_dialog = ErrorDetailDialog(self) + self._user_input_changed_timer = user_input_changed_timer # Store current value on input text change # The value is unset when is passed to controller # The goal is to have controll over changes happened during user change # in UI and controller auto-changes - self._variant_input_text = None + self._new_folder_name_enabled = None self._new_folder_name_input_text = None + self._variant_input_text = None self._comment_input_text = None self._first_show = True @@ -235,6 +322,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._folder_is_valid = None publish_btn.setEnabled(False) + show_detail_btn.setVisible(False) overlay_close_btn.setVisible(False) overlay_try_btn.setVisible(False) @@ -289,6 +377,11 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self.refresh() + def _on_new_folder_check(self): + self._new_folder_name_enabled = self._new_folder_checkbox.isChecked() + self._folder_name_input.setEnabled(self._new_folder_name_enabled) + self._user_input_changed_timer.start() + def _on_new_folder_change(self, text): self._new_folder_name_input_text = text self._user_input_changed_timer.start() @@ -302,9 +395,15 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._user_input_changed_timer.start() def _on_user_input_timer(self): + folder_name_enabled = self._new_folder_name_enabled folder_name = self._new_folder_name_input_text - if folder_name is not None: + if folder_name is not None or folder_name_enabled is not None: self._new_folder_name_input_text = None + self._new_folder_name_enabled = None + if not self._new_folder_checkbox.isChecked(): + folder_name = None + elif folder_name is None: + folder_name = self._folder_name_input.text() self._controller.set_user_value_folder_name(folder_name) variant = self._variant_input_text @@ -350,16 +449,13 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._header_label.setText(self._controller.get_source_label()) def _invalidate_new_folder_name(self, folder_name, is_valid): - self._tasks_widget.setVisible(not folder_name) + self._tasks_widget.setVisible(folder_name is None) if self._folder_is_valid is is_valid: return self._folder_is_valid = is_valid state = "" - if folder_name: - if is_valid is True: - state = "valid" - elif is_valid is False: - state = "invalid" + if folder_name is not None: + state = "valid" if is_valid else "invalid" set_style_property( self._folder_name_input, "state", state ) @@ -374,6 +470,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget): def _on_submission_change(self, event): self._publish_btn.setEnabled(event["enabled"]) + def _on_show_detail_click(self): + self._error_detail_dialog.show() + def _on_close_click(self): self.close() @@ -384,8 +483,11 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._process_item_id = None self._last_submit_message = None + self._error_detail_dialog.close() + self._overlay_close_btn.setVisible(False) self._overlay_try_btn.setVisible(False) + self._show_detail_btn.setVisible(False) self._main_layout.setCurrentWidget(self._main_context_widget) def _on_main_thread_timer(self): @@ -401,13 +503,24 @@ class PushToContextSelectWindow(QtWidgets.QWidget): if self._main_thread_timer_can_stop: self._main_thread_timer.stop() self._overlay_close_btn.setVisible(True) - if push_failed and not fail_traceback: + if push_failed: self._overlay_try_btn.setVisible(True) + if fail_traceback: + self._show_detail_btn.setVisible(True) if push_failed: - message = "Push Failed:\n{}".format(process_status["fail_reason"]) + reason = process_status["fail_reason"] if fail_traceback: - message += "\n{}".format(fail_traceback) + message = ( + "Unhandled error happened." + " Check error detail for more information." + ) + self._error_detail_dialog.set_detail( + reason, fail_traceback + ) + else: + message = f"Push Failed:\n{reason}" + self._overlay_label.setText(message) set_style_property(self._overlay_close_btn, "state", "error") diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index f6a8add861..aad89b6081 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -20,9 +20,10 @@ from ayon_core.lib import ( ) from ayon_core.settings import get_studio_settings from ayon_core.addon import ( - ITrayAction, + ITrayAddon, ITrayService, ) +from ayon_core.pipeline import install_ayon_plugins from ayon_core.tools.utils import ( WrappedCallbackItem, get_ayon_qt_app, @@ -32,6 +33,12 @@ from ayon_core.tools.tray.lib import ( remove_tray_server_url, TrayIsRunningError, ) +from ayon_core.tools.launcher.ui import LauncherWindow +from ayon_core.tools.loader.ui import LoaderWindow +from ayon_core.tools.console_interpreter.ui import ConsoleInterpreterWindow +from ayon_core.tools.publisher.publish_report_viewer import ( + PublishReportViewerWindow, +) from .addons_manager import TrayAddonsManager from .host_console_listener import HostListener @@ -82,6 +89,11 @@ class TrayManager: self._outdated_dialog = None + self._launcher_window = None + self._browser_window = None + self._console_window = ConsoleInterpreterWindow() + self._publish_report_viewer_window = PublishReportViewerWindow() + self._update_check_timer = update_check_timer self._update_check_interval = update_check_interval self._main_thread_timer = main_thread_timer @@ -109,12 +121,15 @@ class TrayManager: @property def doubleclick_callback(self): """Double-click callback for Tray icon.""" - return self._addons_manager.get_doubleclick_callback() + callback = self._addons_manager.get_doubleclick_callback() + if callback is None: + callback = self._show_launcher_window + return callback def execute_doubleclick(self): """Execute double click callback in main thread.""" callback = self.doubleclick_callback - if callback: + if callback is not None: self.execute_in_main_thread(callback) def show_tray_message(self, title, message, icon=None, msecs=None): @@ -144,8 +159,34 @@ class TrayManager: return tray_menu = self.tray_widget.menu + # Add launcher at first place + launcher_action = QtWidgets.QAction( + "Launcher", tray_menu + ) + launcher_action.triggered.connect(self._show_launcher_window) + tray_menu.addAction(launcher_action) + + console_action = ITrayAddon.add_action_to_admin_submenu( + "Console", tray_menu + ) + console_action.triggered.connect(self._show_console_window) + + publish_report_viewer_action = ITrayAddon.add_action_to_admin_submenu( + "Publish report viewer", tray_menu + ) + publish_report_viewer_action.triggered.connect( + self._show_publish_report_viewer + ) + self._addons_manager.initialize(tray_menu) + # Add browser action after addon actions + browser_action = QtWidgets.QAction( + "Browser", tray_menu + ) + browser_action.triggered.connect(self._show_browser_window) + tray_menu.addAction(browser_action) + self._addons_manager.add_route( "GET", "/tray", self._web_get_tray_info ) @@ -153,7 +194,7 @@ class TrayManager: "POST", "/tray/message", self._web_show_tray_message ) - admin_submenu = ITrayAction.admin_submenu(tray_menu) + admin_submenu = ITrayAddon.admin_submenu(tray_menu) tray_menu.addMenu(admin_submenu) # Add services if they are @@ -522,6 +563,35 @@ class TrayManager: self._info_widget.raise_() self._info_widget.activateWindow() + def _show_launcher_window(self): + if self._launcher_window is None: + self._launcher_window = LauncherWindow() + + self._launcher_window.show() + self._launcher_window.raise_() + self._launcher_window.activateWindow() + + def _show_browser_window(self): + if self._browser_window is None: + self._browser_window = LoaderWindow() + self._browser_window.setWindowTitle("AYON Browser") + install_ayon_plugins() + + self._browser_window.show() + self._browser_window.raise_() + self._browser_window.activateWindow() + + def _show_console_window(self): + self._console_window.show() + self._console_window.raise_() + self._console_window.activateWindow() + + def _show_publish_report_viewer(self): + self._publish_report_viewer_window.refresh() + self._publish_report_viewer_window.show() + self._publish_report_viewer_window.raise_() + self._publish_report_viewer_window.activateWindow() + class SystemTrayIcon(QtWidgets.QSystemTrayIcon): """Tray widget.