From c40062878759558d61e1c6f1d4b5345136f254f3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:41:01 +0100 Subject: [PATCH 1/8] added launcher and browser actions to tray --- client/ayon_core/tools/tray/ui/tray.py | 45 ++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index f6a8add861..e61f903c80 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -23,6 +23,7 @@ from ayon_core.addon import ( ITrayAction, ITrayService, ) +from ayon_core.pipeline import install_ayon_plugins from ayon_core.tools.utils import ( WrappedCallbackItem, get_ayon_qt_app, @@ -32,6 +33,8 @@ 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 .addons_manager import TrayAddonsManager from .host_console_listener import HostListener @@ -82,6 +85,9 @@ class TrayManager: self._outdated_dialog = None + self._launcher_window = None + self._browser_window = None + self._update_check_timer = update_check_timer self._update_check_interval = update_check_interval self._main_thread_timer = main_thread_timer @@ -109,12 +115,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 +153,22 @@ class TrayManager: return tray_menu = self.tray_widget.menu + self._addons_manager.initialize(tray_menu) + # Add default actions under addon actions + launcher_action = QtWidgets.QAction( + "Launcher", tray_menu + ) + launcher_action.triggered.connect(self._show_launcher_window) + tray_menu.addAction(launcher_action) + + 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 ) @@ -522,6 +545,24 @@ 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() + class SystemTrayIcon(QtWidgets.QSystemTrayIcon): """Tray widget. From 167cea29b5d8bba2c5af44cc22346cdabfbf7eba Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:41:11 +0100 Subject: [PATCH 2/8] remove action addons --- client/ayon_core/modules/launcher_action.py | 60 ------------------ client/ayon_core/modules/loader_action.py | 68 --------------------- 2 files changed, 128 deletions(-) delete mode 100644 client/ayon_core/modules/launcher_action.py delete mode 100644 client/ayon_core/modules/loader_action.py 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 From 77efd56157470058561fdf7b3fffdd7d29595b51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:10:35 +0100 Subject: [PATCH 3/8] created tool with basic separation of some logic to controller --- .../tools/console_interpreter/__init__.py | 8 + .../tools/console_interpreter/abstract.py | 33 ++ .../tools/console_interpreter/control.py | 63 ++++ .../tools/console_interpreter/ui/__init__.py | 8 + .../tools/console_interpreter/ui/utils.py | 42 +++ .../tools/console_interpreter/ui/widgets.py | 251 ++++++++++++++ .../tools/console_interpreter/ui/window.py | 324 ++++++++++++++++++ 7 files changed, 729 insertions(+) create mode 100644 client/ayon_core/tools/console_interpreter/__init__.py create mode 100644 client/ayon_core/tools/console_interpreter/abstract.py create mode 100644 client/ayon_core/tools/console_interpreter/control.py create mode 100644 client/ayon_core/tools/console_interpreter/ui/__init__.py create mode 100644 client/ayon_core/tools/console_interpreter/ui/utils.py create mode 100644 client/ayon_core/tools/console_interpreter/ui/widgets.py create mode 100644 client/ayon_core/tools/console_interpreter/ui/window.py 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() From bf631d565d2bfff14409d41023d7a4f0ed3e73ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:10:52 +0100 Subject: [PATCH 4/8] add Console to default tray actions --- client/ayon_core/tools/tray/ui/tray.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index e61f903c80..638a316634 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -35,6 +35,7 @@ from ayon_core.tools.tray.lib import ( ) 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 .addons_manager import TrayAddonsManager from .host_console_listener import HostListener @@ -87,6 +88,7 @@ class TrayManager: self._launcher_window = None self._browser_window = None + self._console_window = ConsoleInterpreterWindow() self._update_check_timer = update_check_timer self._update_check_interval = update_check_interval @@ -154,6 +156,11 @@ class TrayManager: tray_menu = self.tray_widget.menu + console_action = ITrayAction.add_action_to_admin_submenu( + "Console", tray_menu + ) + console_action.triggered.connect(self._show_console_window) + self._addons_manager.initialize(tray_menu) # Add default actions under addon actions @@ -563,6 +570,11 @@ class TrayManager: 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() + class SystemTrayIcon(QtWidgets.QSystemTrayIcon): """Tray widget. From 21e60135f434b2b8f6553cc2d0aeb12e4e68049e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:11:20 +0100 Subject: [PATCH 5/8] remove 'ayon_core.modules' --- client/ayon_core/addon/base.py | 58 +- client/ayon_core/modules/__init__.py | 0 .../python_console_interpreter/__init__.py | 8 - .../python_console_interpreter/addon.py | 42 -- .../window/__init__.py | 8 - .../window/widgets.py | 660 ------------------ 6 files changed, 1 insertion(+), 775 deletions(-) delete mode 100644 client/ayon_core/modules/__init__.py delete mode 100644 client/ayon_core/modules/python_console_interpreter/__init__.py delete mode 100644 client/ayon_core/modules/python_console_interpreter/addon.py delete mode 100644 client/ayon_core/modules/python_console_interpreter/window/__init__.py delete mode 100644 client/ayon_core/modules/python_console_interpreter/window/widgets.py 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/modules/__init__.py b/client/ayon_core/modules/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 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() From a8441e3036816e6fe3cb44239e4bc3cdc8c8b4a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:14:51 +0100 Subject: [PATCH 6/8] enhanced admin menu options --- client/ayon_core/addon/interfaces.py | 40 +++++++++++++++----------- client/ayon_core/tools/tray/ui/tray.py | 6 ++-- 2 files changed, 26 insertions(+), 20 deletions(-) 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/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 638a316634..dbaf13dfe9 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -20,7 +20,7 @@ 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 @@ -156,7 +156,7 @@ class TrayManager: tray_menu = self.tray_widget.menu - console_action = ITrayAction.add_action_to_admin_submenu( + console_action = ITrayAddon.add_action_to_admin_submenu( "Console", tray_menu ) console_action.triggered.connect(self._show_console_window) @@ -183,7 +183,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 From 14d4c75a123b203f2d27a73316d122fea88426b3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:24:20 +0100 Subject: [PATCH 7/8] add publish report viewer to admin actions --- .../publisher/publish_report_viewer/window.py | 70 +++++++++++-------- client/ayon_core/tools/tray/ui/tray.py | 17 +++++ 2 files changed, 57 insertions(+), 30 deletions(-) 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/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index dbaf13dfe9..98e3c783c4 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -36,6 +36,9 @@ from ayon_core.tools.tray.lib import ( 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 @@ -89,6 +92,7 @@ class TrayManager: 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 @@ -161,6 +165,13 @@ class TrayManager: ) 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 default actions under addon actions @@ -575,6 +586,12 @@ class TrayManager: 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. From 40e5a4a3ade8f2062d7c7944b3c78e77f740d943 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:44:09 +0100 Subject: [PATCH 8/8] move launcher to the top --- client/ayon_core/tools/tray/ui/tray.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 98e3c783c4..aad89b6081 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -159,6 +159,12 @@ 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 @@ -174,13 +180,7 @@ class TrayManager: self._addons_manager.initialize(tray_menu) - # Add default actions under addon actions - launcher_action = QtWidgets.QAction( - "Launcher", tray_menu - ) - launcher_action.triggered.connect(self._show_launcher_window) - tray_menu.addAction(launcher_action) - + # Add browser action after addon actions browser_action = QtWidgets.QAction( "Browser", tray_menu )