eric6/Snapshot/SnapshotRegionGrabber.py

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

eric ide

mercurial