Snapshot/SnapshotRegionGrabber.py

changeset 1770
c17e67e69ef5
child 1772
f325dfdc8f6b
equal deleted inserted replaced
1768:8a04ce23e083 1770:c17e67e69ef5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the snapshot region grabber widget.
8 """
9
10 from PyQt4.QtCore import pyqtSignal, Qt, QRect, QPoint, QTimer
11 from PyQt4.QtGui import QWidget, QPixmap, QColor, QRegion, QApplication, QPainter, \
12 QPalette, QToolTip
13
14
15 def drawRect(painter, rect, outline, fill=QColor()):
16 """
17 Module function to draw a rectangle with the given parameters.
18
19 @param painter reference to the painter to be used (QPainter)
20 @param rect rectangle to be drawn (QRect)
21 @param outline color of the outline (QColor)
22 @param fill fill color (QColor)
23 """
24 clip = QRegion(rect)
25 clip = clip.subtracted(QRegion(rect.adjusted(1, 1, -1, -1)))
26
27 painter.save()
28 painter.setClipRegion(clip)
29 painter.setPen(Qt.NoPen)
30 painter.setBrush(outline)
31 painter.drawRect(rect)
32 if fill.isValid():
33 painter.setClipping(False)
34 painter.setBrush(fill)
35 painter.drawRect(rect.adjusted(1, 1, -1, -1))
36 painter.restore()
37
38
39 class SnapshotRegionGrabber(QWidget):
40 """
41 Class implementing the snapshot region grabber widget.
42
43 @signal grabbed(QPixmap) emitted after the region was grabbed
44 """
45 grabbed = pyqtSignal(QPixmap)
46
47 StrokeMask = 0
48 FillMask = 1
49
50 ModeFullscreen = 0
51 ModeRectangle = 1
52
53 def __init__(self, mode, delay):
54 """
55 Constructor
56
57 @param mode snapshot mode (SnapshotRegionGrabber.ModeFullscreen,
58 SnapshotRegionGrabber.ModeRectangle)
59 @param delay delay in seconds before taking the snapshot (int)
60 """
61 super().__init__(None,
62 Qt.X11BypassWindowManagerHint | Qt.WindowStaysOnTopHint |
63 Qt.FramelessWindowHint | Qt.Tool)
64
65 self.__mode = mode
66
67 self.__selection = QRect()
68 self.__mouseDown = False
69 self.__newSelection = False
70 self.__handleSize = 10
71 self.__mouseOverHandle = None
72 self.__showHelp = True
73 self.__grabbing = False
74 self.__dragStartPoint = QPoint()
75 self.__selectionBeforeDrag = QRect()
76
77 # naming conventions for handles
78 # T top, B bottom, R Right, L left
79 # 2 letters: a corner
80 # 1 letter: the handle on the middle of the corresponding side
81 self.__TLHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
82 self.__TRHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
83 self.__BLHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
84 self.__BRHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
85 self.__LHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
86 self.__THandle = QRect(0, 0, self.__handleSize, self.__handleSize)
87 self.__RHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
88 self.__BHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
89 self.__handles = [self.__TLHandle, self.__TRHandle, self.__BLHandle,
90 self.__BRHandle, self.__LHandle, self.__THandle,
91 self.__RHandle, self.__BHandle]
92 self.__helpTextRect = QRect()
93 self.__helpText = self.trUtf8(
94 "Select a region using the mouse. To take the snapshot, press the Enter key"
95 " or double click. Press Esc to quit.")
96
97 self.__pixmap = QPixmap()
98
99 self.setMouseTracking(True)
100
101 if delay == 0:
102 delay = 200
103 else:
104 delay = delay * 1000
105 QTimer.singleShot(delay, self.__initialize)
106
107 def __initialize(self):
108 """
109 Private slot to initialize the rest of the widget.
110 """
111 self.__pixmap = QPixmap.grabWindow(QApplication.desktop().winId())
112 if self.__mode == SnapshotRegionGrabber.ModeFullscreen:
113 self.grabbed.emit(self.__pixmap.copy())
114 else:
115 self.resize(self.__pixmap.size())
116 self.move(0, 0)
117 self.setCursor(Qt.CrossCursor)
118 self.show()
119
120 self.grabMouse()
121 self.grabKeyboard()
122
123 def paintEvent(self, evt):
124 """
125 Protected method handling paint events.
126
127 @param evt paint event (QPaintEvent)
128 """
129 if self.__grabbing: # grabWindow() should just get the background
130 return
131
132 painter = QPainter(self)
133 pal = QPalette(QToolTip.palette())
134 font = QToolTip.font()
135
136 handleColor = pal.color(QPalette.Active, QPalette.Highlight)
137 handleColor.setAlpha(160)
138 overlayColor = QColor(0, 0, 0, 160)
139 textColor = pal.color(QPalette.Active, QPalette.Text)
140 textBackgroundColor = pal.color(QPalette.Active, QPalette.Base)
141 painter.drawPixmap(0, 0, self.__pixmap)
142 painter.setFont(font)
143
144 r = QRect(self.__selection)
145 if not self.__selection.isNull():
146 grey = QRegion(self.rect())
147 grey = grey.subtracted(QRegion(r))
148 painter.setClipRegion(grey)
149 painter.setPen(Qt.NoPen)
150 painter.setBrush(overlayColor)
151 painter.drawRect(self.rect())
152 painter.setClipRegion(QRegion(self.rect()))
153 drawRect(painter, r, handleColor)
154
155 if self.__showHelp:
156 painter.setPen(textColor)
157 painter.setBrush(textBackgroundColor)
158 self.__helpTextRect = painter.boundingRect(self.rect().adjusted(2, 2, -2, -2),
159 Qt.TextWordWrap, self.__helpText)
160 self.__helpTextRect.adjust(-2, -2, 4, 2)
161 drawRect(painter, self.__helpTextRect, textColor, textBackgroundColor)
162 painter.drawText(self.__helpTextRect.adjusted(3, 3, -3, -3),
163 Qt.TextWordWrap, self.__helpText)
164
165 if self.__selection.isNull():
166 return
167
168 # The grabbed region is everything which is covered by the drawn
169 # rectangles (border included). This means that there is no 0px
170 # selection, since a 0px wide rectangle will always be drawn as a line.
171 txt = "{0}, {1} ({2}x{3})".format(
172 self.__selection.x(), self.__selection.y(),
173 self.__selection.width(), self.__selection.height())
174 textRect = painter.boundingRect(self.rect(), Qt.AlignLeft, txt)
175 boundingRect = textRect.adjusted(-4, 0, 0, 0)
176
177 if textRect.width() < r.width() - 2 * self.__handleSize and \
178 textRect.height() < r.height() - 2 * self.__handleSize and \
179 r.width() > 100 and \
180 r.height() > 100:
181 # center, unsuitable for small selections
182 boundingRect.moveCenter(r.center())
183 textRect.moveCenter(r.center())
184 elif r.y() - 3 > textRect.height() and \
185 r.x() + textRect.width() < self.rect().width():
186 # on top, left aligned
187 boundingRect.moveBottomLeft(QPoint(r.x(), r.y() - 3))
188 textRect.moveBottomLeft(QPoint(r.x() + 2, r.y() - 3))
189 elif r.x() - 3 > textRect.width():
190 # left, top aligned
191 boundingRect.moveTopRight(QPoint(r.x() - 3, r.y()))
192 textRect.moveTopRight(QPoint(r.x() - 5, r.y()))
193 elif r.bottom() + 3 + textRect.height() < self.rect().bottom() and \
194 r.right() > textRect.width():
195 # at bottom, right aligned
196 boundingRect.moveTopRight(QPoint(r.right(), r.bottom() + 3))
197 textRect.moveTopRight(QPoint(r.right() - 2, r.bottom() + 3))
198 elif r.right() + textRect.width() + 3 < self.rect().width():
199 # right, bottom aligned
200 boundingRect.moveBottomLeft(QPoint(r.right() + 3, r.bottom()))
201 textRect.moveBottomLeft(QPoint(r.right() + 5, r.bottom()))
202
203 # If the above didn't catch it, you are running on a very tiny screen...
204 drawRect(painter, boundingRect, textColor, textBackgroundColor)
205 painter.drawText(textRect, Qt.AlignHCenter, txt)
206
207 if (r.height() > self.__handleSize * 2 and \
208 r.width() > self.__handleSize * 2) or \
209 not self.__mouseDown:
210 self.__updateHandles()
211 painter.setPen(Qt.NoPen)
212 painter.setBrush(handleColor)
213 painter.setClipRegion(self.__handleMask(SnapshotRegionGrabber.StrokeMask))
214 painter.drawRect(self.rect())
215 handleColor.setAlpha(60)
216 painter.setBrush(handleColor)
217 painter.setClipRegion(self.__handleMask(SnapshotRegionGrabber.FillMask))
218 painter.drawRect(self.rect())
219
220 def resizeEvent(self, evt):
221 """
222 Protected method to handle resize events.
223
224 @param evt resize event (QResizeEvent)
225 """
226 if self.__selection.isNull():
227 return
228
229 r = QRect(self.__selection)
230 r.setTopLeft(self.__limitPointToRect(r.topLeft(), self.rect()))
231 r.setBottomRight(self.__limitPointToRect(r.bottomRight(), self.rect()))
232 if r.width() <= 1 or r.height() <= 1:
233 # This just results in ugly drawing...
234 self.__selection = QRect()
235 else:
236 self.__selection = self.__normalizeSelection(r)
237
238 def mousePressEvent(self, evt):
239 """
240 Protected method to handle mouse button presses.
241
242 @param evt mouse press event (QMouseEvent)
243 """
244 self.__showHelp = not self.__helpTextRect.contains(evt.pos())
245 if evt.button() == Qt.LeftButton:
246 self.__mouseDown = True
247 self.__dragStartPoint = evt.pos()
248 self.__selectionBeforeDrag = QRect(self.__selection)
249 if not self.__selection.contains(evt.pos()):
250 self.__newSelection = True
251 self.__selection = QRect()
252 else:
253 self.setCursor(Qt.ClosedHandCursor)
254 elif evt.button() == Qt.RightButton:
255 self.__newSelection = False
256 self.__selection = QRect()
257 self.setCursor(Qt.CrossCursor)
258 self.update()
259
260 def mouseMoveEvent(self, evt):
261 """
262 Protected method to handle mouse movements.
263
264 @param evt mouse move event (QMouseEvent)
265 """
266 shouldShowHelp = not self.__helpTextRect.contains(evt.pos())
267 if shouldShowHelp != self.__showHelp:
268 self.__showHelp = shouldShowHelp
269 self.update()
270
271 if self.__mouseDown:
272 if self.__newSelection:
273 p = evt.pos()
274 r = self.rect()
275 self.__selection = self.__normalizeSelection(
276 QRect(self.__dragStartPoint, self.__limitPointToRect(p, r)))
277 elif self.__mouseOverHandle is None:
278 # moving the whole selection
279 r = self.rect().normalized()
280 s = self.__selectionBeforeDrag.normalized()
281 p = s.topLeft() + evt.pos() - self.__dragStartPoint
282 r.setBottomRight(
283 r.bottomRight() - QPoint(s.width(), s.height()) + QPoint(1, 1))
284 if not r.isNull() and r.isValid():
285 self.__selection.moveTo(self.__limitPointToRect(p, r))
286 else:
287 # dragging a handle
288 r = QRect(self.__selectionBeforeDrag)
289 offset = evt.pos() - self.__dragStartPoint
290
291 if self.__mouseOverHandle in \
292 [self.__TLHandle, self.__THandle, self.__TRHandle]:
293 r.setTop(r.top() + offset.y())
294
295 if self.__mouseOverHandle in \
296 [self.__TLHandle, self.__LHandle, self.__BLHandle]:
297 r.setLeft(r.left() + offset.x())
298
299 if self.__mouseOverHandle in \
300 [self.__BLHandle, self.__BHandle, self.__BRHandle]:
301 r.setBottom(r.bottom() + offset.y())
302
303 if self.__mouseOverHandle in \
304 [self.__TRHandle, self.__RHandle, self.__BRHandle]:
305 r.setRight(r.right() + offset.x())
306
307 r.setTopLeft(self.__limitPointToRect(r.topLeft(), self.rect()))
308 r.setBottomRight(self.__limitPointToRect(r.bottomRight(), self.rect()))
309 self.__selection = self.__normalizeSelection(r)
310
311 self.update()
312 else:
313 if self.__selection.isNull():
314 return
315
316 found = False
317 for r in self.__handles:
318 if r.contains(evt.pos()):
319 self.__mouseOverHandle = r
320 found = True
321 break
322
323 if not found:
324 self.__mouseOverHandle = None
325 if self.__selection.contains(evt.pos()):
326 self.setCursor(Qt.OpenHandCursor)
327 else:
328 self.setCursor(Qt.CrossCursor)
329 else:
330 if self.__mouseOverHandle in [self.__TLHandle, self.__BRHandle]:
331 self.setCursor(Qt.SizeFDiagCursor)
332 elif self.__mouseOverHandle in [self.__TRHandle, self.__BLHandle]:
333 self.setCursor(Qt.SizeBDiagCursor)
334 elif self.__mouseOverHandle in [self.__LHandle, self.__RHandle]:
335 self.setCursor(Qt.SizeHorCursor)
336 elif self.__mouseOverHandle in [self.__THandle, self.__BHandle]:
337 self.setCursor(Qt.SizeVerCursor)
338
339 def mouseReleaseEvent(self, evt):
340 """
341 Protected method to handle mouse button releases.
342
343 @param evt mouse release event (QMouseEvent)
344 """
345 self.__mouseDown = False
346 self.__newSelection = False
347 if self.__mouseOverHandle is None and \
348 self.__selection.contains(evt.pos()):
349 self.setCursor(Qt.OpenHandCursor)
350 self.update()
351
352 def mouseDoubleClickEvent(self, evt):
353 """
354 Protected method to handle mouse double clicks.
355
356 @param evt mouse double click event (QMouseEvent)
357 """
358 self.__grabRect()
359
360 def keyPressEvent(self, evt):
361 """
362 Protected method to handle key presses.
363
364 @param evt key press event (QKeyEvent)
365 """
366 if evt.key() == Qt.Key_Escape:
367 self.grabbed.emit(QPixmap())
368 elif evt.key() in [Qt.Key_Enter, Qt.Key_Return]:
369 self.__grabRect()
370 else:
371 evt.ignore()
372
373 def __updateHandles(self):
374 """
375 Private method to update the handles.
376 """
377 r = QRect(self.__selection)
378 s2 = self.__handleSize // 2
379
380 self.__TLHandle.moveTopLeft(r.topLeft())
381 self.__TRHandle.moveTopRight(r.topRight())
382 self.__BLHandle.moveBottomLeft(r.bottomLeft())
383 self.__BRHandle.moveBottomRight(r.bottomRight())
384
385 self.__LHandle.moveTopLeft(QPoint(r.x(), r.y() + r.height() // 2 - s2))
386 self.__THandle.moveTopLeft(QPoint(r.x() + r.width() // 2 - s2, r.y()))
387 self.__RHandle.moveTopRight(QPoint(r.right(), r.y() + r.height() // 2 - s2))
388 self.__BHandle.moveBottomLeft(QPoint(r.x() + r.width() // 2 - s2, r.bottom()))
389
390 def __handleMask(self, maskType):
391 """
392 Private method to calculate the handle mask.
393
394 @param maskType type of the mask to be used (SnapshotRegionGrabber.FillMask or
395 SnapshotRegionGrabber.StrokeMask)
396 @return calculated mask (QRegion)
397 """
398 mask = QRegion()
399 for rect in self.__handles:
400 if maskType == SnapshotRegionGrabber.StrokeMask:
401 r = QRegion(rect)
402 mask += r.subtracted(QRegion(rect.adjusted(1, 1, -1, -1)))
403 else:
404 mask += QRegion(rect.adjusted(1, 1, -1, -1))
405 return mask
406
407 def __limitPointToRect(self, point, rect):
408 """
409 Private method to limit the given point to the given rectangle.
410
411 @param point point to be limited (QPoint)
412 @param rect rectangle the point shall be limited to (QRect)
413 @return limited point (QPoint)
414 """
415 q = QPoint()
416 if point.x() < rect.x():
417 q.setX(rect.x())
418 elif point.x() < rect.right():
419 q.setX(point.x())
420 else:
421 q.setX(rect.right())
422 if point.y() < rect.y():
423 q.setY(rect.y())
424 elif point.y() < rect.bottom():
425 q.setY(point.y())
426 else:
427 q.setY(rect.bottom())
428 return q
429
430 def __normalizeSelection(self, sel):
431 """
432 Private method to normalize the given selection.
433
434 @param sel selection to be normalized (QRect)
435 @return normalized selection (QRect)
436 """
437 r = QRect(sel)
438 if r.width() <= 0:
439 l = r.left()
440 w = r.width()
441 r.setLeft(l + w -1)
442 r.setRight(l)
443 if r.height() <= 0:
444 t = r.top()
445 h = r.height()
446 r.setTop(t + h - 1)
447 r.setBottom(t)
448 return r
449
450 def __grabRect(self):
451 """
452 Private method to grab the selected rectangle (i.e. do the snapshot).
453 """
454 r = QRect(self.__selection)
455 if not r.isNull() and r.isValid():
456 self.__grabbing = True
457 self.grabbed.emit(self.__pixmap.copy(r))

eric ide

mercurial