|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2012 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a Notification widget. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 import enum |
|
12 |
|
13 from PyQt5.QtCore import Qt, QTimer, QPoint |
|
14 from PyQt5.QtWidgets import QFrame, QWidget, QVBoxLayout |
|
15 |
|
16 from .Ui_NotificationFrame import Ui_NotificationFrame |
|
17 |
|
18 import Globals |
|
19 import Preferences |
|
20 import UI.PixmapCache |
|
21 |
|
22 |
|
23 class NotificationTypes(enum.Enum): |
|
24 """ |
|
25 Class implementing the notification types. |
|
26 """ |
|
27 INFORMATION = 0 |
|
28 WARNING = 1 |
|
29 CRITICAL = 2 |
|
30 OTHER = 99 |
|
31 |
|
32 |
|
33 class NotificationFrame(QFrame, Ui_NotificationFrame): |
|
34 """ |
|
35 Class implementing a Notification widget. |
|
36 """ |
|
37 NotificationStyleSheetTemplate = "color:{0};background-color:{1};" |
|
38 |
|
39 def __init__(self, icon, heading, text, |
|
40 kind=NotificationTypes.INFORMATION, parent=None): |
|
41 """ |
|
42 Constructor |
|
43 |
|
44 @param icon icon to be used |
|
45 @type QPixmap |
|
46 @param heading heading to be used |
|
47 @type str |
|
48 @param text text to be used |
|
49 @type str |
|
50 @param kind kind of notification to be shown |
|
51 @type NotificationTypes |
|
52 @param parent reference to the parent widget |
|
53 @type QWidget |
|
54 """ |
|
55 super().__init__(parent) |
|
56 self.setupUi(self) |
|
57 |
|
58 self.layout().setAlignment( |
|
59 self.verticalLayout, |
|
60 Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter |
|
61 ) |
|
62 |
|
63 self.setStyleSheet(NotificationFrame.getStyleSheet(kind)) |
|
64 |
|
65 if icon is None: |
|
66 icon = NotificationFrame.getIcon(kind) |
|
67 self.icon.setPixmap(icon) |
|
68 |
|
69 self.heading.setText(heading) |
|
70 self.text.setText(text) |
|
71 |
|
72 self.show() |
|
73 self.adjustSize() |
|
74 |
|
75 @classmethod |
|
76 def getIcon(cls, kind): |
|
77 """ |
|
78 Class method to get the icon for a specific notification kind. |
|
79 |
|
80 @param kind notification kind |
|
81 @type NotificationTypes |
|
82 @return icon for the notification kind |
|
83 @rtype QPixmap |
|
84 """ |
|
85 if kind == NotificationTypes.CRITICAL: |
|
86 return UI.PixmapCache.getPixmap("notificationCritical48") |
|
87 elif kind == NotificationTypes.WARNING: # __NO-TASK__ |
|
88 return UI.PixmapCache.getPixmap("notificationWarning48") |
|
89 elif kind == NotificationTypes.INFORMATION: |
|
90 return UI.PixmapCache.getPixmap("notificationInformation48") |
|
91 else: |
|
92 return UI.PixmapCache.getPixmap("notification48") |
|
93 |
|
94 @classmethod |
|
95 def getStyleSheet(cls, kind): |
|
96 """ |
|
97 Class method to get a style sheet for specific notification kind. |
|
98 |
|
99 @param kind notification kind |
|
100 @type NotificationTypes |
|
101 @return string containing the style sheet for the notification kind |
|
102 @rtype str |
|
103 """ |
|
104 if kind == NotificationTypes.CRITICAL: |
|
105 return NotificationFrame.NotificationStyleSheetTemplate.format( |
|
106 Preferences.getUI("NotificationCriticalForeground"), |
|
107 Preferences.getUI("NotificationCriticalBackground") |
|
108 ) |
|
109 elif kind == NotificationTypes.WARNING: # __NO-TASK__ |
|
110 return NotificationFrame.NotificationStyleSheetTemplate.format( |
|
111 Preferences.getUI("NotificationWarningForeground"), |
|
112 Preferences.getUI("NotificationWarningBackground") |
|
113 ) |
|
114 else: |
|
115 return "" |
|
116 |
|
117 |
|
118 class NotificationWidget(QWidget): |
|
119 """ |
|
120 Class implementing a Notification list widget. |
|
121 """ |
|
122 def __init__(self, parent=None, setPosition=False): |
|
123 """ |
|
124 Constructor |
|
125 |
|
126 @param parent reference to the parent widget |
|
127 @type QWidget |
|
128 @param setPosition flag indicating to set the display |
|
129 position interactively |
|
130 @type bool |
|
131 """ |
|
132 super().__init__(parent) |
|
133 |
|
134 self.__layout = QVBoxLayout(self) |
|
135 self.__layout.setContentsMargins(0, 0, 0, 0) |
|
136 self.setLayout(self.__layout) |
|
137 |
|
138 self.__timeout = 5000 |
|
139 self.__dragPosition = QPoint() |
|
140 self.__timers = {} |
|
141 self.__notifications = [] |
|
142 |
|
143 self.__settingPosition = setPosition |
|
144 |
|
145 flags = ( |
|
146 Qt.WindowType.Tool | |
|
147 Qt.WindowType.FramelessWindowHint | |
|
148 Qt.WindowType.WindowStaysOnTopHint | |
|
149 Qt.WindowType.X11BypassWindowManagerHint |
|
150 ) |
|
151 if Globals.isWindowsPlatform(): |
|
152 flags |= Qt.WindowType.ToolTip |
|
153 self.setWindowFlags(flags) |
|
154 |
|
155 if self.__settingPosition: |
|
156 self.setCursor(Qt.CursorShape.OpenHandCursor) |
|
157 |
|
158 def showNotification(self, icon, heading, text, |
|
159 kind=NotificationTypes.INFORMATION, timeout=0): |
|
160 """ |
|
161 Public method to show a notification. |
|
162 |
|
163 @param icon icon to be used |
|
164 @type QPixmap |
|
165 @param heading heading to be used |
|
166 @type str |
|
167 @param text text to be used |
|
168 @type str |
|
169 @param kind kind of notification to be shown |
|
170 @type NotificationTypes |
|
171 @param timeout timeout in seconds after which the notification is |
|
172 to be removed (0 = do not remove until it is clicked on) |
|
173 @type int |
|
174 """ |
|
175 notificationFrame = NotificationFrame( |
|
176 icon, heading, text, kind=kind, parent=self) |
|
177 self.__layout.addWidget(notificationFrame) |
|
178 self.__notifications.append(notificationFrame) |
|
179 |
|
180 self.show() |
|
181 |
|
182 self.__adjustSizeAndPosition() |
|
183 |
|
184 if timeout: |
|
185 timer = QTimer() |
|
186 self.__timers[id(notificationFrame)] = timer |
|
187 timer.setSingleShot(True) |
|
188 timer.timeout.connect( |
|
189 lambda: self.__removeNotification(notificationFrame) |
|
190 ) |
|
191 timer.setInterval(timeout * 1000) |
|
192 timer.start() |
|
193 |
|
194 def __adjustSizeAndPosition(self): |
|
195 """ |
|
196 Private slot to adjust the notification list widget size and position. |
|
197 """ |
|
198 self.adjustSize() |
|
199 |
|
200 if not self.__settingPosition: |
|
201 pos = Preferences.getUI("NotificationPosition") |
|
202 try: |
|
203 screen = self.screen() |
|
204 except AttributeError: |
|
205 # < Qt 5.15 |
|
206 from PyQt5.QtGui import QGuiApplication |
|
207 screen = QGuiApplication.screenAt(pos) |
|
208 screenGeom = screen.geometry() |
|
209 |
|
210 newX = pos.x() |
|
211 newY = pos.y() |
|
212 if newX < screenGeom.x(): |
|
213 newX = screenGeom.x() |
|
214 if newY < screenGeom.y(): |
|
215 newY = screenGeom.y() |
|
216 if newX + self.width() > screenGeom.width(): |
|
217 newX = screenGeom.width() - self.width() |
|
218 if newY + self.height() > screenGeom.height(): |
|
219 newY = screenGeom.height() - self.height() |
|
220 |
|
221 self.move(newX, newY) |
|
222 |
|
223 def __removeNotification(self, notification): |
|
224 """ |
|
225 Private method to remove a notification from the list. |
|
226 |
|
227 @param notification reference to the notification to be removed |
|
228 @type NotificationFrame |
|
229 """ |
|
230 notification.hide() |
|
231 |
|
232 # delete timer of an auto close notification |
|
233 key = id(notification) |
|
234 if key in self.__timers: |
|
235 self.__timers[key].stop() |
|
236 del self.__timers[key] |
|
237 |
|
238 # delete the notification |
|
239 index = self.__layout.indexOf(notification) |
|
240 self.__layout.takeAt(index) |
|
241 with contextlib.suppress(ValueError): |
|
242 self.__notifications.remove(notification) |
|
243 notification.deleteLater() |
|
244 |
|
245 if self.__layout.count(): |
|
246 self.__adjustSizeAndPosition() |
|
247 else: |
|
248 self.hide() |
|
249 |
|
250 def mousePressEvent(self, evt): |
|
251 """ |
|
252 Protected method to handle presses of a mouse button. |
|
253 |
|
254 @param evt reference to the mouse event (QMouseEvent) |
|
255 """ |
|
256 if not self.__settingPosition: |
|
257 clickedLabel = self.childAt(evt.pos()) |
|
258 if clickedLabel: |
|
259 clickedNotification = clickedLabel.parent() |
|
260 self.__removeNotification(clickedNotification) |
|
261 return |
|
262 |
|
263 if evt.button() == Qt.MouseButton.LeftButton: |
|
264 self.__dragPosition = ( |
|
265 evt.globalPos() - self.frameGeometry().topLeft() |
|
266 ) |
|
267 self.setCursor(Qt.CursorShape.ClosedHandCursor) |
|
268 evt.accept() |
|
269 |
|
270 def mouseReleaseEvent(self, evt): |
|
271 """ |
|
272 Protected method to handle releases of a mouse button. |
|
273 |
|
274 @param evt reference to the mouse event (QMouseEvent) |
|
275 """ |
|
276 if ( |
|
277 self.__settingPosition and |
|
278 evt.button() == Qt.MouseButton.LeftButton |
|
279 ): |
|
280 self.setCursor(Qt.CursorShape.OpenHandCursor) |
|
281 |
|
282 def mouseMoveEvent(self, evt): |
|
283 """ |
|
284 Protected method to handle dragging the window. |
|
285 |
|
286 @param evt reference to the mouse event (QMouseEvent) |
|
287 """ |
|
288 if evt.buttons() & Qt.MouseButton.LeftButton: |
|
289 self.move(evt.globalPos() - self.__dragPosition) |
|
290 evt.accept() |