mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #3104 from pypeclub/enhancement/overlay_messages_object
Local Settings UI: Overlay messages on save and reset
This commit is contained in:
commit
3092ba54f9
5 changed files with 361 additions and 1 deletions
|
|
@ -61,7 +61,11 @@
|
|||
"icon-entity-default": "#bfccd6",
|
||||
"icon-entity-disabled": "#808080",
|
||||
"font-entity-deprecated": "#666666",
|
||||
|
||||
"overlay-messages": {
|
||||
"close-btn": "#D3D8DE",
|
||||
"bg-success": "#458056",
|
||||
"bg-success-hover": "#55a066"
|
||||
},
|
||||
"tab-widget": {
|
||||
"bg": "#21252B",
|
||||
"bg-selected": "#434a56",
|
||||
|
|
|
|||
|
|
@ -687,6 +687,26 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
|||
background: none;
|
||||
}
|
||||
|
||||
/* Messages overlay */
|
||||
#OverlayMessageWidget {
|
||||
border-radius: 0.2em;
|
||||
background: {color:bg-buttons};
|
||||
}
|
||||
|
||||
#OverlayMessageWidget:hover {
|
||||
background: {color:bg-button-hover};
|
||||
}
|
||||
#OverlayMessageWidget {
|
||||
background: {color:overlay-messages:bg-success};
|
||||
}
|
||||
#OverlayMessageWidget:hover {
|
||||
background: {color:overlay-messages:bg-success-hover};
|
||||
}
|
||||
|
||||
#OverlayMessageWidget QWidget {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Password dialog*/
|
||||
#PasswordBtn {
|
||||
border: none;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from openpype.settings.lib import (
|
|||
save_local_settings
|
||||
)
|
||||
from openpype.tools.settings import CHILD_OFFSET
|
||||
from openpype.tools.utils import MessageOverlayObject
|
||||
from openpype.api import (
|
||||
Logger,
|
||||
SystemSettings,
|
||||
|
|
@ -221,6 +222,8 @@ class LocalSettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
self.setWindowTitle("OpenPype Local settings")
|
||||
|
||||
overlay_object = MessageOverlayObject(self)
|
||||
|
||||
stylesheet = style.load_stylesheet()
|
||||
self.setStyleSheet(stylesheet)
|
||||
self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))
|
||||
|
|
@ -247,6 +250,7 @@ class LocalSettingsWindow(QtWidgets.QWidget):
|
|||
save_btn.clicked.connect(self._on_save_clicked)
|
||||
reset_btn.clicked.connect(self._on_reset_clicked)
|
||||
|
||||
self._overlay_object = overlay_object
|
||||
# Do not create local settings widget in init phase as it's using
|
||||
# settings objects that must be OK to be able create this widget
|
||||
# - we want to show dialog if anything goes wrong
|
||||
|
|
@ -312,8 +316,10 @@ class LocalSettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
def _on_reset_clicked(self):
|
||||
self.reset()
|
||||
self._overlay_object.add_message("Refreshed...")
|
||||
|
||||
def _on_save_clicked(self):
|
||||
value = self._settings_widget.settings_value()
|
||||
save_local_settings(value)
|
||||
self._overlay_object.add_message("Saved...", message_type="success")
|
||||
self.reset()
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ from .lib import (
|
|||
from .models import (
|
||||
RecursiveSortFilterProxyModel,
|
||||
)
|
||||
from .overlay_messages import (
|
||||
MessageOverlayObject,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PlaceholderLineEdit",
|
||||
|
|
@ -45,4 +49,6 @@ __all__ = (
|
|||
"get_asset_icon",
|
||||
|
||||
"RecursiveSortFilterProxyModel",
|
||||
|
||||
"MessageOverlayObject",
|
||||
)
|
||||
|
|
|
|||
324
openpype/tools/utils/overlay_messages.py
Normal file
324
openpype/tools/utils/overlay_messages.py
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
import uuid
|
||||
|
||||
from Qt 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)
|
||||
colors = get_objected_colors()
|
||||
close_btn_color = 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 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):
|
||||
"""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.
|
||||
"""
|
||||
# Skip empty messages
|
||||
if not message:
|
||||
return
|
||||
|
||||
if timeout is None:
|
||||
timeout = self._default_timeout
|
||||
|
||||
# Create unique id of message
|
||||
label_id = str(uuid.uuid4())
|
||||
# Create message widget
|
||||
widget = OverlayMessageWidget(
|
||||
label_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[label_id] = widget
|
||||
self._messages_order.append(label_id)
|
||||
# Trigger recalculation timer
|
||||
self._recalculate_timer.start()
|
||||
|
||||
def _on_message_close_request(self, label_id):
|
||||
"""Message widget requested removement."""
|
||||
|
||||
widget = self._messages.get(label_id)
|
||||
if widget is not None:
|
||||
# Add message to closing messages and start recalculation
|
||||
self._closing_messages.add(label_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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue