diff -r 3fc8dfeb6ebe -r b99e7fd55fd3 src/eric7/UI/NotificationWidget.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/UI/NotificationWidget.py Thu Jul 07 11:23:56 2022 +0200 @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a Notification widget. +""" + +import contextlib +import enum + +from PyQt6.QtCore import Qt, QTimer, QPoint +from PyQt6.QtWidgets import QFrame, QWidget, QVBoxLayout + +from .Ui_NotificationFrame import Ui_NotificationFrame + +import Globals +import Preferences +import UI.PixmapCache + + +class NotificationTypes(enum.Enum): + """ + Class implementing the notification types. + """ + INFORMATION = 0 + WARNING = 1 + CRITICAL = 2 + OTHER = 99 + + +class NotificationFrame(QFrame, Ui_NotificationFrame): + """ + Class implementing a Notification widget. + """ + NotificationStyleSheetTemplate = "color:{0};background-color:{1};" + + def __init__(self, icon, heading, text, + kind=NotificationTypes.INFORMATION, parent=None): + """ + Constructor + + @param icon icon to be used + @type QPixmap + @param heading heading to be used + @type str + @param text text to be used + @type str + @param kind kind of notification to be shown + @type NotificationTypes + @param parent reference to the parent widget + @type QWidget + """ + super().__init__(parent) + self.setupUi(self) + + self.layout().setAlignment( + self.verticalLayout, + Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter + ) + + self.setStyleSheet(NotificationFrame.getStyleSheet(kind)) + + if icon is None: + icon = NotificationFrame.getIcon(kind) + self.icon.setPixmap(icon) + + self.heading.setText(heading) + self.text.setText(text) + + self.show() + self.adjustSize() + + @classmethod + def getIcon(cls, kind): + """ + Class method to get the icon for a specific notification kind. + + @param kind notification kind + @type NotificationTypes + @return icon for the notification kind + @rtype QPixmap + """ + if kind == NotificationTypes.CRITICAL: + return UI.PixmapCache.getPixmap("notificationCritical48") + elif kind == NotificationTypes.WARNING: # __NO-TASK__ + return UI.PixmapCache.getPixmap("notificationWarning48") + elif kind == NotificationTypes.INFORMATION: + return UI.PixmapCache.getPixmap("notificationInformation48") + else: + return UI.PixmapCache.getPixmap("notification48") + + @classmethod + def getStyleSheet(cls, kind): + """ + Class method to get a style sheet for specific notification kind. + + @param kind notification kind + @type NotificationTypes + @return string containing the style sheet for the notification kind + @rtype str + """ + if kind == NotificationTypes.CRITICAL: + return NotificationFrame.NotificationStyleSheetTemplate.format( + Preferences.getUI("NotificationCriticalForeground"), + Preferences.getUI("NotificationCriticalBackground") + ) + elif kind == NotificationTypes.WARNING: # __NO-TASK__ + return NotificationFrame.NotificationStyleSheetTemplate.format( + Preferences.getUI("NotificationWarningForeground"), + Preferences.getUI("NotificationWarningBackground") + ) + else: + return "" + + +class NotificationWidget(QWidget): + """ + Class implementing a Notification list widget. + """ + def __init__(self, parent=None, setPosition=False): + """ + Constructor + + @param parent reference to the parent widget + @type QWidget + @param setPosition flag indicating to set the display + position interactively + @type bool + """ + super().__init__(parent) + + self.__layout = QVBoxLayout(self) + self.__layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(self.__layout) + + self.__timeout = 5000 + self.__dragPosition = QPoint() + self.__timers = {} + self.__notifications = [] + + self.__settingPosition = setPosition + + flags = ( + Qt.WindowType.Tool | + Qt.WindowType.FramelessWindowHint | + Qt.WindowType.WindowStaysOnTopHint | + Qt.WindowType.X11BypassWindowManagerHint + ) + if Globals.isWindowsPlatform(): + flags |= Qt.WindowType.ToolTip + self.setWindowFlags(flags) + + if self.__settingPosition: + self.setCursor(Qt.CursorShape.OpenHandCursor) + + def showNotification(self, icon, heading, text, + kind=NotificationTypes.INFORMATION, timeout=0): + """ + Public method to show a notification. + + @param icon icon to be used + @type QPixmap + @param heading heading to be used + @type str + @param text text to be used + @type str + @param kind kind of notification to be shown + @type NotificationTypes + @param timeout timeout in seconds after which the notification is + to be removed (0 = do not remove until it is clicked on) + @type int + """ + notificationFrame = NotificationFrame( + icon, heading, text, kind=kind, parent=self) + self.__layout.addWidget(notificationFrame) + self.__notifications.append(notificationFrame) + + self.show() + + self.__adjustSizeAndPosition() + + if timeout: + timer = QTimer() + self.__timers[id(notificationFrame)] = timer + timer.setSingleShot(True) + timer.timeout.connect( + lambda: self.__removeNotification(notificationFrame) + ) + timer.setInterval(timeout * 1000) + timer.start() + + def __adjustSizeAndPosition(self): + """ + Private slot to adjust the notification list widget size and position. + """ + self.adjustSize() + + if not self.__settingPosition: + pos = Preferences.getUI("NotificationPosition") + try: + screen = self.screen() + except AttributeError: + # < Qt 5.15 + from PyQt6.QtGui import QGuiApplication + screen = QGuiApplication.screenAt(pos) + screenGeom = screen.geometry() + + newX = pos.x() + newY = pos.y() + if newX < screenGeom.x(): + newX = screenGeom.x() + if newY < screenGeom.y(): + newY = screenGeom.y() + if newX + self.width() > screenGeom.width(): + newX = screenGeom.width() - self.width() + if newY + self.height() > screenGeom.height(): + newY = screenGeom.height() - self.height() + + self.move(newX, newY) + + def __removeNotification(self, notification): + """ + Private method to remove a notification from the list. + + @param notification reference to the notification to be removed + @type NotificationFrame + """ + notification.hide() + + # delete timer of an auto close notification + key = id(notification) + if key in self.__timers: + self.__timers[key].stop() + del self.__timers[key] + + # delete the notification + index = self.__layout.indexOf(notification) + self.__layout.takeAt(index) + with contextlib.suppress(ValueError): + self.__notifications.remove(notification) + notification.deleteLater() + + if self.__layout.count(): + self.__adjustSizeAndPosition() + else: + self.hide() + + def mousePressEvent(self, evt): + """ + Protected method to handle presses of a mouse button. + + @param evt reference to the mouse event (QMouseEvent) + """ + if not self.__settingPosition: + clickedLabel = self.childAt(evt.position().toPoint()) + if clickedLabel: + clickedNotification = clickedLabel.parent() + self.__removeNotification(clickedNotification) + return + + if evt.button() == Qt.MouseButton.LeftButton: + self.__dragPosition = ( + evt.globalPosition().toPoint() - self.frameGeometry().topLeft() + ) + self.setCursor(Qt.CursorShape.ClosedHandCursor) + evt.accept() + + def mouseReleaseEvent(self, evt): + """ + Protected method to handle releases of a mouse button. + + @param evt reference to the mouse event (QMouseEvent) + """ + if ( + self.__settingPosition and + evt.button() == Qt.MouseButton.LeftButton + ): + self.setCursor(Qt.CursorShape.OpenHandCursor) + + def mouseMoveEvent(self, evt): + """ + Protected method to handle dragging the window. + + @param evt reference to the mouse event (QMouseEvent) + """ + if evt.buttons() & Qt.MouseButton.LeftButton: + self.move(evt.globalPosition().toPoint() - self.__dragPosition) + evt.accept()