diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py
index 364a84cb7b..72270fa585 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):
@@ -950,6 +894,21 @@ class AddonsManager:
output.extend(paths)
return output
+ def collect_launcher_action_paths(self):
+ """Helper to collect launcher action paths from addons.
+
+ Returns:
+ list: List of paths to launcher actions.
+
+ """
+ output = self._collect_plugin_paths(
+ "get_launcher_action_paths"
+ )
+ # Add default core actions
+ actions_dir = os.path.join(AYON_CORE_ROOT, "plugins", "actions")
+ output.insert(0, actions_dir)
+ return output
+
def collect_create_plugin_paths(self, host_name):
"""Helper to collect creator plugin paths from addons.
diff --git a/client/ayon_core/addon/interfaces.py b/client/ayon_core/addon/interfaces.py
index b273e7839b..72191e3453 100644
--- a/client/ayon_core/addon/interfaces.py
+++ b/client/ayon_core/addon/interfaces.py
@@ -54,6 +54,13 @@ class IPluginPaths(AYONInterface):
paths = [paths]
return paths
+ def get_launcher_action_paths(self):
+ """Receive launcher actions paths.
+
+ Give addons ability to add launcher actions paths.
+ """
+ return self._get_plugin_paths_by_type("actions")
+
def get_create_plugin_paths(self, host_name):
"""Receive create plugin paths.
@@ -125,6 +132,7 @@ class ITrayAddon(AYONInterface):
tray_initialized = False
_tray_manager = None
+ _admin_submenu = None
@abstractmethod
def tray_init(self):
@@ -198,6 +206,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 +240,6 @@ class ITrayAction(ITrayAddon):
"""
admin_action = False
- _admin_submenu = None
_action_item = None
@property
@@ -229,12 +257,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 +271,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/context_tools.py b/client/ayon_core/pipeline/context_tools.py
index 44c9e5d673..b9ae906ab4 100644
--- a/client/ayon_core/pipeline/context_tools.py
+++ b/client/ayon_core/pipeline/context_tools.py
@@ -585,9 +585,6 @@ def version_up_current_workfile():
"""Function to increment and save workfile
"""
host = registered_host()
- if not host.has_unsaved_changes():
- print("No unsaved changes, skipping file save..")
- return
project_name = get_current_project_name()
folder_path = get_current_folder_path()
diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py
index 2ba40d7687..ecdcc0f0c1 100644
--- a/client/ayon_core/pipeline/publish/lib.py
+++ b/client/ayon_core/pipeline/publish/lib.py
@@ -764,7 +764,7 @@ def replace_with_published_scene_path(instance, replace_in_path=True):
return
# determine published path from Anatomy.
- template_data = workfile_instance.data.get("anatomyData")
+ template_data = copy.deepcopy(workfile_instance.data["anatomyData"])
rep = workfile_instance.data["representations"][0]
template_data["representation"] = rep.get("name")
template_data["ext"] = rep.get("ext")
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/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 5fb5fe22fa..7f9f293b70 100644
--- a/client/ayon_core/plugins/publish/extract_thumbnail.py
+++ b/client/ayon_core/plugins/publish/extract_thumbnail.py
@@ -38,6 +38,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
"nuke",
"aftereffects",
"unreal",
+ "houdini",
"circuit",
]
enabled = False
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/launcher/models/actions.py b/client/ayon_core/tools/launcher/models/actions.py
index 8bd30daffa..e1612e2b9f 100644
--- a/client/ayon_core/tools/launcher/models/actions.py
+++ b/client/ayon_core/tools/launcher/models/actions.py
@@ -7,6 +7,7 @@ from ayon_core.pipeline.actions import (
discover_launcher_actions,
LauncherAction,
LauncherActionSelection,
+ register_launcher_action_path,
)
from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch
@@ -459,6 +460,14 @@ class ActionsModel:
def _get_discovered_action_classes(self):
if self._discovered_actions is None:
+ # NOTE We don't need to register the paths, but that would
+ # require to change discovery logic and deprecate all functions
+ # related to registering and discovering launcher actions.
+ addons_manager = self._get_addons_manager()
+ actions_paths = addons_manager.collect_launcher_action_paths()
+ for path in actions_paths:
+ if path and os.path.exists(path):
+ register_launcher_action_path(path)
self._discovered_actions = (
discover_launcher_actions()
+ self._get_applications_action_classes()
diff --git a/client/ayon_core/tools/launcher/ui/window.py b/client/ayon_core/tools/launcher/ui/window.py
index 34aeab35bb..2d52a73c38 100644
--- a/client/ayon_core/tools/launcher/ui/window.py
+++ b/client/ayon_core/tools/launcher/ui/window.py
@@ -202,8 +202,9 @@ class LauncherWindow(QtWidgets.QWidget):
self._go_to_hierarchy_page(project_name)
def _on_projects_refresh(self):
- # There is nothing to do, we're on projects page
+ # Refresh only actions on projects page
if self._is_on_projects_page:
+ self._actions_widget.refresh()
return
# No projects were found -> go back to projects page
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.
diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py
index a4ae75914c..2417897a47 100644
--- a/client/ayon_core/version.py
+++ b/client/ayon_core/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'core' version."""
-__version__ = "1.0.11+dev"
+__version__ = "1.0.12+dev"
diff --git a/package.py b/package.py
index b8d88fc2ad..8ade5ceeed 100644
--- a/package.py
+++ b/package.py
@@ -1,6 +1,6 @@
name = "core"
title = "Core"
-version = "1.0.11+dev"
+version = "1.0.12+dev"
client_dir = "ayon_core"
diff --git a/pyproject.toml b/pyproject.toml
index bdfaf797e4..b8d6a5a537 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@
[tool.poetry]
name = "ayon-core"
-version = "1.0.11+dev"
+version = "1.0.12+dev"
description = ""
authors = ["Ynput Team "]
readme = "README.md"