eric7/Snapshot/SnapshotRegionGrabber.py

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

eric ide

mercurial