src/eric7/Snapshot/SnapWidget.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9162
8b75b1668583
child 9221
bf71ee032bb4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Snapshot/SnapWidget.py	Thu Jul 07 11:23:56 2022 +0200
@@ -0,0 +1,430 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the snapshot widget.
+"""
+
+#
+# SnapWidget and its associated modules are PyQt6 ports of Ksnapshot.
+#
+
+import os
+import pathlib
+import re
+import contextlib
+
+from PyQt6.QtCore import (
+    pyqtSlot, Qt, QTimer, QPoint, QMimeData, QLocale, QStandardPaths
+)
+from PyQt6.QtGui import QImageWriter, QPixmap, QDrag, QKeySequence, QShortcut
+from PyQt6.QtWidgets import QWidget, QApplication
+
+from EricWidgets import EricFileDialog, EricMessageBox
+
+from .Ui_SnapWidget import Ui_SnapWidget
+
+import UI.PixmapCache
+import Preferences
+import Globals
+
+from .SnapshotModes import SnapshotModes
+
+
+class SnapWidget(QWidget, Ui_SnapWidget):
+    """
+    Class implementing the snapshot widget.
+    """
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent widget (QWidget)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        
+        self.saveButton.setIcon(UI.PixmapCache.getIcon("fileSaveAs"))
+        self.takeButton.setIcon(UI.PixmapCache.getIcon("cameraPhoto"))
+        self.copyButton.setIcon(UI.PixmapCache.getIcon("editCopy"))
+        self.copyPreviewButton.setIcon(UI.PixmapCache.getIcon("editCopy"))
+        self.setWindowIcon(UI.PixmapCache.getIcon("ericSnap"))
+        
+        if Globals.isWaylandSession():
+            from .SnapshotWaylandGrabber import SnapshotWaylandGrabber
+            self.__grabber = SnapshotWaylandGrabber(self)
+        else:
+            from .SnapshotDefaultGrabber import SnapshotDefaultGrabber
+            self.__grabber = SnapshotDefaultGrabber(self)
+            self.decorationsCheckBox.hide()
+            self.mouseCursorCheckBox.hide()
+        self.__grabber.grabbed.connect(self.__captured)
+        supportedModes = self.__grabber.supportedModes()
+        
+        if SnapshotModes.FULLSCREEN in supportedModes:
+            self.modeCombo.addItem(self.tr("Fullscreen"),
+                                   SnapshotModes.FULLSCREEN)
+        if (
+            SnapshotModes.SELECTEDSCREEN in supportedModes and
+            len(QApplication.screens()) > 1
+        ):
+            self.modeCombo.addItem(self.tr("Select Screen"),
+                                   SnapshotModes.SELECTEDSCREEN)
+        if SnapshotModes.SELECTEDWINDOW in supportedModes:
+            self.modeCombo.addItem(self.tr("Select Window"),
+                                   SnapshotModes.SELECTEDWINDOW)
+        if SnapshotModes.RECTANGLE in supportedModes:
+            self.modeCombo.addItem(self.tr("Rectangular Selection"),
+                                   SnapshotModes.RECTANGLE)
+        if SnapshotModes.ELLIPSE in supportedModes:
+            self.modeCombo.addItem(self.tr("Elliptical Selection"),
+                                   SnapshotModes.ELLIPSE)
+        if SnapshotModes.FREEHAND in supportedModes:
+            self.modeCombo.addItem(self.tr("Freehand Selection"),
+                                   SnapshotModes.FREEHAND)
+        mode = int(Preferences.getSettings().value("Snapshot/Mode", 0))
+        index = self.modeCombo.findData(SnapshotModes(mode))
+        if index == -1:
+            index = 0
+        self.modeCombo.setCurrentIndex(index)
+        
+        delay = int(Preferences.getSettings().value("Snapshot/Delay", 0))
+        self.delaySpin.setValue(delay)
+        
+        picturesLocation = QStandardPaths.writableLocation(
+            QStandardPaths.StandardLocation.PicturesLocation)
+        self.__filename = Preferences.getSettings().value(
+            "Snapshot/Filename",
+            os.path.join(picturesLocation,
+                         self.tr("snapshot") + "1.png"))
+        
+        self.__snapshot = QPixmap()
+        self.__savedPosition = QPoint()
+        self.__modified = False
+        self.__locale = QLocale()
+        
+        self.__initFileFilters()
+        self.__initShortcuts()
+        
+        self.preview.startDrag.connect(self.__dragSnapshot)
+        
+        self.__updateTimer = QTimer()
+        self.__updateTimer.setSingleShot(True)
+        self.__updateTimer.timeout.connect(self.__updatePreview)
+        
+        self.__updateCaption()
+        self.takeButton.setFocus()
+    
+    def __initFileFilters(self):
+        """
+        Private method to define the supported image file filters.
+        """
+        filters = {
+            'bmp': self.tr("Windows Bitmap File (*.bmp)"),
+            'gif': self.tr("Graphic Interchange Format File (*.gif)"),
+            'ico': self.tr("Windows Icon File (*.ico)"),
+            'jpg': self.tr("JPEG File (*.jpg)"),
+            'mng': self.tr("Multiple-Image Network Graphics File (*.mng)"),
+            'pbm': self.tr("Portable Bitmap File (*.pbm)"),
+            'pcx': self.tr("Paintbrush Bitmap File (*.pcx)"),
+            'pgm': self.tr("Portable Graymap File (*.pgm)"),
+            'png': self.tr("Portable Network Graphics File (*.png)"),
+            'ppm': self.tr("Portable Pixmap File (*.ppm)"),
+            'sgi': self.tr("Silicon Graphics Image File (*.sgi)"),
+            'svg': self.tr("Scalable Vector Graphics File (*.svg)"),
+            'tga': self.tr("Targa Graphic File (*.tga)"),
+            'tif': self.tr("TIFF File (*.tif)"),
+            'xbm': self.tr("X11 Bitmap File (*.xbm)"),
+            'xpm': self.tr("X11 Pixmap File (*.xpm)"),
+        }
+        
+        outputFormats = []
+        writeFormats = QImageWriter.supportedImageFormats()
+        for writeFormat in writeFormats:
+            with contextlib.suppress(KeyError):
+                outputFormats.append(filters[bytes(writeFormat).decode()])
+        outputFormats.sort()
+        self.__outputFilter = ';;'.join(outputFormats)
+        
+        self.__defaultFilter = filters['png']
+    
+    def __initShortcuts(self):
+        """
+        Private method to initialize the keyboard shortcuts.
+        """
+        self.__quitShortcut = QShortcut(
+            QKeySequence(QKeySequence.StandardKey.Quit), self, self.close)
+        
+        self.__copyShortcut = QShortcut(
+            QKeySequence(QKeySequence.StandardKey.Copy), self,
+            self.copyButton.animateClick)
+        
+        self.__quickSaveShortcut = QShortcut(
+            QKeySequence(Qt.Key.Key_Q), self, self.__quickSave)
+        
+        self.__save1Shortcut = QShortcut(
+            QKeySequence(QKeySequence.StandardKey.Save), self,
+            self.saveButton.animateClick)
+        self.__save2Shortcut = QShortcut(
+            QKeySequence(Qt.Key.Key_S), self, self.saveButton.animateClick)
+        
+        self.__grab1Shortcut = QShortcut(
+            QKeySequence(QKeySequence.StandardKey.New),
+            self, self.takeButton.animateClick)
+        self.__grab2Shortcut = QShortcut(
+            QKeySequence(Qt.Key.Key_N), self, self.takeButton.animateClick)
+        self.__grab3Shortcut = QShortcut(
+            QKeySequence(Qt.Key.Key_Space), self, self.takeButton.animateClick)
+    
+    def __quickSave(self):
+        """
+        Private slot to save the snapshot bypassing the file selection dialog.
+        """
+        if not self.__snapshot.isNull():
+            while os.path.exists(self.__filename):
+                self.__autoIncFilename()
+            
+            if self.__saveImage(self.__filename):
+                self.__modified = False
+                self.__autoIncFilename()
+                self.__updateCaption()
+    
+    @pyqtSlot()
+    def on_saveButton_clicked(self):
+        """
+        Private slot to save the snapshot.
+        """
+        if not self.__snapshot.isNull():
+            while os.path.exists(self.__filename):
+                self.__autoIncFilename()
+            
+            fileName, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
+                self,
+                self.tr("Save Snapshot"),
+                self.__filename,
+                self.__outputFilter,
+                self.__defaultFilter,
+                EricFileDialog.DontConfirmOverwrite)
+            if not fileName:
+                return
+            
+            fpath = pathlib.Path(fileName)
+            if not fpath.suffix:
+                ex = selectedFilter.split("(*")[1].split(")")[0]
+                if ex:
+                    fpath = fpath.with_suffix(ex)
+            
+            if self.__saveImage(str(fpath)):
+                self.__modified = False
+                self.__filename = str(fpath)
+                self.__autoIncFilename()
+                self.__updateCaption()
+    
+    def __saveImage(self, fileName):
+        """
+        Private method to save the snapshot.
+        
+        @param fileName name of the file to save to (string)
+        @return flag indicating success (boolean)
+        """
+        if pathlib.Path(fileName).exists():
+            res = EricMessageBox.yesNo(
+                self,
+                self.tr("Save Snapshot"),
+                self.tr("<p>The file <b>{0}</b> already exists."
+                        " Overwrite it?</p>").format(fileName),
+                icon=EricMessageBox.Warning)
+            if not res:
+                return False
+        
+        ok = self.__snapshot.save(fileName)
+        if not ok:
+            EricMessageBox.warning(
+                self, self.tr("Save Snapshot"),
+                self.tr("Cannot write file '{0}'.").format(fileName))
+        
+        return ok
+    
+    def __autoIncFilename(self):
+        """
+        Private method to auto-increment the file name.
+        """
+        # Extract the file name
+        name = os.path.basename(self.__filename)
+        
+        # If the name contains a number, then increment it.
+        numSearch = re.compile("(^|[^\\d])(\\d+)")
+        # We want to match as far left as possible, and when the number is
+        # at the start of the name.
+        
+        # Does it have a number?
+        matches = list(numSearch.finditer(name))
+        if matches:
+            # It has a number, increment it.
+            match = matches[-1]
+            start = match.start(2)
+            # Only the second group is of interest.
+            numAsStr = match.group(2)
+            number = "{0:0{width}d}".format(
+                int(numAsStr) + 1, width=len(numAsStr))
+            name = name[:start] + number + name[start + len(numAsStr):]
+        else:
+            # no number
+            start = name.rfind('.')
+            if start != -1:
+                # has a '.' somewhere, e.g. it has an extension
+                name = name[:start] + '-1' + name[start:]
+            else:
+                # no extension, just tack it on to the end
+                name += '-1'
+        
+        self.__filename = os.path.join(os.path.dirname(self.__filename), name)
+        self.__updateCaption()
+    
+    @pyqtSlot()
+    def on_takeButton_clicked(self):
+        """
+        Private slot to take a snapshot.
+        """
+        self.__savedPosition = self.pos()
+        self.hide()
+        
+        self.__grabber.grab(
+            self.modeCombo.itemData(self.modeCombo.currentIndex()),
+            self.delaySpin.value(),
+            self.mouseCursorCheckBox.isChecked(),
+            self.decorationsCheckBox.isChecked(),
+        )
+    
+    def __redisplay(self):
+        """
+        Private method to redisplay the window.
+        """
+        self.__updatePreview()
+        if not self.__savedPosition.isNull():
+            self.move(self.__savedPosition)
+        self.show()
+        self.raise_()
+        
+        self.saveButton.setEnabled(not self.__snapshot.isNull())
+        self.copyButton.setEnabled(not self.__snapshot.isNull())
+        self.copyPreviewButton.setEnabled(not self.__snapshot.isNull())
+    
+    @pyqtSlot()
+    def on_copyButton_clicked(self):
+        """
+        Private slot to copy the snapshot to the clipboard.
+        """
+        if not self.__snapshot.isNull():
+            QApplication.clipboard().setPixmap(QPixmap(self.__snapshot))
+    
+    @pyqtSlot()
+    def on_copyPreviewButton_clicked(self):
+        """
+        Private slot to copy the snapshot preview to the clipboard.
+        """
+        QApplication.clipboard().setPixmap(self.preview.pixmap())
+    
+    def __captured(self, pixmap):
+        """
+        Private slot to show a preview of the snapshot.
+        
+        @param pixmap pixmap of the snapshot (QPixmap)
+        """
+        self.__snapshot = QPixmap(pixmap)
+        
+        self.__redisplay()
+        self.__modified = not pixmap.isNull()
+        self.__updateCaption()
+    
+    def __updatePreview(self):
+        """
+        Private slot to update the preview picture.
+        """
+        self.preview.setToolTip(self.tr(
+            "Preview of the snapshot image ({0} x {1})").format(
+            self.__locale.toString(self.__snapshot.width()),
+            self.__locale.toString(self.__snapshot.height()))
+        )
+        self.preview.setPreview(self.__snapshot)
+        self.preview.adjustSize()
+    
+    def resizeEvent(self, evt):
+        """
+        Protected method handling a resizing of the window.
+        
+        @param evt resize event (QResizeEvent)
+        """
+        self.__updateTimer.start(200)
+    
+    def __dragSnapshot(self):
+        """
+        Private slot handling the dragging of the preview picture.
+        """
+        drag = QDrag(self)
+        mimeData = QMimeData()
+        mimeData.setImageData(self.__snapshot)
+        drag.setMimeData(mimeData)
+        drag.setPixmap(self.preview.pixmap())
+        drag.exec(Qt.DropAction.CopyAction)
+    
+    def closeEvent(self, evt):
+        """
+        Protected method handling the close event.
+        
+        @param evt close event (QCloseEvent)
+        """
+        if self.__modified:
+            res = EricMessageBox.question(
+                self,
+                self.tr("eric Snapshot"),
+                self.tr(
+                    """The application contains an unsaved snapshot."""),
+                EricMessageBox.Abort |
+                EricMessageBox.Discard |
+                EricMessageBox.Save
+            )
+            if res == EricMessageBox.Abort:
+                evt.ignore()
+                return
+            elif res == EricMessageBox.Save:
+                self.on_saveButton_clicked()
+        
+        Preferences.getSettings().setValue(
+            "Snapshot/Delay", self.delaySpin.value())
+        modeData = self.modeCombo.itemData(self.modeCombo.currentIndex())
+        if modeData is not None:
+            Preferences.getSettings().setValue(
+                "Snapshot/Mode",
+                modeData.value)
+        Preferences.getSettings().setValue(
+            "Snapshot/Filename", self.__filename)
+        Preferences.getSettings().sync()
+    
+    def __updateCaption(self):
+        """
+        Private method to update the window caption.
+        """
+        self.setWindowTitle("{0}[*] - {1}".format(
+            os.path.basename(self.__filename),
+            self.tr("eric Snapshot")))
+        self.setWindowModified(self.__modified)
+        self.pathNameEdit.setText(os.path.dirname(self.__filename))
+    
+    @pyqtSlot(int)
+    def on_modeCombo_currentIndexChanged(self, index):
+        """
+        Private slot handling the selection of a screenshot mode.
+        
+        @param index index of the selection
+        @type int
+        """
+        isWindowMode = False
+        if index >= 0:
+            mode = self.modeCombo.itemData(index)
+            isWindowMode = (mode == SnapshotModes.SELECTEDWINDOW)
+        
+        self.decorationsCheckBox.setEnabled(isWindowMode)
+        self.decorationsCheckBox.setChecked(isWindowMode)

eric ide

mercurial