eric7/Snapshot/SnapshotWaylandGrabber.py

branch
eric7
changeset 8312
800c432b34c8
parent 8265
0090cfa83159
child 8318
962bce857696
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/Snapshot/SnapshotWaylandGrabber.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,370 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a grabber object for non-Wayland desktops.
+"""
+
+import os
+import uuid
+import contextlib
+
+from PyQt5.QtCore import pyqtSignal, QObject, QTimer
+from PyQt5.QtGui import QPixmap, QCursor
+from PyQt5.QtWidgets import QApplication
+
+try:
+    from PyQt5.QtDBus import QDBusInterface, QDBusMessage
+    DBusAvailable = True
+except ImportError:
+    DBusAvailable = False
+
+from E5Gui import E5MessageBox
+
+from .SnapshotModes import SnapshotModes
+
+import Globals
+
+
+class SnapshotWaylandGrabber(QObject):
+    """
+    Class implementing a grabber object for non-Wayland desktops.
+    
+    @signal grabbed(QPixmap) emitted after the grab operation is finished
+    """
+    grabbed = pyqtSignal(QPixmap)
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent object
+        @type QObject
+        """
+        super().__init__(parent)
+        
+        from .SnapshotTimer import SnapshotTimer
+        self.__grabTimer = SnapshotTimer()
+        self.__grabTimer.timeout.connect(self.__performGrab)
+    
+    def supportedModes(self):
+        """
+        Public method to get the supported screenshot modes.
+        
+        @return tuple of supported screenshot modes
+        @rtype tuple of SnapshotModes
+        """
+        if DBusAvailable and Globals.isKdeDesktop():
+            # __IGNORE_WARNING_Y114__
+            return (
+                SnapshotModes.FULLSCREEN,
+                SnapshotModes.SELECTEDSCREEN,
+                SnapshotModes.SELECTEDWINDOW,
+            )
+        elif DBusAvailable and Globals.isGnomeDesktop():
+            return (
+                SnapshotModes.FULLSCREEN,
+                SnapshotModes.SELECTEDSCREEN,
+                SnapshotModes.SELECTEDWINDOW,
+                SnapshotModes.RECTANGLE,
+            )
+        else:
+            return ()
+    
+    def grab(self, mode, delay=0, captureCursor=False,
+             captureDecorations=False):
+        """
+        Public method to perform a grab operation potentially after a delay.
+        
+        @param mode screenshot mode
+        @type ScreenshotModes
+        @param delay delay in seconds
+        @type int
+        @param captureCursor flag indicating to include the mouse cursor
+        @type bool
+        @param captureDecorations flag indicating to include the window
+            decorations (only used for mode SnapshotModes.SELECTEDWINDOW)
+        @type bool
+        """
+        if not DBusAvailable:
+            # just to play it safe
+            self.grabbed.emit(QPixmap())
+            return
+        
+        self.__mode = mode
+        self.__captureCursor = captureCursor
+        self.__captureDecorations = captureDecorations
+        if delay:
+            self.__grabTimer.start(delay)
+        else:
+            QTimer.singleShot(200, self.__performGrab)
+    
+    def __performGrab(self):
+        """
+        Private method to perform the grab operations.
+        
+        @exception RuntimeError raised to indicate an unsupported grab mode
+        """
+        if self.__mode not in (
+            SnapshotModes.FULLSCREEN, SnapshotModes.SELECTEDSCREEN,
+            SnapshotModes.SELECTEDWINDOW, SnapshotModes.RECTANGLE,
+        ):
+            raise RuntimeError("unsupported grab mode given")
+        
+        if self.__mode == SnapshotModes.FULLSCREEN:
+            self.__grabFullscreen()
+        elif self.__mode == SnapshotModes.SELECTEDSCREEN:
+            self.__grabSelectedScreen()
+        elif self.__mode == SnapshotModes.SELECTEDWINDOW:
+            self.__grabSelectedWindow()
+        else:
+            self.__grabRectangle()
+    
+    def __grabFullscreen(self):
+        """
+        Private method to grab the complete desktop.
+        """
+        snapshot = QPixmap()
+        
+        if Globals.isKdeDesktop():
+            interface = QDBusInterface(
+                "org.kde.KWin",
+                "/Screenshot",
+                "org.kde.kwin.Screenshot"
+            )
+            reply = interface.call(
+                "screenshotFullscreen",
+                self.__captureCursor
+            )
+            if self.__checkReply(reply, 1):
+                filename = reply.arguments()[0]
+                if filename:
+                    snapshot = QPixmap(filename)
+                    with contextlib.suppress(OSError):
+                        os.remove(filename)
+        elif Globals.isGnomeDesktop():
+            path = self.__temporaryFilename()
+            interface = QDBusInterface(
+                "org.gnome.Shell",
+                "/org/gnome/Shell/Screenshot",
+                "org.gnome.Shell.Screenshot"
+            )
+            reply = interface.call(
+                "Screenshot",
+                self.__captureCursor,
+                False,
+                path
+            )
+            if self.__checkReply(reply, 2):
+                filename = reply.arguments()[1]
+                if filename:
+                    snapshot = QPixmap(filename)
+                    with contextlib.suppress(OSError):
+                        os.remove(filename)
+        
+        self.grabbed.emit(snapshot)
+    
+    def __grabSelectedScreen(self):
+        """
+        Private method to grab a selected screen.
+        """
+        snapshot = QPixmap()
+        
+        if Globals.isKdeDesktop():
+            screen = QApplication.screenAt(QCursor.pos())
+            try:
+                screenId = QApplication.screens().index(screen)
+            except ValueError:
+                # default to screen 0
+                screenId = 0
+            
+            # Step 2: grab the screen
+            interface = QDBusInterface(
+                "org.kde.KWin",
+                "/Screenshot",
+                "org.kde.kwin.Screenshot"
+            )
+            reply = interface.call(
+                "screenshotScreen",
+                screenId,
+                self.__captureCursor
+            )
+            if self.__checkReply(reply, 1):
+                filename = reply.arguments()[0]
+                if filename:
+                    snapshot = QPixmap(filename)
+                    with contextlib.suppress(OSError):
+                        os.remove(filename)
+        elif Globals.isGnomeDesktop():
+            # Step 1: grab entire desktop
+            path = self.__temporaryFilename()
+            interface = QDBusInterface(
+                "org.gnome.Shell",
+                "/org/gnome/Shell/Screenshot",
+                "org.gnome.Shell.Screenshot"
+            )
+            reply = interface.call(
+                "ScreenshotWindow",
+                self.__captureDecorations,
+                self.__captureCursor,
+                False,
+                path
+            )
+            if self.__checkReply(reply, 2):
+                filename = reply.arguments()[1]
+                if filename:
+                    snapshot = QPixmap(filename)
+                    with contextlib.suppress(OSError):
+                        os.remove(filename)
+                    
+                    # Step 2: extract the area of the screen containing
+                    #         the cursor
+                    if not snapshot.isNull():
+                        screen = QApplication.screenAt(QCursor.pos())
+                        geom = screen.geometry()
+                        snapshot = snapshot.copy(geom)
+        
+        self.grabbed.emit(snapshot)
+    
+    def __grabSelectedWindow(self):
+        """
+        Private method to grab a selected window.
+        """
+        snapshot = QPixmap()
+        
+        if Globals.isKdeDesktop():
+            mask = 0
+            if self.__captureDecorations:
+                mask |= 1
+            if self.__captureCursor:
+                mask |= 2
+            interface = QDBusInterface(
+                "org.kde.KWin",
+                "/Screenshot",
+                "org.kde.kwin.Screenshot"
+            )
+            reply = interface.call(
+                "interactive",
+                mask
+            )
+            if self.__checkReply(reply, 1):
+                filename = reply.arguments()[0]
+                if filename:
+                    snapshot = QPixmap(filename)
+                    with contextlib.suppress(OSError):
+                        os.remove(filename)
+        elif Globals.isGnomeDesktop():
+            path = self.__temporaryFilename()
+            interface = QDBusInterface(
+                "org.gnome.Shell",
+                "/org/gnome/Shell/Screenshot",
+                "org.gnome.Shell.Screenshot"
+            )
+            reply = interface.call(
+                "ScreenshotWindow",
+                self.__captureDecorations,
+                self.__captureCursor,
+                False,
+                path
+            )
+            if self.__checkReply(reply, 2):
+                filename = reply.arguments()[1]
+                if filename:
+                    snapshot = QPixmap(filename)
+                    with contextlib.suppress(OSError):
+                        os.remove(filename)
+        
+        self.grabbed.emit(snapshot)
+    
+    def __grabRectangle(self):
+        """
+        Private method to grab a rectangular desktop area.
+        """
+        snapshot = QPixmap()
+        
+        if Globals.isGnomeDesktop():
+            # Step 1: let the user select the area
+            interface = QDBusInterface(
+                "org.gnome.Shell",
+                "/org/gnome/Shell/Screenshot",
+                "org.gnome.Shell.Screenshot"
+            )
+            reply = interface.call("SelectArea")
+            if self.__checkReply(reply, 4):
+                x, y, width, height = reply.arguments()[:4]
+                
+                # Step 2: grab the selected area
+                path = self.__temporaryFilename()
+                reply = interface.call(
+                    "ScreenshotArea",
+                    x, y, width, height,
+                    False,
+                    path
+                )
+                if self.__checkReply(reply, 2):
+                    filename = reply.arguments()[1]
+                    if filename:
+                        snapshot = QPixmap(filename)
+                        with contextlib.suppress(OSError):
+                            os.remove(filename)
+        
+        self.grabbed.emit(snapshot)
+    
+    def __checkReply(self, reply, argumentsCount):
+        """
+        Private method to check, if a reply is valid.
+        
+        @param reply reference to the reply message
+        @type QDBusMessage
+        @param argumentsCount number of expected arguments
+        @type int
+        @return flag indicating validity
+        @rtype bool
+        """
+        if reply.type() == QDBusMessage.MessageType.ReplyMessage:
+            if len(reply.arguments()) == argumentsCount:
+                return True
+            
+            E5MessageBox.warning(
+                None,
+                self.tr("Screenshot Error"),
+                self.tr("<p>Received an unexpected number of reply arguments."
+                        " Expected {0} but got {1}</p>").format(
+                    argumentsCount,
+                    len(reply.arguments()),
+                ))
+        
+        elif reply.type() == QDBusMessage.MessageType.ErrorMessage:
+            E5MessageBox.warning(
+                None,
+                self.tr("Screenshot Error"),
+                self.tr("<p>Received error <b>{0}</b> from DBus while"
+                        " performing screenshot.</p><p>{1}</p>").format(
+                    reply.errorName(),
+                    reply.errorMessage(),
+                ))
+        
+        elif reply.type() == QDBusMessage.MessageType.InvalidMessage:
+            E5MessageBox.warning(
+                None,
+                self.tr("Screenshot Error"),
+                self.tr("Received an invalid reply."))
+        
+        else:
+            E5MessageBox.warning(
+                None,
+                self.tr("Screenshot Error"),
+                self.tr("Received an unexpected reply."))
+        
+        return False
+    
+    def __temporaryFilename(self):
+        """
+        Private method to generate a temporary filename.
+        
+        @return path name for a unique, temporary file
+        @rtype str
+        """
+        return "/tmp/eric-snap-{0}.png".format(uuid.uuid4().hex)   # secok

eric ide

mercurial