Snapshot/SnapshotRegionGrabber.py

changeset 1770
c17e67e69ef5
child 1772
f325dfdc8f6b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Snapshot/SnapshotRegionGrabber.py	Sat Apr 07 21:19:24 2012 +0200
@@ -0,0 +1,457 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2012 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the snapshot region grabber widget.
+"""
+
+from PyQt4.QtCore import pyqtSignal, Qt, QRect, QPoint, QTimer
+from PyQt4.QtGui import QWidget, QPixmap, QColor, QRegion, QApplication, QPainter, \
+    QPalette, QToolTip
+
+
+def drawRect(painter, rect, outline, fill=QColor()):
+    """
+    Module function to draw a rectangle with the given parameters.
+    
+    @param painter reference to the painter to be used (QPainter)
+    @param rect rectangle to be drawn (QRect)
+    @param outline color of the outline (QColor)
+    @param fill fill color (QColor)
+    """
+    clip = QRegion(rect)
+    clip = clip.subtracted(QRegion(rect.adjusted(1, 1, -1, -1)))
+    
+    painter.save()
+    painter.setClipRegion(clip)
+    painter.setPen(Qt.NoPen)
+    painter.setBrush(outline)
+    painter.drawRect(rect)
+    if fill.isValid():
+        painter.setClipping(False)
+        painter.setBrush(fill)
+        painter.drawRect(rect.adjusted(1, 1, -1, -1))
+    painter.restore()
+
+
+class SnapshotRegionGrabber(QWidget):
+    """
+    Class implementing the snapshot region grabber widget.
+    
+    @signal grabbed(QPixmap) emitted after the region was grabbed
+    """
+    grabbed = pyqtSignal(QPixmap)
+    
+    StrokeMask = 0
+    FillMask = 1
+    
+    ModeFullscreen = 0
+    ModeRectangle = 1
+    
+    def __init__(self, mode, delay):
+        """
+        Constructor
+        
+        @param mode snapshot mode (SnapshotRegionGrabber.ModeFullscreen,
+            SnapshotRegionGrabber.ModeRectangle)
+        @param delay delay in seconds before taking the snapshot (int)
+        """
+        super().__init__(None,
+            Qt.X11BypassWindowManagerHint | Qt.WindowStaysOnTopHint | 
+            Qt.FramelessWindowHint | Qt.Tool)
+        
+        self.__mode = mode
+        
+        self.__selection = QRect()
+        self.__mouseDown = False
+        self.__newSelection = False
+        self.__handleSize = 10
+        self.__mouseOverHandle = None
+        self.__showHelp = True
+        self.__grabbing = False
+        self.__dragStartPoint = QPoint()
+        self.__selectionBeforeDrag = QRect()
+        
+        # naming conventions for handles
+        # T top, B bottom, R Right, L left
+        # 2 letters: a corner
+        # 1 letter: the handle on the middle of the corresponding side
+        self.__TLHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
+        self.__TRHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
+        self.__BLHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
+        self.__BRHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
+        self.__LHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
+        self.__THandle = QRect(0, 0, self.__handleSize, self.__handleSize)
+        self.__RHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
+        self.__BHandle = QRect(0, 0, self.__handleSize, self.__handleSize)
+        self.__handles = [self.__TLHandle, self.__TRHandle, self.__BLHandle,
+                          self.__BRHandle, self.__LHandle, self.__THandle, 
+                          self.__RHandle, self.__BHandle]
+        self.__helpTextRect = QRect()
+        self.__helpText = self.trUtf8(
+            "Select a region using the mouse. To take the snapshot, press the Enter key"
+            " or double click. Press Esc to quit.")
+        
+        self.__pixmap = QPixmap()
+        
+        self.setMouseTracking(True)
+        
+        if delay == 0:
+            delay = 200
+        else:
+            delay = delay * 1000
+        QTimer.singleShot(delay, self.__initialize)
+    
+    def __initialize(self):
+        """
+        Private slot to initialize the rest of the widget.
+        """
+        self.__pixmap = QPixmap.grabWindow(QApplication.desktop().winId())
+        if self.__mode == SnapshotRegionGrabber.ModeFullscreen:
+            self.grabbed.emit(self.__pixmap.copy())
+        else:
+            self.resize(self.__pixmap.size())
+            self.move(0, 0)
+            self.setCursor(Qt.CrossCursor)
+            self.show()
+
+            self.grabMouse()
+            self.grabKeyboard()
+    
+    def paintEvent(self, evt):
+        """
+        Protected method handling paint events.
+        
+        @param evt paint event (QPaintEvent)
+        """
+        if self.__grabbing:     # grabWindow() should just get the background
+            return
+        
+        painter = QPainter(self)
+        pal = QPalette(QToolTip.palette())
+        font = QToolTip.font()
+        
+        handleColor = pal.color(QPalette.Active, QPalette.Highlight)
+        handleColor.setAlpha(160)
+        overlayColor = QColor(0, 0, 0, 160)
+        textColor = pal.color(QPalette.Active, QPalette.Text)
+        textBackgroundColor = pal.color(QPalette.Active, QPalette.Base)
+        painter.drawPixmap(0, 0, self.__pixmap)
+        painter.setFont(font)
+        
+        r = QRect(self.__selection)
+        if not self.__selection.isNull():
+            grey = QRegion(self.rect())
+            grey = grey.subtracted(QRegion(r))
+            painter.setClipRegion(grey)
+            painter.setPen(Qt.NoPen)
+            painter.setBrush(overlayColor)
+            painter.drawRect(self.rect())
+            painter.setClipRegion(QRegion(self.rect()))
+            drawRect(painter, r, handleColor)
+        
+        if self.__showHelp:
+            painter.setPen(textColor)
+            painter.setBrush(textBackgroundColor)
+            self.__helpTextRect = painter.boundingRect(self.rect().adjusted(2, 2, -2, -2),
+                Qt.TextWordWrap, self.__helpText)
+            self.__helpTextRect.adjust(-2, -2, 4, 2)
+            drawRect(painter, self.__helpTextRect, textColor, textBackgroundColor)
+            painter.drawText(self.__helpTextRect.adjusted(3, 3, -3, -3),
+                Qt.TextWordWrap, self.__helpText)
+        
+        if self.__selection.isNull():
+            return
+        
+        # The grabbed region is everything which is covered by the drawn
+        # rectangles (border included). This means that there is no 0px
+        # selection, since a 0px wide rectangle will always be drawn as a line.
+        txt = "{0}, {1} ({2}x{3})".format(
+            self.__selection.x(), self.__selection.y(),
+            self.__selection.width(), self.__selection.height())
+        textRect = painter.boundingRect(self.rect(), Qt.AlignLeft, txt)
+        boundingRect = textRect.adjusted(-4, 0, 0, 0)
+        
+        if textRect.width() < r.width() - 2 * self.__handleSize and \
+           textRect.height() < r.height() - 2 * self.__handleSize and \
+           r.width() > 100 and \
+           r.height() > 100:
+            # center, unsuitable for small selections
+            boundingRect.moveCenter(r.center())
+            textRect.moveCenter(r.center())
+        elif r.y() - 3 > textRect.height() and \
+             r.x() + textRect.width() < self.rect().width():
+            # on top, left aligned
+            boundingRect.moveBottomLeft(QPoint(r.x(), r.y() - 3))
+            textRect.moveBottomLeft(QPoint(r.x() + 2, r.y() - 3))
+        elif r.x() - 3 > textRect.width():
+            # left, top aligned
+            boundingRect.moveTopRight(QPoint(r.x() - 3, r.y()))
+            textRect.moveTopRight(QPoint(r.x() - 5, r.y()))
+        elif r.bottom() + 3 + textRect.height() < self.rect().bottom() and \
+             r.right() > textRect.width():
+            # at bottom, right aligned
+            boundingRect.moveTopRight(QPoint(r.right(), r.bottom() + 3))
+            textRect.moveTopRight(QPoint(r.right() - 2, r.bottom() + 3))
+        elif r.right() + textRect.width() + 3 < self.rect().width():
+            # right, bottom aligned
+            boundingRect.moveBottomLeft(QPoint(r.right() + 3, r.bottom()))
+            textRect.moveBottomLeft(QPoint(r.right() + 5, r.bottom()))
+        
+        # If the above didn't catch it, you are running on a very tiny screen...
+        drawRect(painter, boundingRect, textColor, textBackgroundColor)
+        painter.drawText(textRect, Qt.AlignHCenter, txt)
+        
+        if (r.height() > self.__handleSize * 2 and \
+            r.width() > self.__handleSize * 2) or \
+           not self.__mouseDown:
+            self.__updateHandles()
+            painter.setPen(Qt.NoPen)
+            painter.setBrush(handleColor)
+            painter.setClipRegion(self.__handleMask(SnapshotRegionGrabber.StrokeMask))
+            painter.drawRect(self.rect())
+            handleColor.setAlpha(60)
+            painter.setBrush(handleColor)
+            painter.setClipRegion(self.__handleMask(SnapshotRegionGrabber.FillMask))
+            painter.drawRect(self.rect())
+    
+    def resizeEvent(self, evt):
+        """
+        Protected method to handle resize events.
+        
+        @param evt resize event (QResizeEvent)
+        """
+        if self.__selection.isNull():
+            return
+        
+        r = QRect(self.__selection)
+        r.setTopLeft(self.__limitPointToRect(r.topLeft(), self.rect()))
+        r.setBottomRight(self.__limitPointToRect(r.bottomRight(), self.rect()))
+        if r.width() <= 1 or r.height() <= 1:
+            # This just results in ugly drawing...
+            self.__selection = QRect()
+        else:
+            self.__selection = self.__normalizeSelection(r)
+    
+    def mousePressEvent(self, evt):
+        """
+        Protected method to handle mouse button presses.
+        
+        @param evt mouse press event (QMouseEvent)
+        """
+        self.__showHelp = not self.__helpTextRect.contains(evt.pos())
+        if evt.button() == Qt.LeftButton:
+            self.__mouseDown = True
+            self.__dragStartPoint = evt.pos()
+            self.__selectionBeforeDrag = QRect(self.__selection)
+            if not self.__selection.contains(evt.pos()):
+                self.__newSelection = True
+                self.__selection = QRect()
+            else:
+                self.setCursor(Qt.ClosedHandCursor)
+        elif evt.button() == Qt.RightButton:
+            self.__newSelection = False
+            self.__selection = QRect()
+            self.setCursor(Qt.CrossCursor)
+        self.update()
+    
+    def mouseMoveEvent(self, evt):
+        """
+        Protected method to handle mouse movements.
+        
+        @param evt mouse move event (QMouseEvent)
+        """
+        shouldShowHelp = not self.__helpTextRect.contains(evt.pos())
+        if shouldShowHelp != self.__showHelp:
+            self.__showHelp = shouldShowHelp
+            self.update()
+        
+        if self.__mouseDown:
+            if self.__newSelection:
+                p = evt.pos()
+                r = self.rect()
+                self.__selection = self.__normalizeSelection(
+                    QRect(self.__dragStartPoint, self.__limitPointToRect(p, r)))
+            elif self.__mouseOverHandle is None:
+                # moving the whole selection
+                r = self.rect().normalized()
+                s = self.__selectionBeforeDrag.normalized()
+                p = s.topLeft() + evt.pos() - self.__dragStartPoint
+                r.setBottomRight(
+                    r.bottomRight() - QPoint(s.width(), s.height()) + QPoint(1, 1))
+                if not r.isNull() and r.isValid():
+                    self.__selection.moveTo(self.__limitPointToRect(p, r))
+            else:
+                # dragging a handle
+                r = QRect(self.__selectionBeforeDrag)
+                offset = evt.pos() - self.__dragStartPoint
+                
+                if self.__mouseOverHandle in \
+                   [self.__TLHandle, self.__THandle, self.__TRHandle]:
+                    r.setTop(r.top() + offset.y())
+                
+                if self.__mouseOverHandle in \
+                   [self.__TLHandle, self.__LHandle, self.__BLHandle]:
+                    r.setLeft(r.left() + offset.x())
+                
+                if self.__mouseOverHandle in \
+                   [self.__BLHandle, self.__BHandle, self.__BRHandle]:
+                    r.setBottom(r.bottom() + offset.y())
+                
+                if self.__mouseOverHandle in \
+                   [self.__TRHandle, self.__RHandle, self.__BRHandle]:
+                    r.setRight(r.right() + offset.x())
+                
+                r.setTopLeft(self.__limitPointToRect(r.topLeft(), self.rect()))
+                r.setBottomRight(self.__limitPointToRect(r.bottomRight(), self.rect()))
+                self.__selection = self.__normalizeSelection(r)
+            
+            self.update()
+        else:
+            if self.__selection.isNull():
+                return
+            
+            found = False
+            for r in self.__handles:
+                if r.contains(evt.pos()):
+                    self.__mouseOverHandle = r
+                    found = True
+                    break
+            
+            if not found:
+                self.__mouseOverHandle = None
+                if self.__selection.contains(evt.pos()):
+                    self.setCursor(Qt.OpenHandCursor)
+                else:
+                    self.setCursor(Qt.CrossCursor)
+            else:
+                if self.__mouseOverHandle in [self.__TLHandle, self.__BRHandle]:
+                    self.setCursor(Qt.SizeFDiagCursor)
+                elif self.__mouseOverHandle in [self.__TRHandle, self.__BLHandle]:
+                    self.setCursor(Qt.SizeBDiagCursor)
+                elif self.__mouseOverHandle in [self.__LHandle, self.__RHandle]:
+                    self.setCursor(Qt.SizeHorCursor)
+                elif self.__mouseOverHandle in [self.__THandle, self.__BHandle]:
+                    self.setCursor(Qt.SizeVerCursor)
+    
+    def mouseReleaseEvent(self, evt):
+        """
+        Protected method to handle mouse button releases.
+        
+        @param evt mouse release event (QMouseEvent)
+        """
+        self.__mouseDown = False
+        self.__newSelection = False
+        if self.__mouseOverHandle is None and \
+           self.__selection.contains(evt.pos()):
+            self.setCursor(Qt.OpenHandCursor)
+        self.update()
+    
+    def mouseDoubleClickEvent(self, evt):
+        """
+        Protected method to handle mouse double clicks.
+        
+        @param evt mouse double click event (QMouseEvent)
+        """
+        self.__grabRect()
+    
+    def keyPressEvent(self, evt):
+        """
+        Protected method to handle key presses.
+        
+        @param evt key press event (QKeyEvent)
+        """
+        if evt.key() == Qt.Key_Escape:
+            self.grabbed.emit(QPixmap())
+        elif evt.key() in [Qt.Key_Enter, Qt.Key_Return]:
+            self.__grabRect()
+        else:
+            evt.ignore()
+    
+    def __updateHandles(self):
+        """
+        Private method to update the handles.
+        """
+        r = QRect(self.__selection)
+        s2 = self.__handleSize // 2
+        
+        self.__TLHandle.moveTopLeft(r.topLeft())
+        self.__TRHandle.moveTopRight(r.topRight())
+        self.__BLHandle.moveBottomLeft(r.bottomLeft())
+        self.__BRHandle.moveBottomRight(r.bottomRight())
+        
+        self.__LHandle.moveTopLeft(QPoint(r.x(), r.y() + r.height() // 2 - s2))
+        self.__THandle.moveTopLeft(QPoint(r.x() + r.width() // 2 - s2, r.y()))
+        self.__RHandle.moveTopRight(QPoint(r.right(), r.y() + r.height() // 2 - s2))
+        self.__BHandle.moveBottomLeft(QPoint(r.x() + r.width() // 2 - s2, r.bottom()))
+    
+    def __handleMask(self, maskType):
+        """
+        Private method to calculate the handle mask.
+        
+        @param maskType type of the mask to be used (SnapshotRegionGrabber.FillMask or
+            SnapshotRegionGrabber.StrokeMask)
+        @return calculated mask (QRegion)
+        """
+        mask = QRegion()
+        for rect in self.__handles:
+            if maskType == SnapshotRegionGrabber.StrokeMask:
+                r = QRegion(rect)
+                mask += r.subtracted(QRegion(rect.adjusted(1, 1, -1, -1)))
+            else:
+                mask += QRegion(rect.adjusted(1, 1, -1, -1))
+        return mask
+    
+    def __limitPointToRect(self, point, rect):
+        """
+        Private method to limit the given point to the given rectangle.
+        
+        @param point point to be limited (QPoint)
+        @param rect rectangle the point shall be limited to (QRect)
+        @return limited point (QPoint)
+        """
+        q = QPoint()
+        if point.x() < rect.x():
+            q.setX(rect.x())
+        elif point.x() < rect.right():
+            q.setX(point.x())
+        else:
+            q.setX(rect.right())
+        if point.y() < rect.y():
+            q.setY(rect.y())
+        elif point.y() < rect.bottom():
+            q.setY(point.y())
+        else:
+            q.setY(rect.bottom())
+        return q
+    
+    def __normalizeSelection(self, sel):
+        """
+        Private method to normalize the given selection.
+        
+        @param sel selection to be normalized (QRect)
+        @return normalized selection (QRect)
+        """
+        r = QRect(sel)
+        if r.width() <= 0:
+            l = r.left()
+            w = r.width()
+            r.setLeft(l + w -1)
+            r.setRight(l)
+        if r.height() <= 0:
+            t = r.top()
+            h = r.height()
+            r.setTop(t + h - 1)
+            r.setBottom(t)
+        return r
+    
+    def __grabRect(self):
+        """
+        Private method to grab the selected rectangle (i.e. do the snapshot).
+        """
+        r = QRect(self.__selection)
+        if not r.isNull() and r.isValid():
+            self.__grabbing = True
+            self.grabbed.emit(self.__pixmap.copy(r))

eric ide

mercurial