Merge branch 'develop' into bugfix/replace_published_scene_path_deepcopy

This commit is contained in:
Jakub Trllo 2024-12-13 13:50:12 +01:00 committed by GitHub
commit f6c7ee07fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1064 additions and 995 deletions

View file

@ -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):

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -1,8 +0,0 @@
from .addon import (
PythonInterpreterAction
)
__all__ = (
"PythonInterpreterAction",
)

View file

@ -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()

View file

@ -1,8 +0,0 @@
from .widgets import (
PythonInterpreterWidget
)
__all__ = (
"PythonInterpreterWidget",
)

View file

@ -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()

View file

@ -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

View file

@ -2,6 +2,7 @@ import os
import re
import copy
import platform
from typing import Optional, Dict, Any
import ayon_api
@ -16,12 +17,12 @@ from ayon_core.pipeline.template_data import get_template_data
def get_workfile_template_key_from_context(
project_name,
folder_path,
task_name,
host_name,
project_settings=None
):
project_name: str,
folder_path: str,
task_name: str,
host_name: str,
project_settings: Optional[Dict[str, Any]] = None,
) -> str:
"""Helper function to get template key for workfile template.
Do the same as `get_workfile_template_key` but returns value for "session
@ -34,15 +35,23 @@ def get_workfile_template_key_from_context(
host_name (str): Host name.
project_settings (Dict[str, Any]): Project settings for passed
'project_name'. Not required at all but makes function faster.
"""
Returns:
str: Workfile template name.
"""
folder_entity = ayon_api.get_folder_by_path(
project_name, folder_path, fields={"id"}
project_name,
folder_path,
fields={"id"},
)
task_entity = ayon_api.get_task_by_name(
project_name, folder_entity["id"], task_name
project_name,
folder_entity["id"],
task_name,
fields={"taskType"},
)
task_type = task_entity.get("type")
task_type = task_entity.get("taskType")
return get_workfile_template_key(
project_name, task_type, host_name, project_settings

View file

@ -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 = []

View file

@ -37,7 +37,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
"substancepainter",
"nuke",
"aftereffects",
"unreal"
"unreal",
"houdini"
]
enabled = False

View file

@ -254,7 +254,7 @@ class FilesModel(QtGui.QStandardItemModel):
"""Make sure that removed items are removed from items mapping.
Connected with '_on_insert'. When user drag item and drop it to same
view the item is actually removed and creted again but it happens in
view the item is actually removed and created again but it happens in
inner calls of Qt.
"""
@ -841,7 +841,7 @@ class FilesWidget(QtWidgets.QFrame):
self._multivalue
)
widget.context_menu_requested.connect(
self._on_context_menu_requested
self._on_item_context_menu_request
)
self._files_view.setIndexWidget(index, widget)
self._files_proxy_model.setData(
@ -859,7 +859,7 @@ class FilesWidget(QtWidgets.QFrame):
for row in range(self._files_proxy_model.rowCount()):
index = self._files_proxy_model.index(row, 0)
item_id = index.data(ITEM_ID_ROLE)
available_item_ids.add(index.data(ITEM_ID_ROLE))
available_item_ids.add(item_id)
widget_ids = set(self._widgets_by_id.keys())
for item_id in available_item_ids:
@ -923,6 +923,9 @@ class FilesWidget(QtWidgets.QFrame):
if menu.actions():
menu.popup(pos)
def _on_item_context_menu_request(self, pos):
self._on_context_menu_requested(pos, True)
def dragEnterEvent(self, event):
if self._multivalue:
return

View file

@ -0,0 +1,8 @@
from .abstract import AbstractInterpreterController
from .control import InterpreterController
__all__ = (
"AbstractInterpreterController",
"InterpreterController",
)

View file

@ -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

View file

@ -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)

View file

@ -0,0 +1,8 @@
from .window import (
ConsoleInterpreterWindow
)
__all__ = (
"ConsoleInterpreterWindow",
)

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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"
" <b>ayon+settings://core/tools/creator/product_name_profiles"
f"?project={self._item.dst_project_name}</b>."
)
raise PushToProjectError(self._status.fail_reason)
self._log_info(
f"Push will be integrating to product with name '{product_name}'"
)

View file

@ -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
)

View file

@ -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")

View file

@ -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.