|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2010 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing dialog-like popup that displays messages without |
|
8 interrupting the user. |
|
9 """ |
|
10 |
|
11 import enum |
|
12 |
|
13 from PyQt6.QtCore import pyqtSignal, Qt, QTimer, QPoint, QRect |
|
14 from PyQt6.QtWidgets import QFrame, QVBoxLayout, QApplication |
|
15 |
|
16 |
|
17 class EricPassivePopupStyle(enum.Enum): |
|
18 """ |
|
19 Class defining the popup styles. |
|
20 """ |
|
21 BOXED = 0 # box with no shadow |
|
22 STYLED = 1 # styled panel with no shadow |
|
23 CUSTOM = 128 # reserved for extensions |
|
24 |
|
25 |
|
26 class EricPassivePopup(QFrame): |
|
27 """ |
|
28 Class implementing dialog-like popup that displays messages without |
|
29 interrupting the user. |
|
30 |
|
31 @signal clicked emitted to indicate a mouse button click |
|
32 """ |
|
33 DefaultPopupTime = 6 * 1000 # time im milliseconds |
|
34 |
|
35 clicked = pyqtSignal((), (QPoint, )) |
|
36 |
|
37 def __init__(self, style=EricPassivePopupStyle.BOXED, parent=None): |
|
38 """ |
|
39 Constructor |
|
40 |
|
41 @param style style of the popup |
|
42 @type EricPassivePopupStyle |
|
43 @param parent reference to the parent widget |
|
44 @type QWidget |
|
45 """ |
|
46 super().__init__(None) |
|
47 |
|
48 self.__msgView = None |
|
49 self.__topLayout = None |
|
50 self.__hideDelay = EricPassivePopup.DefaultPopupTime |
|
51 self.__hideTimer = QTimer(self) |
|
52 self.__autoDelete = False |
|
53 self.__fixedPosition = QPoint() |
|
54 |
|
55 self.setWindowFlags( |
|
56 Qt.WindowType.Tool | |
|
57 Qt.WindowType.X11BypassWindowManagerHint | |
|
58 Qt.WindowType.WindowStaysOnTopHint | |
|
59 Qt.WindowType.FramelessWindowHint |
|
60 ) |
|
61 if style == EricPassivePopupStyle.STYLED: |
|
62 self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Plain) |
|
63 else: |
|
64 # default style is Boxed - Plain |
|
65 self.setFrameStyle(QFrame.Shape.Box | QFrame.Shadow.Plain) |
|
66 self.setLineWidth(2) |
|
67 self.__hideTimer.timeout.connect(self.hide) |
|
68 self.clicked.connect(self.hide) |
|
69 |
|
70 self.__customData = {} # dictionary to store some custom data |
|
71 |
|
72 def setView(self, child): |
|
73 """ |
|
74 Public method to set the message view. |
|
75 |
|
76 @param child reference to the widget to set as the message view |
|
77 (QWidget) |
|
78 """ |
|
79 self.__msgView = child |
|
80 self.__topLayout = QVBoxLayout(self) |
|
81 self.__topLayout.addWidget(self.__msgView) |
|
82 self.__topLayout.activate() |
|
83 |
|
84 def view(self): |
|
85 """ |
|
86 Public method to get a reference to the message view. |
|
87 |
|
88 @return reference to the message view (QWidget) |
|
89 """ |
|
90 return self.__msgView |
|
91 |
|
92 def setVisible(self, visible): |
|
93 """ |
|
94 Public method to show or hide the popup. |
|
95 |
|
96 @param visible flag indicating the visibility status (boolean) |
|
97 """ |
|
98 if not visible: |
|
99 super().setVisible(visible) |
|
100 return |
|
101 |
|
102 if self.size() != self.sizeHint(): |
|
103 self.resize(self.sizeHint()) |
|
104 |
|
105 if self.__fixedPosition.isNull(): |
|
106 self.__positionSelf() |
|
107 else: |
|
108 self.move(self.__fixedPosition) |
|
109 super().setVisible(True) |
|
110 |
|
111 delay = self.__hideDelay |
|
112 if delay < 0: |
|
113 delay = EricPassivePopup.DefaultPopupTime |
|
114 if delay > 0: |
|
115 self.__hideTimer.start(delay) |
|
116 |
|
117 def show(self, p=None): |
|
118 """ |
|
119 Public slot to show the popup. |
|
120 |
|
121 @param p position for the popup (QPoint) |
|
122 """ |
|
123 if p is not None: |
|
124 self.__fixedPosition = p |
|
125 super().show() |
|
126 |
|
127 def setTimeout(self, delay): |
|
128 """ |
|
129 Public method to set the delay for the popup is removed automatically. |
|
130 |
|
131 Setting the delay to 0 disables the timeout. If you're doing this, you |
|
132 may want to connect the clicked() signal to the hide() slot. Setting |
|
133 the delay to -1 makes it use the default value. |
|
134 |
|
135 @param delay value for the delay in milliseconds (integer) |
|
136 """ |
|
137 self.__hideDelay = delay |
|
138 if self.__hideTimer.isActive(): |
|
139 if delay: |
|
140 if delay == -1: |
|
141 delay = EricPassivePopup.DefaultPopupTime |
|
142 self.__hideTimer.start(delay) |
|
143 else: |
|
144 self.__hideTimer.stop() |
|
145 |
|
146 def timeout(self): |
|
147 """ |
|
148 Public method to get the delay before the popup is removed |
|
149 automatically. |
|
150 |
|
151 @return the delay before the popup is removed automatically (integer) |
|
152 """ |
|
153 return self.__hideDelay |
|
154 |
|
155 def mouseReleaseEvent(self, evt): |
|
156 """ |
|
157 Protected method to handle a mouse release event. |
|
158 |
|
159 @param evt reference to the mouse event (QMouseEvent) |
|
160 """ |
|
161 self.clicked.emit() |
|
162 self.clicked.emit(evt.position().toPoint()) |
|
163 |
|
164 def hideEvent(self, evt): |
|
165 """ |
|
166 Protected method to handle the hide event. |
|
167 |
|
168 @param evt reference to the hide event (QHideEvent) |
|
169 """ |
|
170 self.__hideTimer.stop() |
|
171 |
|
172 def __defaultArea(self): |
|
173 """ |
|
174 Private method to determine the default rectangle to be passed to |
|
175 moveNear(). |
|
176 |
|
177 @return default rectangle (QRect) |
|
178 """ |
|
179 return QRect(100, 100, 200, 200) |
|
180 |
|
181 def __positionSelf(self): |
|
182 """ |
|
183 Private method to position the popup. |
|
184 """ |
|
185 self.__moveNear(self.__defaultArea()) |
|
186 |
|
187 def __moveNear(self, target): |
|
188 """ |
|
189 Private method to move the popup to be adjacent to the specified |
|
190 rectangle. |
|
191 |
|
192 @param target rectangle to be placed at (QRect) |
|
193 """ |
|
194 pos = self.__calculateNearbyPoint(target) |
|
195 self.move(pos.x(), pos.y()) |
|
196 |
|
197 def __calculateNearbyPoint(self, target): |
|
198 """ |
|
199 Private method to calculate the position to place the popup near the |
|
200 specified rectangle. |
|
201 |
|
202 @param target rectangle to be placed at (QRect) |
|
203 @return position to place the popup (QPoint) |
|
204 """ |
|
205 pos = target.topLeft() |
|
206 x = pos.x() |
|
207 y = pos.y() |
|
208 w = self.minimumSizeHint().width() |
|
209 h = self.minimumSizeHint().height() |
|
210 |
|
211 r = QApplication.screenAt(QPoint(x + w // 2, y + h // 2)).geometry() |
|
212 |
|
213 if x < r.center().x(): |
|
214 x += target.width() |
|
215 else: |
|
216 x -= w |
|
217 |
|
218 # It's apparently trying to go off screen, so display it ALL at the |
|
219 # bottom. |
|
220 if (y + h) > r.bottom(): |
|
221 y = r.bottom() - h |
|
222 |
|
223 if (x + w) > r.right(): |
|
224 x = r.right() - w |
|
225 |
|
226 if y < r.top(): |
|
227 y = r.top() |
|
228 |
|
229 if x < r.left(): |
|
230 x = r.left() |
|
231 |
|
232 return QPoint(x, y) |
|
233 |
|
234 def setCustomData(self, key, data): |
|
235 """ |
|
236 Public method to set some custom data. |
|
237 |
|
238 @param key key for the custom data |
|
239 @type str |
|
240 @param data data to be stored |
|
241 @type any |
|
242 """ |
|
243 self.__customData[key] = data |
|
244 |
|
245 def getCustomData(self, key): |
|
246 """ |
|
247 Public method to get some custom data. |
|
248 |
|
249 @param key key for the custom data |
|
250 @type str |
|
251 @return stored data |
|
252 @rtype any |
|
253 """ |
|
254 return self.__customData[key] |