mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 21:32:15 +01:00
* make sure the message type is set and unset correctly * Update dummy data in readme * remove debug message from main thread callbacks * removed unused import * cleanup code in muster addon * simplified 'get_publish_instance_label' function * even better json file handling Co-authored-by: Roy Nieterau <roy_nieterau@hotmail.com> --------- Co-authored-by: Roy Nieterau <roy_nieterau@hotmail.com>
357 lines
12 KiB
Python
357 lines
12 KiB
Python
import uuid
|
|
|
|
from qtpy import QtWidgets, QtCore, QtGui
|
|
|
|
from openpype.style import get_objected_colors
|
|
|
|
from .lib import set_style_property
|
|
|
|
|
|
class CloseButton(QtWidgets.QFrame):
|
|
"""Close button drawed manually."""
|
|
|
|
clicked = QtCore.Signal()
|
|
|
|
def __init__(self, parent):
|
|
super(CloseButton, self).__init__(parent)
|
|
close_btn_color = get_objected_colors("overlay-messages", "close-btn")
|
|
self._color = close_btn_color.get_qcolor()
|
|
self._mouse_pressed = False
|
|
policy = QtWidgets.QSizePolicy(
|
|
QtWidgets.QSizePolicy.Fixed,
|
|
QtWidgets.QSizePolicy.Fixed
|
|
)
|
|
self.setSizePolicy(policy)
|
|
|
|
def sizeHint(self):
|
|
size = self.fontMetrics().height()
|
|
return QtCore.QSize(size, size)
|
|
|
|
def mousePressEvent(self, event):
|
|
if event.button() == QtCore.Qt.LeftButton:
|
|
self._mouse_pressed = True
|
|
super(CloseButton, self).mousePressEvent(event)
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
if self._mouse_pressed:
|
|
self._mouse_pressed = False
|
|
if self.rect().contains(event.pos()):
|
|
self.clicked.emit()
|
|
|
|
super(CloseButton, self).mouseReleaseEvent(event)
|
|
|
|
def paintEvent(self, event):
|
|
rect = self.rect()
|
|
painter = QtGui.QPainter(self)
|
|
painter.setClipRect(event.rect())
|
|
pen = QtGui.QPen()
|
|
pen.setWidth(2)
|
|
pen.setColor(self._color)
|
|
pen.setStyle(QtCore.Qt.SolidLine)
|
|
pen.setCapStyle(QtCore.Qt.RoundCap)
|
|
painter.setPen(pen)
|
|
offset = int(rect.height() / 4)
|
|
top = rect.top() + offset
|
|
left = rect.left() + offset
|
|
right = rect.right() - offset
|
|
bottom = rect.bottom() - offset
|
|
painter.drawLine(
|
|
left, top,
|
|
right, bottom
|
|
)
|
|
painter.drawLine(
|
|
left, bottom,
|
|
right, top
|
|
)
|
|
|
|
|
|
class OverlayMessageWidget(QtWidgets.QFrame):
|
|
"""Message widget showed as overlay.
|
|
|
|
Message is hidden after timeout but can be overriden by mouse hover.
|
|
Mouse hover can add additional 2 seconds of widget's visibility.
|
|
|
|
Args:
|
|
message_id (str): Unique identifier of message widget for
|
|
'MessageOverlayObject'.
|
|
message (str): Text shown in message.
|
|
parent (QWidget): Parent widget where message is visible.
|
|
timeout (int): Timeout of message's visibility (default 5000).
|
|
message_type (str): Property which can be used in styles for specific
|
|
kid of message.
|
|
"""
|
|
|
|
close_requested = QtCore.Signal(str)
|
|
_default_timeout = 5000
|
|
|
|
def __init__(
|
|
self, message_id, message, parent, message_type=None, timeout=None
|
|
):
|
|
super(OverlayMessageWidget, self).__init__(parent)
|
|
self.setObjectName("OverlayMessageWidget")
|
|
|
|
if message_type:
|
|
set_style_property(self, "type", message_type)
|
|
|
|
if not timeout:
|
|
timeout = self._default_timeout
|
|
timeout_timer = QtCore.QTimer()
|
|
timeout_timer.setInterval(timeout)
|
|
timeout_timer.setSingleShot(True)
|
|
|
|
hover_timer = QtCore.QTimer()
|
|
hover_timer.setInterval(2000)
|
|
hover_timer.setSingleShot(True)
|
|
|
|
label_widget = QtWidgets.QLabel(message, self)
|
|
label_widget.setAlignment(QtCore.Qt.AlignCenter)
|
|
label_widget.setWordWrap(True)
|
|
close_btn = CloseButton(self)
|
|
|
|
layout = QtWidgets.QHBoxLayout(self)
|
|
layout.setContentsMargins(5, 5, 0, 5)
|
|
layout.addWidget(label_widget, 1)
|
|
layout.addWidget(close_btn, 0)
|
|
|
|
close_btn.clicked.connect(self._on_close_clicked)
|
|
timeout_timer.timeout.connect(self._on_timer_timeout)
|
|
hover_timer.timeout.connect(self._on_hover_timeout)
|
|
|
|
self._label_widget = label_widget
|
|
self._message_id = message_id
|
|
self._timeout_timer = timeout_timer
|
|
self._hover_timer = hover_timer
|
|
|
|
def update_message(self, message, message_type=None, timeout=None):
|
|
self._label_widget.setText(message)
|
|
if timeout:
|
|
self._timeout_timer.setInterval(timeout)
|
|
|
|
set_style_property(self, "type", message_type)
|
|
|
|
self._timeout_timer.start()
|
|
|
|
def size_hint_without_word_wrap(self):
|
|
"""Size hint in cases that word wrap of label is disabled."""
|
|
self._label_widget.setWordWrap(False)
|
|
size_hint = self.sizeHint()
|
|
self._label_widget.setWordWrap(True)
|
|
return size_hint
|
|
|
|
def showEvent(self, event):
|
|
"""Start timeout on show."""
|
|
super(OverlayMessageWidget, self).showEvent(event)
|
|
self._timeout_timer.start()
|
|
|
|
def _on_timer_timeout(self):
|
|
"""On message timeout."""
|
|
# Skip closing if hover timer is active
|
|
if not self._hover_timer.isActive():
|
|
self._close_message()
|
|
|
|
def _on_hover_timeout(self):
|
|
"""Hover timer timed out."""
|
|
# Check if is still under widget
|
|
if self.underMouse():
|
|
self._hover_timer.start()
|
|
else:
|
|
self._close_message()
|
|
|
|
def _on_close_clicked(self):
|
|
self._close_message()
|
|
|
|
def _close_message(self):
|
|
"""Emmit close request to 'MessageOverlayObject'."""
|
|
self.close_requested.emit(self._message_id)
|
|
|
|
def enterEvent(self, event):
|
|
"""Start hover timer on hover."""
|
|
super(OverlayMessageWidget, self).enterEvent(event)
|
|
self._hover_timer.start()
|
|
|
|
def leaveEvent(self, event):
|
|
"""Start hover timer on hover leave."""
|
|
super(OverlayMessageWidget, self).leaveEvent(event)
|
|
self._hover_timer.start()
|
|
|
|
|
|
class MessageOverlayObject(QtCore.QObject):
|
|
"""Object that can be used to add overlay messages.
|
|
|
|
Args:
|
|
widget (QWidget):
|
|
"""
|
|
|
|
def __init__(self, widget, default_timeout=None):
|
|
super(MessageOverlayObject, self).__init__()
|
|
|
|
widget.installEventFilter(self)
|
|
|
|
# Timer which triggers recalculation of message positions
|
|
recalculate_timer = QtCore.QTimer()
|
|
recalculate_timer.setInterval(10)
|
|
|
|
recalculate_timer.timeout.connect(self._recalculate_positions)
|
|
|
|
self._widget = widget
|
|
self._recalculate_timer = recalculate_timer
|
|
|
|
self._messages_order = []
|
|
self._closing_messages = set()
|
|
self._messages = {}
|
|
self._spacing = 5
|
|
self._move_size = 4
|
|
self._move_size_remove = 8
|
|
self._default_timeout = default_timeout
|
|
|
|
def add_message(
|
|
self, message, message_type=None, timeout=None, message_id=None
|
|
):
|
|
"""Add single message into overlay.
|
|
|
|
Args:
|
|
message (str): Message that will be shown.
|
|
timeout (int): Message timeout.
|
|
message_type (str): Message type can be used as property in
|
|
stylesheets.
|
|
message_id (str): UUID of already existing message to update
|
|
it's message and timeout. Is created with different id if is
|
|
not available anymore.
|
|
|
|
Returns:
|
|
str: UUID of message which can be used to update message.
|
|
"""
|
|
# Skip empty messages
|
|
if not message:
|
|
return
|
|
|
|
if timeout is None:
|
|
timeout = self._default_timeout
|
|
|
|
# Create unique id of message
|
|
widget = None
|
|
if message_id is not None:
|
|
widget = self._messages.get(message_id)
|
|
|
|
if message_id is None:
|
|
message_id = str(uuid.uuid4())
|
|
|
|
elif message_id in self._messages_order:
|
|
self._messages_order.remove(message_id)
|
|
|
|
if widget is not None:
|
|
# NOTE: Update of message won't change paint order which should be
|
|
# ok in most of cases as it matters only when messages are
|
|
# animated
|
|
widget.update_message(message, message_type, timeout)
|
|
else:
|
|
# Create message widget
|
|
widget = OverlayMessageWidget(
|
|
message_id, message, self._widget, message_type, timeout
|
|
)
|
|
widget.close_requested.connect(self._on_message_close_request)
|
|
widget.show()
|
|
|
|
# Move widget outside of window
|
|
pos = widget.pos()
|
|
pos.setY(pos.y() - widget.height())
|
|
widget.move(pos)
|
|
# Store message
|
|
self._messages[message_id] = widget
|
|
self._messages_order.append(message_id)
|
|
# Trigger recalculation timer
|
|
self._recalculate_timer.start()
|
|
|
|
return message_id
|
|
|
|
def _on_message_close_request(self, message_id):
|
|
"""Message widget requested removement."""
|
|
|
|
widget = self._messages.get(message_id)
|
|
if widget is not None:
|
|
# Add message to closing messages and start recalculation
|
|
self._closing_messages.add(message_id)
|
|
self._recalculate_timer.start()
|
|
|
|
def _recalculate_positions(self):
|
|
"""Recalculate positions of widgets."""
|
|
|
|
# Skip if there are no messages to process
|
|
if not self._messages_order:
|
|
self._recalculate_timer.stop()
|
|
return
|
|
|
|
# All message widgets are in expected positions
|
|
all_at_place = True
|
|
# Starting y position
|
|
pos_y = self._spacing
|
|
# Current widget width
|
|
widget_width = self._widget.width()
|
|
max_width = widget_width - (2 * self._spacing)
|
|
widget_half_width = widget_width / 2
|
|
|
|
# Store message ids that should be removed
|
|
message_ids_to_remove = set()
|
|
for message_id in reversed(self._messages_order):
|
|
widget = self._messages[message_id]
|
|
pos = widget.pos()
|
|
# Messages to remove are moved upwards
|
|
if message_id in self._closing_messages:
|
|
bottom = pos.y() + widget.height()
|
|
# Add message to remove if is not visible
|
|
if bottom < 0 or self._move_size_remove < 1:
|
|
message_ids_to_remove.add(message_id)
|
|
continue
|
|
|
|
# Calculate new y position of message
|
|
dst_pos_y = pos.y() - self._move_size_remove
|
|
|
|
else:
|
|
# Calculate y position of message
|
|
# - use y position of previous message widget and add
|
|
# move size if is not in final destination yet
|
|
if widget.underMouse():
|
|
dst_pos_y = pos.y()
|
|
elif pos.y() == pos_y or self._move_size < 1:
|
|
dst_pos_y = pos_y
|
|
elif pos.y() < pos_y:
|
|
dst_pos_y = min(pos_y, pos.y() + self._move_size)
|
|
else:
|
|
dst_pos_y = max(pos_y, pos.y() - self._move_size)
|
|
|
|
# Store if widget is in place where should be
|
|
if all_at_place and dst_pos_y != pos_y:
|
|
all_at_place = False
|
|
|
|
# Calculate ideal width and height of message widget
|
|
height = widget.heightForWidth(max_width)
|
|
w_size_hint = widget.size_hint_without_word_wrap()
|
|
widget.resize(min(max_width, w_size_hint.width()), height)
|
|
|
|
# Center message widget
|
|
size = widget.size()
|
|
pos_x = widget_half_width - (size.width() / 2)
|
|
# Move widget to destination position
|
|
widget.move(pos_x, dst_pos_y)
|
|
|
|
# Add message widget height and spacing for next message widget
|
|
pos_y += size.height() + self._spacing
|
|
|
|
# Remove widgets to remove
|
|
for message_id in message_ids_to_remove:
|
|
self._messages_order.remove(message_id)
|
|
self._closing_messages.remove(message_id)
|
|
widget = self._messages.pop(message_id)
|
|
widget.hide()
|
|
widget.deleteLater()
|
|
|
|
# Stop recalculation timer if all widgets are where should be
|
|
if all_at_place:
|
|
self._recalculate_timer.stop()
|
|
|
|
def eventFilter(self, source, event):
|
|
# Trigger recalculation of timer on resize of widget
|
|
if source is self._widget and event.type() == QtCore.QEvent.Resize:
|
|
self._recalculate_timer.start()
|
|
|
|
return super(MessageOverlayObject, self).eventFilter(source, event)
|