src/eric7/Snapshot/SnapWidget.py

Fri, 04 Nov 2022 13:52:26 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 04 Nov 2022 13:52:26 +0100
branch
eric7
changeset 9473
3f23dbf37dbe
parent 9413
80c06d472826
child 9482
a2bc06a54d9d
permissions
-rw-r--r--

Resorted the import statements using isort.

# -*- 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 contextlib
import os
import pathlib
import re

from PyQt6.QtCore import (
    QLocale,
    QMimeData,
    QPoint,
    QStandardPaths,
    Qt,
    QTimer,
    pyqtSlot,
)
from PyQt6.QtGui import QDrag, QImageWriter, QKeySequence, QPixmap, QShortcut
from PyQt6.QtWidgets import QApplication, QWidget

from eric7 import Globals, Preferences
from eric7.EricGui import EricPixmapCache
from eric7.EricWidgets import EricFileDialog, EricMessageBox

from .SnapshotModes import SnapshotModes
from .Ui_SnapWidget import Ui_SnapWidget


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(EricPixmapCache.getIcon("fileSaveAs"))
        self.takeButton.setIcon(EricPixmapCache.getIcon("cameraPhoto"))
        self.copyButton.setIcon(EricPixmapCache.getIcon("editCopy"))
        self.copyPreviewButton.setIcon(EricPixmapCache.getIcon("editCopy"))
        self.setWindowIcon(EricPixmapCache.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