ayon-core/openpype/tools/publisher/window.py
Jakub Trllo adb7e19c23
Publisher: Window is not always on top (#6107)
* make publisher a window without always on top

* put publisher window to the top on process

* make sure screenshot window is active

* removed unnecessary variable
2024-01-11 13:21:41 +01:00

1186 lines
40 KiB
Python

import os
import json
import time
import collections
import copy
from qtpy import QtWidgets, QtCore, QtGui
from openpype import (
resources,
style
)
from openpype import AYON_SERVER_ENABLED
from openpype.tools.utils import (
ErrorMessageBox,
PlaceholderLineEdit,
MessageOverlayObject,
PixmapLabel,
)
from openpype.tools.utils.lib import center_window
from .constants import ResetKeySequence
from .publish_report_viewer import PublishReportViewerWidget
from .control import CardMessageTypes
from .control_qt import QtPublisherController
from .widgets import (
OverviewWidget,
ReportPageWidget,
PublishFrame,
PublisherTabsWidget,
SaveBtn,
ResetBtn,
StopBtn,
ValidateBtn,
PublishBtn,
HelpButton,
HelpDialog,
CreateNextPageOverlay,
)
class PublisherWindow(QtWidgets.QWidget):
"""Main window of publisher."""
default_width = 1300
default_height = 800
footer_border = 8
publish_footer_spacer = 2
def __init__(self, parent=None, controller=None, reset_on_show=None):
super(PublisherWindow, self).__init__()
self.setObjectName("PublishWindow")
self.setWindowTitle("{} publisher".format(
"AYON" if AYON_SERVER_ENABLED else "OpenPype"
))
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
self.setWindowIcon(icon)
if reset_on_show is None:
reset_on_show = True
self.setWindowFlags(
QtCore.Qt.Window
| QtCore.Qt.WindowTitleHint
| QtCore.Qt.WindowMaximizeButtonHint
| QtCore.Qt.WindowMinimizeButtonHint
| QtCore.Qt.WindowCloseButtonHint
)
if controller is None:
controller = QtPublisherController()
help_dialog = HelpDialog(controller, self)
overlay_object = MessageOverlayObject(self)
# Header
header_widget = QtWidgets.QWidget(self)
icon_pixmap = QtGui.QPixmap(resources.get_openpype_icon_filepath())
icon_label = PixmapLabel(icon_pixmap, header_widget)
icon_label.setObjectName("PublishContextLabel")
context_label = QtWidgets.QLabel(header_widget)
context_label.setObjectName("PublishContextLabel")
header_extra_widget = QtWidgets.QWidget(header_widget)
help_btn = HelpButton(header_widget)
header_layout = QtWidgets.QHBoxLayout(header_widget)
header_layout.setContentsMargins(15, 15, 0, 15)
header_layout.setSpacing(15)
header_layout.addWidget(icon_label, 0)
header_layout.addWidget(context_label, 0)
header_layout.addStretch(1)
header_layout.addWidget(header_extra_widget, 0)
header_layout.addWidget(help_btn, 0)
# Tabs widget under header
tabs_widget = PublisherTabsWidget(self)
create_tab = tabs_widget.add_tab("Create", "create")
tabs_widget.add_tab("Publish", "publish")
tabs_widget.add_tab("Report", "report")
tabs_widget.add_tab("Details", "details")
# Widget where is stacked publish overlay and widgets that should be
# covered by it
under_publish_stack = QtWidgets.QWidget(self)
# Added wrap widget where all widgets under overlay are added
# - this is because footer is also under overlay and the top part
# is faked with floating frame
under_publish_widget = QtWidgets.QWidget(under_publish_stack)
# Footer
footer_widget = QtWidgets.QWidget(under_publish_widget)
footer_bottom_widget = QtWidgets.QWidget(footer_widget)
comment_input = PlaceholderLineEdit(footer_widget)
comment_input.setObjectName("PublishCommentInput")
comment_input.setPlaceholderText(
"Attach a comment to your publish"
)
save_btn = SaveBtn(footer_widget)
reset_btn = ResetBtn(footer_widget)
stop_btn = StopBtn(footer_widget)
validate_btn = ValidateBtn(footer_widget)
publish_btn = PublishBtn(footer_widget)
footer_bottom_layout = QtWidgets.QHBoxLayout(footer_bottom_widget)
footer_bottom_layout.setContentsMargins(0, 0, 0, 0)
footer_bottom_layout.addStretch(1)
footer_bottom_layout.addWidget(save_btn, 0)
footer_bottom_layout.addWidget(reset_btn, 0)
footer_bottom_layout.addWidget(stop_btn, 0)
footer_bottom_layout.addWidget(validate_btn, 0)
footer_bottom_layout.addWidget(publish_btn, 0)
# Spacer helps keep distance of Publish Frame when comment input
# is hidden - so when is shrunken it is not overlaying pages
footer_spacer = QtWidgets.QWidget(footer_widget)
footer_spacer.setMinimumHeight(self.publish_footer_spacer)
footer_spacer.setMaximumHeight(self.publish_footer_spacer)
footer_spacer.setVisible(False)
footer_layout = QtWidgets.QVBoxLayout(footer_widget)
footer_margins = footer_layout.contentsMargins()
footer_layout.setContentsMargins(
footer_margins.left() + self.footer_border,
footer_margins.top(),
footer_margins.right() + self.footer_border,
footer_margins.bottom() + self.footer_border
)
footer_layout.addWidget(comment_input, 0)
footer_layout.addWidget(footer_spacer, 0)
footer_layout.addWidget(footer_bottom_widget, 0)
# Content
# - wrap stacked widget under one more widget to be able to propagate
# margins (QStackedLayout can't have margins)
content_widget = QtWidgets.QWidget(under_publish_widget)
content_stacked_widget = QtWidgets.QWidget(content_widget)
content_layout = QtWidgets.QVBoxLayout(content_widget)
marings = content_layout.contentsMargins()
marings.setLeft(marings.left() * 2)
marings.setRight(marings.right() * 2)
marings.setTop(marings.top() * 2)
marings.setBottom(0)
content_layout.setContentsMargins(marings)
content_layout.addWidget(content_stacked_widget, 1)
# Overview - create and attributes part
overview_widget = OverviewWidget(
controller, content_stacked_widget
)
report_widget = ReportPageWidget(controller, content_stacked_widget)
# Details - Publish details
publish_details_widget = PublishReportViewerWidget(
content_stacked_widget
)
content_stacked_layout = QtWidgets.QStackedLayout(
content_stacked_widget
)
content_stacked_layout.setContentsMargins(0, 0, 0, 0)
content_stacked_layout.setStackingMode(
QtWidgets.QStackedLayout.StackAll
)
content_stacked_layout.addWidget(overview_widget)
content_stacked_layout.addWidget(report_widget)
content_stacked_layout.addWidget(publish_details_widget)
content_stacked_layout.setCurrentWidget(overview_widget)
under_publish_layout = QtWidgets.QVBoxLayout(under_publish_widget)
under_publish_layout.setContentsMargins(0, 0, 0, 0)
under_publish_layout.setSpacing(0)
under_publish_layout.addWidget(content_widget, 1)
under_publish_layout.addWidget(footer_widget, 0)
# Overlay which covers inputs during publishing
publish_overlay = QtWidgets.QFrame(under_publish_stack)
publish_overlay.setObjectName("OverlayFrame")
under_publish_stack_layout = QtWidgets.QStackedLayout(
under_publish_stack
)
under_publish_stack_layout.setContentsMargins(0, 0, 0, 0)
under_publish_stack_layout.setStackingMode(
QtWidgets.QStackedLayout.StackAll
)
under_publish_stack_layout.addWidget(under_publish_widget)
under_publish_stack_layout.addWidget(publish_overlay)
under_publish_stack_layout.setCurrentWidget(under_publish_widget)
# Add main frame to this window
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
main_layout.addWidget(header_widget, 0)
main_layout.addWidget(tabs_widget, 0)
main_layout.addWidget(under_publish_stack, 1)
# Floating publish frame
publish_frame = PublishFrame(controller, self.footer_border, self)
create_overlay_button = CreateNextPageOverlay(self)
show_timer = QtCore.QTimer()
show_timer.setInterval(1)
show_timer.timeout.connect(self._on_show_timer)
errors_dialog_message_timer = QtCore.QTimer()
errors_dialog_message_timer.setInterval(100)
errors_dialog_message_timer.timeout.connect(
self._on_errors_message_timeout
)
help_btn.clicked.connect(self._on_help_click)
tabs_widget.tab_changed.connect(self._on_tab_change)
overview_widget.active_changed.connect(
self._on_context_or_active_change
)
overview_widget.instance_context_changed.connect(
self._on_context_or_active_change
)
overview_widget.create_requested.connect(
self._on_create_request
)
overview_widget.convert_requested.connect(
self._on_convert_requested
)
save_btn.clicked.connect(self._on_save_clicked)
reset_btn.clicked.connect(self._on_reset_clicked)
stop_btn.clicked.connect(self._on_stop_clicked)
validate_btn.clicked.connect(self._on_validate_clicked)
publish_btn.clicked.connect(self._on_publish_clicked)
publish_frame.details_page_requested.connect(self._go_to_details_tab)
create_overlay_button.clicked.connect(
self._on_create_overlay_button_click
)
controller.event_system.add_callback(
"instances.refresh.finished", self._on_instances_refresh
)
controller.event_system.add_callback(
"publish.reset.finished", self._on_publish_reset
)
controller.event_system.add_callback(
"controller.reset.finished", self._on_controller_reset
)
controller.event_system.add_callback(
"publish.process.started", self._on_publish_start
)
controller.event_system.add_callback(
"publish.has_validated.changed", self._on_publish_validated_change
)
controller.event_system.add_callback(
"publish.finished.changed", self._on_publish_finished_change
)
controller.event_system.add_callback(
"publish.process.stopped", self._on_publish_stop
)
controller.event_system.add_callback(
"publish.process.instance.changed", self._on_instance_change
)
controller.event_system.add_callback(
"publish.process.plugin.changed", self._on_plugin_change
)
controller.event_system.add_callback(
"show.card.message", self._on_overlay_message
)
controller.event_system.add_callback(
"instances.collection.failed", self._on_creator_error
)
controller.event_system.add_callback(
"instances.save.failed", self._on_creator_error
)
controller.event_system.add_callback(
"instances.remove.failed", self._on_creator_error
)
controller.event_system.add_callback(
"instances.create.failed", self._on_creator_error
)
controller.event_system.add_callback(
"convertors.convert.failed", self._on_convertor_error
)
controller.event_system.add_callback(
"convertors.find.failed", self._on_convertor_error
)
controller.event_system.add_callback(
"export_report.request", self._export_report
)
controller.event_system.add_callback(
"copy_report.request", self._copy_report
)
# Store extra header widget for TrayPublisher
# - can be used to add additional widgets to header between context
# label and help button
self._help_dialog = help_dialog
self._help_btn = help_btn
self._header_extra_widget = header_extra_widget
self._tabs_widget = tabs_widget
self._create_tab = create_tab
self._under_publish_stack_layout = under_publish_stack_layout
self._under_publish_widget = under_publish_widget
self._publish_overlay = publish_overlay
self._publish_frame = publish_frame
self._content_widget = content_widget
self._content_stacked_layout = content_stacked_layout
self._overview_widget = overview_widget
self._report_widget = report_widget
self._publish_details_widget = publish_details_widget
self._context_label = context_label
self._comment_input = comment_input
self._footer_spacer = footer_spacer
self._save_btn = save_btn
self._reset_btn = reset_btn
self._stop_btn = stop_btn
self._validate_btn = validate_btn
self._publish_btn = publish_btn
self._overlay_object = overlay_object
self._controller = controller
self._first_show = True
self._first_reset = True
# This is a little bit confusing but 'reset_on_first_show' is too long
# for init
self._reset_on_first_show = reset_on_show
self._reset_on_show = True
self._publish_frame_visible = None
self._tab_on_reset = None
self._error_messages_to_show = collections.deque()
self._errors_dialog_message_timer = errors_dialog_message_timer
self._set_publish_visibility(False)
self._create_overlay_button = create_overlay_button
self._app_event_listener_installed = False
self._show_timer = show_timer
self._show_counter = 0
self._window_is_visible = False
@property
def controller(self):
return self._controller
def show_and_publish(self, comment=None):
"""Show the window and start publishing.
The method will reset controller and start the publishing afterwards.
Todos:
Move validations from '_on_publish_clicked' and change of
'comment' value in controller to controller so it can be
simplified.
Args:
comment (Optional[str]): Comment to be set to publish.
If is set to 'None' a comment is not changed at all.
"""
self._reset_on_show = False
self._reset_on_first_show = False
if comment is not None:
self.set_comment(comment)
self.make_sure_is_visible()
# Reset controller
self._controller.reset()
# Fake publish click to trigger save validation and propagate
# comment to controller
self._on_publish_clicked()
def set_comment(self, comment):
"""Change comment text.
Todos:
Be able to set the comment via controller.
Args:
comment (str): Comment text.
"""
self._comment_input.setText(comment)
def make_sure_is_visible(self):
if self._window_is_visible:
self.setWindowState(QtCore.Qt.WindowActive)
else:
self.show()
def showEvent(self, event):
self._window_is_visible = True
super(PublisherWindow, self).showEvent(event)
if self._first_show:
self._first_show = False
self._on_first_show()
self._show_timer.start()
def resizeEvent(self, event):
super(PublisherWindow, self).resizeEvent(event)
self._update_publish_frame_rect()
self._update_create_overlay_size()
def closeEvent(self, event):
self._window_is_visible = False
self._uninstall_app_event_listener()
# TODO capture changes and ask user if wants to save changes on close
if not self._controller.host_context_has_changed:
self._save_changes(False)
self._comment_input.setText("") # clear comment
self._reset_on_show = True
self._controller.clear_thumbnail_temp_dir_path()
# Trigger custom event that should be captured only in UI
# - backend (controller) must not be dependent on this event topic!!!
self._controller.event_system.emit("main.window.closed", {}, "window")
super(PublisherWindow, self).closeEvent(event)
def leaveEvent(self, event):
super(PublisherWindow, self).leaveEvent(event)
self._update_create_overlay_visibility()
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.MouseMove:
self._update_create_overlay_visibility(event.globalPos())
return super(PublisherWindow, self).eventFilter(obj, event)
def _install_app_event_listener(self):
if self._app_event_listener_installed:
return
self._app_event_listener_installed = True
app = QtWidgets.QApplication.instance()
app.installEventFilter(self)
def _uninstall_app_event_listener(self):
if not self._app_event_listener_installed:
return
self._app_event_listener_installed = False
app = QtWidgets.QApplication.instance()
app.removeEventFilter(self)
def keyPressEvent(self, event):
# Ignore escape button to close window
if event.key() == QtCore.Qt.Key_Escape:
event.accept()
return
save_match = event.matches(QtGui.QKeySequence.Save)
# PySide2 and PySide6 support
if not isinstance(save_match, bool):
save_match = save_match == QtGui.QKeySequence.ExactMatch
if save_match:
if not self._controller.publish_has_started:
self._save_changes(True)
event.accept()
return
# PySide6 Support
if hasattr(event, "keyCombination"):
reset_match_result = ResetKeySequence.matches(
QtGui.QKeySequence(event.keyCombination())
)
else:
reset_match_result = ResetKeySequence.matches(
QtGui.QKeySequence(event.modifiers() | event.key())
)
if reset_match_result == QtGui.QKeySequence.ExactMatch:
if not self.controller.publish_is_running:
self.reset()
event.accept()
return
super(PublisherWindow, self).keyPressEvent(event)
def _on_overlay_message(self, event):
self._overlay_object.add_message(
event["message"],
event.get("message_type")
)
def _on_first_show(self):
self.resize(self.default_width, self.default_height)
self.setStyleSheet(style.load_stylesheet())
center_window(self)
self._reset_on_show = self._reset_on_first_show
def _on_show_timer(self):
# Add 1 to counter until hits 2
if self._show_counter < 3:
self._show_counter += 1
return
# Stop the timer
self._show_timer.stop()
# Reset counter when done for next show event
self._show_counter = 0
self._update_create_overlay_size()
self._update_create_overlay_visibility()
if self._is_on_create_tab():
self._install_app_event_listener()
# Reset if requested
if self._reset_on_show:
self._reset_on_show = False
self.reset()
def _make_sure_on_top(self):
"""Raise window to top and activate it.
This may not work for some DCCs without Qt.
"""
if not self._window_is_visible:
self.show()
self.setWindowState(QtCore.Qt.WindowActive)
self.raise_()
def _checks_before_save(self, explicit_save):
"""Save of changes may trigger some issues.
Check if context did change and ask user if he is really sure the
save should happen. A dialog can be shown during this method.
Args:
explicit_save (bool): Method was called when user explicitly asked
for save. Value affects shown message.
Returns:
bool: Save can happen.
"""
if not self._controller.host_context_has_changed:
return True
title = "Host context changed"
if explicit_save:
message = (
"Context has changed since Publisher window was refreshed last"
" time.\n\nAre you sure you want to save changes?"
)
else:
message = (
"Your action requires save of changes but context has changed"
" since Publisher window was refreshed last time.\n\nAre you"
" sure you want to continue and save changes?"
)
result = QtWidgets.QMessageBox.question(
self,
title,
message,
QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Cancel
)
return result == QtWidgets.QMessageBox.Save
def _save_changes(self, explicit_save):
"""Save changes of Creation part.
All possible triggers of save changes were moved to main window (here),
so it can handle possible issues with save at one place. Do checks,
so user don't accidentally save changes to different file or using
different context.
Moving responsibility to this place gives option to show the dialog and
wait for user's response without breaking action he wanted to do.
Args:
explicit_save (bool): Method was called when user explicitly asked
for save. Value affects shown message.
Returns:
bool: Save happened successfully.
"""
if not self._checks_before_save(explicit_save):
return False
return self._controller.save_changes()
def reset(self):
self._controller.reset()
def set_context_label(self, label):
self._context_label.setText(label)
def set_tab_on_reset(self, tab):
"""Define tab that will be selected on window show.
This is single use method, when publisher window is showed the value is
unset and not used on next show.
Args:
tab (Union[int, Literal[create, publish, details, report]]: Index
or name of tab which will be selected on show (after reset).
"""
self._tab_on_reset = tab
def _update_publish_details_widget(self, force=False):
if not force and not self._is_on_details_tab():
return
report_data = self.controller.get_publish_report()
self._publish_details_widget.set_report_data(report_data)
def _on_help_click(self):
if self._help_dialog.isVisible():
return
self._help_dialog.show()
window = self.window()
if hasattr(QtWidgets.QApplication, "desktop"):
desktop = QtWidgets.QApplication.desktop()
screen_idx = desktop.screenNumber(window)
screen_geo = desktop.screenGeometry(screen_idx)
else:
screen = window.screen()
screen_geo = screen.geometry()
window_geo = window.geometry()
dialog_x = window_geo.x() + window_geo.width()
dialog_right = (dialog_x + self._help_dialog.width()) - 1
diff = dialog_right - screen_geo.right()
if diff > 0:
dialog_x -= diff
self._help_dialog.setGeometry(
dialog_x, window_geo.y(),
self._help_dialog.width(), self._help_dialog.height()
)
def _on_create_overlay_button_click(self):
self._create_overlay_button.set_under_mouse(False)
self._go_to_publish_tab()
def _on_tab_change(self, old_tab, new_tab):
if old_tab == "details":
self._publish_details_widget.close_details_popup()
if new_tab == "details":
self._content_stacked_layout.setCurrentWidget(
self._publish_details_widget
)
self._update_publish_details_widget()
elif new_tab == "report":
self._content_stacked_layout.setCurrentWidget(
self._report_widget
)
old_on_overview = old_tab in ("create", "publish")
if new_tab in ("create", "publish"):
self._content_stacked_layout.setCurrentWidget(
self._overview_widget
)
# Overview state is animated only when switching between
# 'create' and 'publish' tab
self._overview_widget.set_state(new_tab, old_on_overview)
elif old_on_overview:
# Make sure animation finished if previous tab was 'create'
# or 'publish'. That is just for safety to avoid stuck animation
# when user clicks too fast.
self._overview_widget.make_sure_animation_is_finished()
is_create = new_tab == "create"
if is_create:
self._install_app_event_listener()
else:
self._uninstall_app_event_listener()
self._create_overlay_button.set_visible(is_create)
def _on_context_or_active_change(self):
self._validate_create_instances()
def _on_create_request(self):
self._go_to_create_tab()
def _on_convert_requested(self):
if not self._save_changes(False):
return
convertor_identifiers = (
self._overview_widget.get_selected_legacy_convertors()
)
self._controller.trigger_convertor_items(convertor_identifiers)
def _set_current_tab(self, identifier):
self._tabs_widget.set_current_tab(identifier)
def set_current_tab(self, tab):
if tab == "create":
self._go_to_create_tab()
elif tab == "publish":
self._go_to_publish_tab()
elif tab == "report":
self._go_to_report_tab()
elif tab == "details":
self._go_to_details_tab()
if not self._window_is_visible:
self.set_tab_on_reset(tab)
def _is_current_tab(self, identifier):
return self._tabs_widget.is_current_tab(identifier)
def _go_to_create_tab(self):
if self._create_tab.isEnabled():
self._set_current_tab("create")
return
self._overlay_object.add_message(
"Can't switch to Create tab because publishing is paused.",
message_type="info"
)
def _go_to_publish_tab(self):
self._set_current_tab("publish")
def _go_to_report_tab(self):
self._set_current_tab("report")
def _go_to_details_tab(self):
self._set_current_tab("details")
def _is_on_create_tab(self):
return self._is_current_tab("create")
def _is_on_publish_tab(self):
return self._is_current_tab("publish")
def _is_on_report_tab(self):
return self._is_current_tab("report")
def _is_on_details_tab(self):
return self._is_current_tab("details")
def _set_publish_overlay_visibility(self, visible):
if visible:
widget = self._publish_overlay
else:
widget = self._under_publish_widget
self._under_publish_stack_layout.setCurrentWidget(widget)
def _set_publish_visibility(self, visible):
if visible is self._publish_frame_visible:
return
self._publish_frame_visible = visible
self._publish_frame.setVisible(visible)
self._update_publish_frame_rect()
def _on_save_clicked(self):
self._save_changes(True)
def _on_reset_clicked(self):
self.reset()
def _on_stop_clicked(self):
self._controller.stop_publish()
def _set_publish_comment(self):
self._controller.set_comment(self._comment_input.text())
def _on_validate_clicked(self):
if self._save_changes(False):
self._set_publish_comment()
self._controller.validate()
def _on_publish_clicked(self):
if self._save_changes(False):
self._set_publish_comment()
self._controller.publish()
def _set_footer_enabled(self, enabled):
self._save_btn.setEnabled(True)
self._reset_btn.setEnabled(True)
if enabled:
self._stop_btn.setEnabled(False)
self._validate_btn.setEnabled(True)
self._publish_btn.setEnabled(True)
else:
self._stop_btn.setEnabled(enabled)
self._validate_btn.setEnabled(enabled)
self._publish_btn.setEnabled(enabled)
def _on_publish_reset(self):
self._create_tab.setEnabled(True)
self._set_comment_input_visiblity(True)
self._set_publish_overlay_visibility(False)
self._set_publish_visibility(False)
self._set_footer_enabled(False)
self._update_publish_details_widget()
def _on_controller_reset(self):
self._first_reset, first_reset = False, self._first_reset
if self._tab_on_reset is not None:
self._tab_on_reset, new_tab = None, self._tab_on_reset
self._set_current_tab(new_tab)
return
# On first reset change tab based on available items
# - if there is at least one instance the tab is changed to 'publish'
# otherwise 'create' is used
# - this happens only on first show
if first_reset:
self._go_to_create_tab()
elif self._is_on_report_tab():
# Go to 'Publish' tab if is on 'Details' tab
# - this can happen when publishing started and was reset
# at that moment it doesn't make sense to stay at publish
# specific tabs.
self._go_to_publish_tab()
def _on_publish_start(self):
self._create_tab.setEnabled(False)
self._reset_btn.setEnabled(False)
self._stop_btn.setEnabled(True)
self._validate_btn.setEnabled(False)
self._publish_btn.setEnabled(False)
self._set_comment_input_visiblity(False)
self._set_publish_visibility(True)
self._set_publish_overlay_visibility(True)
self._publish_details_widget.close_details_popup()
if self._is_on_create_tab():
self._go_to_publish_tab()
def _on_instance_change(self):
self._make_sure_on_top()
def _on_plugin_change(self):
self._make_sure_on_top()
def _on_publish_validated_change(self, event):
if event["value"]:
self._validate_btn.setEnabled(False)
def _on_publish_finished_change(self, event):
if event["value"]:
# Successful publish, remove comment from UI
self._comment_input.setText("")
def _on_publish_stop(self):
self._make_sure_on_top()
self._set_publish_overlay_visibility(False)
self._reset_btn.setEnabled(True)
self._stop_btn.setEnabled(False)
publish_has_crashed = self._controller.publish_has_crashed
validate_enabled = not publish_has_crashed
publish_enabled = not publish_has_crashed
if self._is_on_publish_tab():
self._go_to_report_tab()
if validate_enabled:
validate_enabled = not self._controller.publish_has_validated
if publish_enabled:
if (
self._controller.publish_has_validated
and self._controller.publish_has_validation_errors
):
publish_enabled = False
else:
publish_enabled = not self._controller.publish_has_finished
self._validate_btn.setEnabled(validate_enabled)
self._publish_btn.setEnabled(publish_enabled)
if not publish_enabled:
self._publish_frame.set_shrunk_state(True)
self._update_publish_details_widget()
def _validate_create_instances(self):
if not self._controller.host_is_valid:
self._set_footer_enabled(True)
return
all_valid = None
for instance in self._controller.instances.values():
if not instance["active"]:
continue
if not instance.has_valid_context:
all_valid = False
break
if all_valid is None:
all_valid = True
self._set_footer_enabled(bool(all_valid))
def _on_instances_refresh(self):
self._validate_create_instances()
context_title = self.controller.get_context_title()
self.set_context_label(context_title)
self._update_publish_details_widget()
def _set_comment_input_visiblity(self, visible):
self._comment_input.setVisible(visible)
self._footer_spacer.setVisible(not visible)
def _update_publish_frame_rect(self):
if not self._publish_frame_visible:
return
window_size = self.size()
size_hint = self._publish_frame.minimumSizeHint()
width = window_size.width()
height = size_hint.height()
self._publish_frame.resize(width, height)
self._publish_frame.move(
0, window_size.height() - height
)
def add_error_message_dialog(self, title, failed_info, message_start=None):
self._error_messages_to_show.append(
(title, failed_info, message_start)
)
self._errors_dialog_message_timer.start()
def _on_errors_message_timeout(self):
if not self._error_messages_to_show:
self._errors_dialog_message_timer.stop()
return
item = self._error_messages_to_show.popleft()
title, failed_info, message_start = item
dialog = ErrorsMessageBox(
title, failed_info, message_start, self
)
dialog.exec_()
dialog.deleteLater()
def _on_creator_error(self, event):
new_failed_info = []
for item in event["failed_info"]:
new_item = copy.deepcopy(item)
new_item["label"] = new_item.pop("creator_label")
new_item["identifier"] = new_item.pop("creator_identifier")
new_failed_info.append(new_item)
self.add_error_message_dialog(event["title"], new_failed_info, "Creator:")
def _on_convertor_error(self, event):
new_failed_info = []
for item in event["failed_info"]:
new_item = copy.deepcopy(item)
new_item["identifier"] = new_item.pop("convertor_identifier")
new_failed_info.append(new_item)
self.add_error_message_dialog(
event["title"], new_failed_info, "Convertor:"
)
def _update_create_overlay_size(self):
metrics = self._create_overlay_button.fontMetrics()
height = int(metrics.height())
width = int(height * 0.7)
end_pos_x = self.width()
start_pos_x = end_pos_x - width
center = self._content_widget.parent().mapTo(
self,
self._content_widget.rect().center()
)
pos_y = center.y() - (height * 0.5)
self._create_overlay_button.setGeometry(
start_pos_x, pos_y,
width, height
)
def _update_create_overlay_visibility(self, global_pos=None):
if global_pos is None:
global_pos = QtGui.QCursor.pos()
under_mouse = False
my_pos = self.mapFromGlobal(global_pos)
if self.rect().contains(my_pos):
widget_geo = self._overview_widget.get_subset_views_geo()
widget_x = widget_geo.left() + (widget_geo.width() * 0.5)
under_mouse = widget_x < global_pos.x()
self._create_overlay_button.set_under_mouse(under_mouse)
def _copy_report(self):
logs = self._controller.get_publish_report()
logs_string = json.dumps(logs, indent=4)
mime_data = QtCore.QMimeData()
mime_data.setText(logs_string)
QtWidgets.QApplication.instance().clipboard().setMimeData(
mime_data
)
self._controller.emit_card_message(
"Report added to clipboard",
CardMessageTypes.info)
def _export_report(self):
default_filename = "publish-report-{}".format(
time.strftime("%y%m%d-%H-%M")
)
default_filepath = os.path.join(
os.path.expanduser("~"),
default_filename
)
new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName(
self, "Save report", default_filepath, ".json"
)
if not ext or not new_filepath:
return
logs = self._controller.get_publish_report()
full_path = new_filepath + ext
dir_path = os.path.dirname(full_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
with open(full_path, "w") as file_stream:
json.dump(logs, file_stream)
self._controller.emit_card_message(
"Report saved",
CardMessageTypes.info)
class ErrorsMessageBox(ErrorMessageBox):
def __init__(self, error_title, failed_info, message_start, parent):
self._failed_info = failed_info
self._message_start = message_start
self._info_with_id = [
# Id must be string when used in tab widget
{"id": str(idx), "info": info}
for idx, info in enumerate(failed_info)
]
self._widgets_by_id = {}
self._tabs_widget = None
self._stack_layout = None
super(ErrorsMessageBox, self).__init__(error_title, parent)
layout = self.layout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
footer_layout = self._footer_widget.layout()
footer_layout.setContentsMargins(5, 5, 5, 5)
def _create_top_widget(self, parent_widget):
return None
def _get_report_data(self):
output = []
for info in self._failed_info:
item_label = info.get("label")
item_identifier = info["identifier"]
if item_label:
report_message = "{} ({})".format(
item_label, item_identifier)
else:
report_message = "{}".format(item_identifier)
if self._message_start:
report_message = "{} {}".format(
self._message_start, report_message
)
report_message += "\n\nError: {}".format(info["message"])
formatted_traceback = info.get("traceback")
if formatted_traceback:
report_message += "\n\n{}".format(formatted_traceback)
output.append(report_message)
return output
def _create_content(self, content_layout):
tabs_widget = PublisherTabsWidget(self)
stack_widget = QtWidgets.QFrame(self._content_widget)
stack_layout = QtWidgets.QStackedLayout(stack_widget)
first = True
for item in self._info_with_id:
item_id = item["id"]
info = item["info"]
message = info["message"]
formatted_traceback = info.get("traceback")
item_label = info.get("label")
if not item_label:
item_label = info["identifier"]
msg_widget = QtWidgets.QWidget(stack_widget)
msg_layout = QtWidgets.QVBoxLayout(msg_widget)
exc_msg_template = "<span style='font-weight:bold'>{}</span>"
message_label_widget = QtWidgets.QLabel(msg_widget)
message_label_widget.setText(
exc_msg_template.format(self.convert_text_for_html(message))
)
msg_layout.addWidget(message_label_widget, 0)
if formatted_traceback:
line_widget = self._create_line(msg_widget)
tb_widget = self._create_traceback_widget(formatted_traceback)
msg_layout.addWidget(line_widget, 0)
msg_layout.addWidget(tb_widget, 0)
msg_layout.addStretch(1)
tabs_widget.add_tab(item_label, item_id)
stack_layout.addWidget(msg_widget)
if first:
first = False
stack_layout.setCurrentWidget(msg_widget)
self._widgets_by_id[item_id] = msg_widget
content_layout.addWidget(tabs_widget, 0)
content_layout.addWidget(stack_widget, 1)
tabs_widget.tab_changed.connect(self._on_tab_change)
self._tabs_widget = tabs_widget
self._stack_layout = stack_layout
def _on_tab_change(self, old_identifier, identifier):
widget = self._widgets_by_id[identifier]
self._stack_layout.setCurrentWidget(widget)