Snapshot: implemented preliminary support for Wayland (needs some more testing). screenshot

Tue, 02 Apr 2019 19:52:36 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 02 Apr 2019 19:52:36 +0200
branch
screenshot
changeset 6916
76810bef8425
parent 6915
57f7afc788e4
child 6917
7075ede48c2f

Snapshot: implemented preliminary support for Wayland (needs some more testing).

Snapshot/SnapWidget.py file | annotate | diff | comparison | revisions
Snapshot/SnapWidget.ui file | annotate | diff | comparison | revisions
Snapshot/SnapshotWaylandGrabber.py file | annotate | diff | comparison | revisions
eric6.e4p file | annotate | diff | comparison | revisions
--- a/Snapshot/SnapWidget.py	Tue Apr 02 19:49:00 2019 +0200
+++ b/Snapshot/SnapWidget.py	Tue Apr 02 19:52:36 2019 +0200
@@ -16,8 +16,8 @@
 import os
 
 from PyQt5.QtCore import pyqtSlot, QFile, QFileInfo, QTimer, QPoint, \
-    QMimeData, Qt, QEvent, QRegExp, QLocale, PYQT_VERSION_STR
-from PyQt5.QtGui import QImageWriter, QPixmap, QCursor, QDrag, QKeySequence
+    QMimeData, Qt, QRegExp, QLocale, PYQT_VERSION_STR
+from PyQt5.QtGui import QImageWriter, QPixmap, QDrag, QKeySequence
 from PyQt5.QtWidgets import QWidget, QApplication, QShortcut
 
 from E5Gui import E5FileDialog, E5MessageBox
@@ -27,7 +27,6 @@
 import UI.PixmapCache
 import Preferences
 import Globals
-from Globals import qVersionTuple
 
 from .SnapshotModes import SnapshotModes
 
@@ -36,12 +35,6 @@
     """
     Class implementing the snapshot widget.
     """
