src/eric7/Snapshot/SnapshotFreehandGrabber.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 freehand 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, QPolygon, QPen, QBrush,
13 QPaintEngine, QGuiApplication, QCursor
14 )
15 from PyQt6.QtWidgets import QWidget, QToolTip
16
17 import Globals
18
19
20 def drawPolygon(painter, polygon, outline, fill=None):
21 """
22 Module function to draw a polygon with the given parameters.
23
24 @param painter reference to the painter to be used (QPainter)
25 @param polygon polygon to be drawn (QPolygon)
26 @param outline color of the outline (QColor)
27 @param fill fill color (QColor)
28 """
29 clip = QRegion(polygon)
30 clip -= QRegion(polygon)
31 pen = QPen(outline, 1, Qt.PenStyle.SolidLine, Qt.PenCapStyle.SquareCap,
32 Qt.PenJoinStyle.BevelJoin)
33
34 painter.save()
35 painter.setClipRegion(clip)
36 painter.setPen(pen)
37 painter.drawPolygon(QPolygon(polygon))
38 if fill and fill.isValid():
39 painter.setClipping(False)
40 painter.setBrush(fill or QColor())
41 painter.drawPolygon(QPolygon(polygon))
42 painter.restore()
43
44
45 class SnapshotFreehandGrabber(QWidget):
46 """
47 Class implementing a grabber widget for a freehand snapshot region.
48
49 @signal grabbed(QPixmap) emitted after the region was grabbed
50 """
51 grabbed = pyqtSignal(QPixmap)
52
53 def __init__(self):
54 """
55 Constructor
56 """
57 super().__init__(
58 None,
59 Qt.WindowType.X11BypassWindowManagerHint |
60 Qt.WindowType.WindowStaysOnTopHint |
61 Qt.WindowType.FramelessWindowHint |
62 Qt.WindowType.Tool
63 )
64
65 self.__selection = QPolygon()
66 self.__mouseDown = False
67 self.__newSelection = False
68 self.__handleSize = 10
69 self.__showHelp = True
70 self.__grabbing = False
71 self.__dragStartPoint = QPoint()
72 self.__selectionBeforeDrag = QPolygon()
73 self.__locale = QLocale()
74
75 self.__helpTextRect = QRect()
76 self.__helpText = self.tr(
77 "Select a region using the mouse. To take the snapshot,"
78 " press the Enter key or double click. Press Esc to quit.")
79
80 self.__pixmap = QPixmap()
81 self.__pBefore = QPoint()
82
83 self.setMouseTracking(True)
84
85 QTimer.singleShot(200, self.__initialize)
86
87 def __initialize(self):
88 """
89 Private slot to initialize the rest of the widget.
90 """
91 if Globals.isMacPlatform():
92 # macOS variant
93 screen = QGuiApplication.screenAt(QCursor.pos())
94 geom = screen.geometry()
95 self.__pixmap = screen.grabWindow(
96 0, geom.x(), geom.y(), geom.width(), geom.height())
97 else:
98 # Linux variant
99 # Windows variant
100 screen = QGuiApplication.screens()[0]
101 geom = screen.availableVirtualGeometry()
102 self.__pixmap = screen.grabWindow(
103 0, geom.x(), geom.y(), geom.width(), geom.height())
104 self.resize(self.__pixmap.size())
105 self.move(geom.x(), geom.y())
106 self.setCursor(Qt.CursorShape.CrossCursor)
107 self.show()
108
109 self.grabMouse()
110 self.grabKeyboard()
111 self.activateWindow()
112
113 def paintEvent(self, evt):
114 """
115 Protected method handling paint events.
116
117 @param evt paint event (QPaintEvent)
118 """
119 if self.__grabbing: # grabWindow() should just get the background
120 return
121
122 painter = QPainter(self)
123 pal = QPalette(QToolTip.palette())
124 font = QToolTip.font()
125
126 handleColor = pal.color(QPalette.ColorGroup.Active,
127 QPalette.ColorRole.Highlight)
128 handleColor.setAlpha(160)
129 overlayColor = QColor(0, 0, 0, 160)
130 textColor = pal.color(QPalette.ColorGroup.Active,
131 QPalette.ColorRole.Text)
132 textBackgroundColor = pal.color(QPalette.ColorGroup.Active,
133 QPalette.ColorRole.Base)
134 painter.drawPixmap(0, 0, self.__pixmap)
135 painter.setFont(font)
136
137 pol = QPolygon(self.__selection)
138 if not self.__selection.boundingRect().isNull():
139 # Draw outline around selection.
140 # Important: the 1px-wide outline is *also* part of the
141 # captured free-region
142 pen = QPen(handleColor, 1, Qt.PenStyle.SolidLine,
143 Qt.PenCapStyle.SquareCap, Qt.PenJoinStyle.BevelJoin)
144 painter.setPen(pen)
145 painter.drawPolygon(pol)
146
147 # Draw the grey area around the selection.
148 grey = QRegion(self.rect())
149 grey -= QRegion(pol)
150 painter.setClipRegion(grey)
151 painter.setPen(Qt.PenStyle.NoPen)
152 painter.setBrush(overlayColor)
153 painter.drawRect(self.rect())
154 painter.setClipRect(self.rect())
155 drawPolygon(painter, pol, handleColor)
156
157 if self.__showHelp:
158 painter.setPen(textColor)
159 painter.setBrush(textBackgroundColor)
160 self.__helpTextRect = painter.boundingRect(
161 self.rect().adjusted(2, 2, -2, -2),
162 Qt.TextFlag.TextWordWrap, self.__helpText).translated(0, 0)
163 self.__helpTextRect.adjust(-2, -2, 4, 2)
164 drawPolygon(painter, self.__helpTextRect, textColor,
165 textBackgroundColor)
166 painter.drawText(
167 self.__helpTextRect.adjusted(3, 3, -3, -3),
168 Qt.TextFlag.TextWordWrap, self.__helpText)
169
170 if self.__selection.isEmpty():
171 return
172
173 # The grabbed region is everything which is covered by the drawn
174 # rectangles (border included). This means that there is no 0px
175 # selection, since a 0px wide rectangle will always be drawn as a line.
176 boundingRect = self.__selection.boundingRect()
177 txt = "{0}, {1} ({2} x {3})".format(
178 self.__locale.toString(boundingRect.x()),
179 self.__locale.toString(boundingRect.y()),
180 self.__locale.toString(boundingRect.width()),
181 self.__locale.toString(boundingRect.height())
182 )
183 textRect = painter.boundingRect(self.rect(),
184 Qt.AlignmentFlag.AlignLeft, txt)
185 boundingRect = textRect.adjusted(-4, 0, 0, 0)
186
187 polBoundingRect = pol.boundingRect()
188 if (
189 (textRect.width() <
190 polBoundingRect.width() - 2 * self.__handleSize) and
191 (textRect.height() <
192 polBoundingRect.height() - 2 * self.__handleSize) and
193 polBoundingRect.width() > 100 and
194 polBoundingRect.height() > 100
195 ):
196 # center, unsuitable for small selections
197 boundingRect.moveCenter(polBoundingRect.center())
198 textRect.moveCenter(polBoundingRect.center())
199 elif (
200 polBoundingRect.y() - 3 > textRect.height() and
201 polBoundingRect.x() + textRect.width() < self.rect().width()
202 ):
203 # on top, left aligned
204 boundingRect.moveBottomLeft(
205 QPoint(polBoundingRect.x(), polBoundingRect.y() - 3))
206 textRect.moveBottomLeft(
207 QPoint(polBoundingRect.x() + 2, polBoundingRect.y() - 3))
208 elif polBoundingRect.x() - 3 > textRect.width():
209 # left, top aligned
210 boundingRect.moveTopRight(
211 QPoint(polBoundingRect.x() - 3, polBoundingRect.y()))
212 textRect.moveTopRight(
213 QPoint(polBoundingRect.x() - 5, polBoundingRect.y()))
214 elif (
215 (polBoundingRect.bottom() + 3 + textRect.height() <
216 self.rect().bottom()) and
217 polBoundingRect.right() > textRect.width()
218 ):
219 # at bottom, right aligned
220 boundingRect.moveTopRight(
221 QPoint(polBoundingRect.right(), polBoundingRect.bottom() + 3))
222 textRect.moveTopRight(
223 QPoint(polBoundingRect.right() - 2,
224 polBoundingRect.bottom() + 3))
225 elif (
226 polBoundingRect.right() + textRect.width() + 3 <
227 self.rect().width()
228 ):
229 # right, bottom aligned
230 boundingRect.moveBottomLeft(
231 QPoint(polBoundingRect.right() + 3, polBoundingRect.bottom()))
232 textRect.moveBottomLeft(
233 QPoint(polBoundingRect.right() + 5, polBoundingRect.bottom()))
234
235 # If the above didn't catch it, you are running on a very
236 # tiny screen...
237 drawPolygon(painter, boundingRect, textColor, textBackgroundColor)
238 painter.drawText(textRect, Qt.AlignmentFlag.AlignHCenter, txt)
239
240 if (
241 (polBoundingRect.height() > self.__handleSize * 2 and
242 polBoundingRect.width() > self.__handleSize * 2) or
243 not self.__mouseDown
244 ):
245 painter.setBrush(Qt.GlobalColor.transparent)
246 painter.setClipRegion(QRegion(pol))
247 painter.drawPolygon(QPolygon(self.rect()))
248
249 def mousePressEvent(self, evt):
250 """
251 Protected method to handle mouse button presses.
252
253 @param evt mouse press event (QMouseEvent)
254 """
255 self.__pBefore = evt.position().toPoint()
256
257 self.__showHelp = not self.__helpTextRect.contains(
258 evt.position().toPoint())
259 if evt.button() == Qt.MouseButton.LeftButton:
260 self.__mouseDown = True
261 self.__dragStartPoint = evt.position().toPoint()
262 self.__selectionBeforeDrag = QPolygon(self.__selection)
263 if not self.__selection.containsPoint(evt.position().toPoint(),
264 Qt.FillRule.WindingFill):
265 self.__newSelection = True
266 self.__selection = QPolygon()
267 else:
268 self.setCursor(Qt.CursorShape.ClosedHandCursor)
269 elif evt.button() == Qt.MouseButton.RightButton:
270 self.__newSelection = False
271 self.__selection = QPolygon()
272 self.setCursor(Qt.CursorShape.CrossCursor)
273 self.update()
274
275 def mouseMoveEvent(self, evt):
276 """
277 Protected method to handle mouse movements.
278
279 @param evt mouse move event (QMouseEvent)
280 """
281 shouldShowHelp = not self.__helpTextRect.contains(
282 evt.position().toPoint())
283 if shouldShowHelp != self.__showHelp:
284 self.__showHelp = shouldShowHelp
285 self.update()
286
287 if self.__mouseDown:
288 if self.__newSelection:
289 p = evt.position().toPoint()
290 self.__selection.append(p)
291 else:
292 # moving the whole selection
293 p = evt.position().toPoint() - self.__pBefore # Offset
294 self.__pBefore = evt.position().toPoint()
295 # save position for next iteration
296 self.__selection.translate(p)
297
298 self.update()
299 else:
300 if self.__selection.boundingRect().isEmpty():
301 return
302
303 if self.__selection.containsPoint(evt.position().toPoint(),
304 Qt.FillRule.WindingFill):
305 self.setCursor(Qt.CursorShape.OpenHandCursor)
306 else:
307 self.setCursor(Qt.CursorShape.CrossCursor)
308
309 def mouseReleaseEvent(self, evt):
310 """
311 Protected method to handle mouse button releases.
312
313 @param evt mouse release event (QMouseEvent)
314 """
315 self.__mouseDown = False
316 self.__newSelection = False
317 if self.__selection.containsPoint(evt.position().toPoint(),
318 Qt.FillRule.WindingFill):
319 self.setCursor(Qt.CursorShape.OpenHandCursor)
320 self.update()
321
322 def mouseDoubleClickEvent(self, evt):
323 """
324 Protected method to handle mouse double clicks.
325
326 @param evt mouse double click event (QMouseEvent)
327 """
328 self.__grabRegion()
329
330 def keyPressEvent(self, evt):
331 """
332 Protected method to handle key presses.
333
334 @param evt key press event (QKeyEvent)
335 """
336 if evt.key() == Qt.Key.Key_Escape:
337 self.grabbed.emit(QPixmap())
338 elif evt.key() in [Qt.Key.Key_Enter, Qt.Key.Key_Return]:
339 self.__grabRegion()
340 else:
341 evt.ignore()
342
343 def __grabRegion(self):
344 """
345 Private method to grab the selected region (i.e. do the snapshot).
346 """
347 pol = QPolygon(self.__selection)
348 if not pol.isEmpty():
349 self.__grabbing = True
350
351 xOffset = self.__pixmap.rect().x() - pol.boundingRect().x()
352 yOffset = self.__pixmap.rect().y() - pol.boundingRect().y()
353 translatedPol = pol.translated(xOffset, yOffset)
354
355 pixmap2 = QPixmap(pol.boundingRect().size())
356 pixmap2.fill(Qt.GlobalColor.transparent)
357
358 pt = QPainter()
359 pt.begin(pixmap2)
360 if pt.paintEngine().hasFeature(
361 QPaintEngine.PaintEngineFeature.PorterDuff
362 ):
363 pt.setRenderHints(
364 QPainter.RenderHint.Antialiasing |
365 QPainter.RenderHint.SmoothPixmapTransform,
366 True)
367 pt.setBrush(Qt.GlobalColor.black)
368 pt.setPen(QPen(QBrush(Qt.GlobalColor.black), 0.5))
369 pt.drawPolygon(translatedPol)
370 pt.setCompositionMode(
371 QPainter.CompositionMode.CompositionMode_SourceIn)
372 else:
373 pt.setClipRegion(QRegion(translatedPol))
374 pt.setCompositionMode(
375 QPainter.CompositionMode.CompositionMode_Source)
376
377 pt.drawPixmap(pixmap2.rect(), self.__pixmap, pol.boundingRect())
378 pt.end()
379
380 self.grabbed.emit(pixmap2)

eric ide

mercurial