src/eric7/Snapshot/SnapshotRegionGrabber.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a grabber widget for a rectangular snapshot region.
8 """
9
10 from PyQt6.QtCore import pyqtSignal, Qt, QRect, QPoint, QTimer, QLocale
11 from PyQt6.QtGui import (
12 QPixmap, QColor, QRegion, QPainter, QPalette, QPaintEngine, QPen, QBrush,
13 QGuiApplication, QCursor
14 )
15 from PyQt6.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(
290 evt.position().toPoint())
291 if evt.button() == Qt.MouseButton.LeftButton:
292 self.__mouseDown = True
293 self.__dragStartPoint = evt.position().toPoint()
294 self.__selectionBeforeDrag = QRect(self.__selection)
295 if not self.__selection.contains(evt.position().toPoint()):
296 self.__newSelection = True
297 self.__selection = QRect()
298 else:
299 self.setCursor(Qt.CursorShape.ClosedHandCursor)
300 elif evt.button() == Qt.MouseButton.RightButton:
301 self.__newSelection = False
302 self.__selection = QRect()
303 self.setCursor(Qt.CursorShape.CrossCursor)
304 self.update()
305
306 def mouseMoveEvent(self, evt):
307 """
308 Protected method to handle mouse movements.
309
310 @param evt mouse move event (QMouseEvent)
311 """
312 shouldShowHelp = not self.__helpTextRect.contains(
313 evt.position().toPoint())
314 if shouldShowHelp != self.__showHelp:
315 self.__showHelp = shouldShowHelp
316 self.update()
317
318 if self.__mouseDown:
319 if self.__newSelection:
320 p = evt.position().toPoint()
321 r = self.rect()
322 self.__selection = self.__normalizeSelection(
323 QRect(self.__dragStartPoint,
324 self.__limitPointToRect(p, r)))
325 elif self.__mouseOverHandle is None:
326 # moving the whole selection
327 r = self.rect().normalized()
328 s = self.__selectionBeforeDrag.normalized()
329 p = (
330 s.topLeft() + evt.position().toPoint() -
331 self.__dragStartPoint
332 )
333 r.setBottomRight(
334 r.bottomRight() - QPoint(s.width(), s.height()) +
335 QPoint(1, 1))
336 if not r.isNull() and r.isValid():
337 self.__selection.moveTo(self.__limitPointToRect(p, r))
338 else:
339 # dragging a handle
340 r = QRect(self.__selectionBeforeDrag)
341 offset = evt.position().toPoint() - self.__dragStartPoint
342
343 if self.__mouseOverHandle in [
344 self.__TLHandle, self.__THandle, self.__TRHandle]:
345 r.setTop(r.top() + offset.y())
346
347 if self.__mouseOverHandle in [
348 self.__TLHandle, self.__LHandle, self.__BLHandle]:
349 r.setLeft(r.left() + offset.x())
350
351 if self.__mouseOverHandle in [
352 self.__BLHandle, self.__BHandle, self.__BRHandle]:
353 r.setBottom(r.bottom() + offset.y())
354
355 if self.__mouseOverHandle in [
356 self.__TRHandle, self.__RHandle, self.__BRHandle]:
357 r.setRight(r.right() + offset.x())
358
359 r.setTopLeft(self.__limitPointToRect(r.topLeft(), self.rect()))
360 r.setBottomRight(
361 self.__limitPointToRect(r.bottomRight(), self.rect()))
362 self.__selection = self.__normalizeSelection(r)
363
364 self.update()
365 else:
366 if self.__selection.isNull():
367 return
368
369 found = False
370 for r in self.__handles:
371 if r.contains(evt.position().toPoint()):
372 self.__mouseOverHandle = r
373 found = True
374 break
375
376 if not found:
377 self.__mouseOverHandle = None
378 if self.__selection.contains(evt.position().toPoint()):
379 self.setCursor(Qt.CursorShape.OpenHandCursor)
380 else:
381 self.setCursor(Qt.CursorShape.CrossCursor)
382 else:
383 if self.__mouseOverHandle in [self.__TLHandle,
384 self.__BRHandle]:
385 self.setCursor(Qt.CursorShape.SizeFDiagCursor)
386 elif self.__mouseOverHandle in [self.__TRHandle,
387 self.__BLHandle]:
388 self.setCursor(Qt.CursorShape.SizeBDiagCursor)
389 elif self.__mouseOverHandle in [self.__LHandle,
390 self.__RHandle]:
391 self.setCursor(Qt.CursorShape.SizeHorCursor)
392 elif self.__mouseOverHandle in [self.__THandle,
393 self.__BHandle]:
394 self.setCursor(Qt.CursorShape.SizeVerCursor)
395
396 def mouseReleaseEvent(self, evt):
397 """
398 Protected method to handle mouse button releases.
399
400 @param evt mouse release event (QMouseEvent)
401 """
402 self.__mouseDown = False
403 self.__newSelection = False
404 if (
405 self.__mouseOverHandle is None and
406 self.__selection.contains(evt.position().toPoint())
407 ):
408 self.setCursor(Qt.CursorShape.OpenHandCursor)
409 self.update()
410
411 def mouseDoubleClickEvent(self, evt):
412 """
413 Protected method to handle mouse double clicks.
414
415 @param evt mouse double click event (QMouseEvent)
416 """
417 self.__grabRect()
418
419 def keyPressEvent(self, evt):
420 """
421 Protected method to handle key presses.
422
423 @param evt key press event (QKeyEvent)
424 """
425 if evt.key() == Qt.Key.Key_Escape:
426 self.grabbed.emit(QPixmap())
427 elif evt.key() in [Qt.Key.Key_Enter, Qt.Key.Key_Return]:
428 self.__grabRect()
429 else:
430 evt.ignore()
431
432 def __updateHandles(self):
433 """
434 Private method to update the handles.
435 """
436 r = QRect(self.__selection)
437 s2 = self.__handleSize // 2
438
439 self.__TLHandle.moveTopLeft(r.topLeft())
440 self.__TRHandle.moveTopRight(r.topRight())
441 self.__BLHandle.moveBottomLeft(r.bottomLeft())
442 self.__BRHandle.moveBottomRight(r.bottomRight())
443
444 self.__LHandle.moveTopLeft(
445 QPoint(r.x(), r.y() + r.height() // 2 - s2))
446 self.__THandle.moveTopLeft(
447 QPoint(r.x() + r.width() // 2 - s2, r.y()))
448 self.__RHandle.moveTopRight(
449 QPoint(r.right(), r.y() + r.height() // 2 - s2))
450 self.__BHandle.moveBottomLeft(
451 QPoint(r.x() + r.width() // 2 - s2, r.bottom()))
452
453 def __handleMask(self, maskType):
454 """
455 Private method to calculate the handle mask.
456
457 @param maskType type of the mask to be used
458 (SnapshotRegionGrabber.FillMask or
459 SnapshotRegionGrabber.StrokeMask)
460 @return calculated mask (QRegion)
461 """
462 mask = QRegion()
463 for rect in self.__handles:
464 if maskType == SnapshotRegionGrabber.StrokeMask:
465 r = QRegion(rect)
466 mask += r.subtracted(QRegion(rect.adjusted(1, 1, -1, -1)))
467 else:
468 mask += QRegion(rect.adjusted(1, 1, -1, -1))
469 return mask
470
471 def __limitPointToRect(self, point, rect):
472 """
473 Private method to limit the given point to the given rectangle.
474
475 @param point point to be limited (QPoint)
476 @param rect rectangle the point shall be limited to (QRect)
477 @return limited point (QPoint)
478 """
479 q = QPoint()
480 if point.x() < rect.x():
481 q.setX(rect.x())
482 elif point.x() < rect.right():
483 q.setX(point.x())
484 else:
485 q.setX(rect.right())
486 if point.y() < rect.y():
487 q.setY(rect.y())
488 elif point.y() < rect.bottom():
489 q.setY(point.y())
490 else:
491 q.setY(rect.bottom())
492 return q
493
494 def __normalizeSelection(self, sel):
495 """
496 Private method to normalize the given selection.
497
498 @param sel selection to be normalized (QRect)
499 @return normalized selection (QRect)
500 """
501 rect = QRect(sel)
502 if rect.width() <= 0:
503 left = rect.left()
504 width = rect.width()
505 rect.setLeft(left + width - 1)
506 rect.setRight(left)
507 if rect.height() <= 0:
508 top = rect.top()
509 height = rect.height()
510 rect.setTop(top + height - 1)
511 rect.setBottom(top)
512 return rect
513
514 def __grabRect(self):
515 """
516 Private method to grab the selected rectangle (i.e. do the snapshot).
517 """
518 if self.__mode == SnapshotRegionGrabber.Ellipse:
519 ell = QRegion(self.__selection, QRegion.RegionType.Ellipse)
520 if not ell.isEmpty():
521 self.__grabbing = True
522
523 xOffset = self.__pixmap.rect().x() - ell.boundingRect().x()
524 yOffset = self.__pixmap.rect().y() - ell.boundingRect().y()
525 translatedEll = ell.translated(xOffset, yOffset)
526
527 pixmap2 = QPixmap(ell.boundingRect().size())
528 pixmap2.fill(Qt.GlobalColor.transparent)
529
530 pt = QPainter()
531 pt.begin(pixmap2)
532 if pt.paintEngine().hasFeature(
533 QPaintEngine.PaintEngineFeature.PorterDuff
534 ):
535 pt.setRenderHints(
536 QPainter.RenderHint.Antialiasing |
537 QPainter.RenderHint.SmoothPixmapTransform,
538 True)
539 pt.setBrush(Qt.GlobalColor.black)
540 pt.setPen(QPen(QBrush(Qt.GlobalColor.black), 0.5))
541 pt.drawEllipse(translatedEll.boundingRect())
542 pt.setCompositionMode(
543 QPainter.CompositionMode.CompositionMode_SourceIn)
544 else:
545 pt.setClipRegion(translatedEll)
546 pt.setCompositionMode(
547 QPainter.CompositionMode.CompositionMode_Source)
548
549 pt.drawPixmap(pixmap2.rect(), self.__pixmap,
550 ell.boundingRect())
551 pt.end()
552
553 self.grabbed.emit(pixmap2)
554 else:
555 r = QRect(self.__selection)
556 if not r.isNull() and r.isValid():
557 self.__grabbing = True
558 self.grabbed.emit(self.__pixmap.copy(r))

eric ide

mercurial