Merge pull request #3104 from pypeclub/enhancement/overlay_messages_object

Local Settings UI: Overlay messages on save and reset
This commit is contained in:
Jakub Trllo 2022-04-26 13:49:42 +02:00 committed by GitHub
commit 3092ba54f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 361 additions and 1 deletions

View file

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

View file

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

View file

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

View file

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

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