-    ModeFullscreen = 0
-    ModeScreen = 1
-    ModeRectangle = 2
-    ModeFreehand = 3
-    ModeEllipse = 4
-    
     def __init__(self, parent=None):
         """
         Constructor
@@ -57,8 +50,16 @@
         self.copyPreviewButton.setIcon(UI.PixmapCache.getIcon("editCopy.png"))
         self.setWindowIcon(UI.PixmapCache.getIcon("ericSnap.png"))
         
-        from .SnapshotDefaultGrabber import SnapshotDefaultGrabber
-        self.__grabber = SnapshotDefaultGrabber(self)
+        # TODO: correct this once tested
+#        if Globals.isWaylandSession():
+        if True:
+            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()
         
@@ -66,16 +67,16 @@
             self.modeCombo.addItem(self.tr("Fullscreen"),
                                    SnapshotModes.Fullscreen)
         if SnapshotModes.SelectedScreen in supportedModes:
-            if qVersionTuple() >= (5, 10, 0):
+            if Globals.qVersionTuple() >= (5, 10, 0):
                 if len(QApplication.screens()) > 1:
-                    self.modeCombo.addItem(self.tr("Selected Screen"),
+                    self.modeCombo.addItem(self.tr("Select Screen"),
                                            SnapshotModes.SelectedScreen)
             else:
                 if QApplication.desktop().screenCount() > 1:
-                    self.modeCombo.addItem(self.tr("Selected Screen"),
+                    self.modeCombo.addItem(self.tr("Select Screen"),
                                            SnapshotModes.SelectedScreen)
         if SnapshotModes.SelectedWindow in supportedModes:
-            self.modeCombo.addItem(self.tr("Selected Window"),
+            self.modeCombo.addItem(self.tr("Select Window"),
                                    SnapshotModes.SelectedWindow)
         if SnapshotModes.Rectangle in supportedModes:
             self.modeCombo.addItem(self.tr("Rectangular Selection"),
@@ -92,9 +93,8 @@
             index = 0
         self.modeCombo.setCurrentIndex(index)
         
-        self.__delay = int(
-            Preferences.Prefs.settings.value("Snapshot/Delay", 0))
-        self.delaySpin.setValue(self.__delay)
+        delay = int(Preferences.Prefs.settings.value("Snapshot/Delay", 0))
+        self.delaySpin.setValue(delay)
         
         if PYQT_VERSION_STR >= "5.0.0":
             from PyQt5.QtCore import QStandardPaths
@@ -307,11 +307,10 @@
         """
         Private slot to take a snapshot.
         """
-        self.__delay = self.delaySpin.value()
-        
         self.__savedPosition = self.pos()
         self.hide()
         
+        # TODO: add support for decorations and mouse cursor
         self.__grabber.grab(
             self.modeCombo.itemData(self.modeCombo.currentIndex()),
             self.delaySpin.value())
@@ -429,3 +428,19 @@
             self.tr("eric6 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)
--- a/Snapshot/SnapWidget.ui	Tue Apr 02 19:49:00 2019 +0200
+++ b/Snapshot/SnapWidget.ui	Tue Apr 02 19:52:36 2019 +0200
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>500</width>
-    <height>400</height>
+    <width>550</width>
+    <height>450</height>
    </rect>
   </property>
   <property name="minimumSize">
@@ -47,7 +47,7 @@
     </widget>
    </item>
    <item>
-    <layout class="QFormLayout" name="formLayout">
+    <layout class="QGridLayout" name="gridLayout">
      <item row="0" column="0">
       <widget class="QLabel" name="label">
        <property name="text">
@@ -108,6 +108,30 @@
        </item>
       </layout>
      </item>
+     <item row="2" column="0" colspan="2">
+      <layout class="QHBoxLayout" name="horizontalLayout_4">
+       <item>
+        <widget class="QCheckBox" name="decorationsCheckBox">
+         <property name="toolTip">
+          <string>Select to include the window decorations in the screenshot</string>
+         </property>
+         <property name="text">
+          <string>include Decorations</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QCheckBox" name="mouseCursorCheckBox">
+         <property name="toolTip">
+          <string>Select to include the mouse cursor in the screenshot</string>
+         </property>
+         <property name="text">
+          <string>include Mouse Cursor</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </item>
     </layout>
    </item>
    <item>
@@ -236,11 +260,13 @@
  <tabstops>
   <tabstop>modeCombo</tabstop>
   <tabstop>delaySpin</tabstop>
+  <tabstop>decorationsCheckBox</tabstop>
+  <tabstop>mouseCursorCheckBox</tabstop>
   <tabstop>takeButton</tabstop>
   <tabstop>pathNameEdit</tabstop>
   <tabstop>saveButton</tabstop>
+  <tabstop>copyPreviewButton</tabstop>
   <tabstop>copyButton</tabstop>
-  <tabstop>copyPreviewButton</tabstop>
  </tabstops>
  <resources/>
  <connections/>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Snapshot/SnapshotWaylandGrabber.py	Tue Apr 02 19:52:36 2019 +0200
@@ -0,0 +1,398 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a grabber object for non-Wayland desktops.
+"""
+
+from __future__ import unicode_literals
+
+import os
+import uuid
+
+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(SnapshotWaylandGrabber, self).__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():
+            modes = (
+                SnapshotModes.Fullscreen,
+                SnapshotModes.SelectedScreen,
+                SnapshotModes.SelectedWindow,
+            )
+        elif DBusAvailable and Globals.isGnomeDesktop():
+            modes = (
+                SnapshotModes.Fullscreen,
+                SnapshotModes.SelectedScreen,
+                SnapshotModes.SelectedWindow,
+                SnapshotModes.Rectangle,
+            )
+        else:
+            modes = tuple()
+        
+        return modes
+    
+    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)
+        """
+        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 == SnapshotModes.Fullscreen:
+            self.__grabFullscreen()
+        elif self.__mode == SnapshotModes.SelectedScreen:
+            self.__grabSelectedScreen()
+        elif self.__mode == SnapshotModes.SelectedWindow:
+            self.__grabSelectedWindow()
+        elif self.__mode == SnapshotModes.Rectangle:
+            self.__grabRectangle()
+        else:
+            raise RuntimeError("unsupported grab mode given")
+    
+    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)
+                    try:
+                        os.remove(filename)
+                    except OSError:
+                        # just ignore it
+                        pass
+        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)
+                    try:
+                        os.remove(filename)
+                    except OSError:
+                        # just ignore it
+                        pass
+        
+        self.grabbed.emit(snapshot)
+    
+    def __grabSelectedScreen(self):
+        """
+        Private method to grab a selected screen.
+        """
+        snapshot = QPixmap()
+        
+        if Globals.isKdeDesktop():
+            # Step 1: get the screen number of screen containing the cursor
+            if Globals.qVersionTuple() >= (5, 10, 0):
+                screen = QApplication.screenAt(QCursor.pos())
+                try:
+                    screenId = QApplication.screens().index(screen)
+                except ValueError:
+                    # default to screen 0
+                    screenId = 0
+            else:
+                desktop = QApplication.desktop()
+                screenId = desktop.screenNumber(QCursor.pos())
+            
+            # 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)
+                    try:
+                        os.remove(filename)
+                    except OSError:
+                        # just ignore it
+                        pass
+        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)
+                    try:
+                        os.remove(filename)
+                    except OSError:
+                        # just ignore it
+                        pass
+                    
+                    # Step 2: extract the area of the screen containing
+                    #         the cursor
+                    if not snapshot.isNull():
+                        if Globals.qVersionTuple() >= (5, 10, 0):
+                            screen = QApplication.screenAt(QCursor.pos())
+                            geom = screen.geometry()
+                        else:
+                            desktop = QApplication.desktop()
+                            screenId = desktop.screenNumber(QCursor.pos())
+                            geom = desktop.screenGeometry(screenId)
+                        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)
+                    try:
+                        os.remove(filename)
+                    except OSError:
+                        # just ignore it
+                        pass
+        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)
+                    try:
+                        os.remove(filename)
+                    except OSError:
+                        # just ignore it
+                        pass
+        
+        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)
+                        try:
+                            os.remove(filename)
+                        except OSError:
+                            # just ignore it
+                            pass
+        
+        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.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.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.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/eric6-snap-{0}.png".format(uuid.uuid4().hex)
--- a/eric6.e4p	Tue Apr 02 19:49:00 2019 +0200
+++ b/eric6.e4p	Tue Apr 02 19:52:36 2019 +0200
@@ -1043,6 +1043,7 @@
     <Source>Snapshot/SnapshotPreview.py</Source>
     <Source>Snapshot/SnapshotRegionGrabber.py</Source>
     <Source>Snapshot/SnapshotTimer.py</Source>
+    <Source>Snapshot/SnapshotWaylandGrabber.py</Source>
     <Source>Snapshot/__init__.py</Source>
     <Source>SqlBrowser/SqlBrowser.py</Source>
     <Source>SqlBrowser/SqlBrowserWidget.py</Source>

eric ide

mercurial