Merged witzh branch 'screenshot' to start new development cycle.

Fri, 05 Apr 2019 19:19:43 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 05 Apr 2019 19:19:43 +0200
changeset 6926
519145f2ba79
parent 6922
b5d830bdea12 (current diff)
parent 6921
4783b00711d5 (diff)
child 6927
50c60a7a8bab

Merged witzh branch 'screenshot' to start new development cycle.

Snapshot/SnapWidget.py file | annotate | diff | comparison | revisions
--- a/Globals/__init__.py	Fri Apr 05 18:59:36 2019 +0200
+++ b/Globals/__init__.py	Fri Apr 05 19:19:43 2019 +0200
@@ -92,9 +92,100 @@
     if currDesktop:
         return currDesktop
     
+    currDesktop = os.environ.get("GNOME_DESKTOP_SESSION_ID", "")
+    if currDesktop:
+        return currDesktop
+    
+    currDesktop = os.environ.get("KDE_FULL_SESSION", "")
+    if currDesktop:
+        return currDesktop
+    
+    currDesktop = os.environ.get("DESKTOP_SESSION", "")
+    if currDesktop:
+        return currDesktop
+    
     return ""
 
 
+def isKdeDesktop():
+    """
+    Function to check, if the current session is a KDE desktop (Linux only).
+    
+    @return flag indicating a KDE desktop
+    @rtype bool
+    """
+    if not isLinuxPlatform():
+        return False
+    
+    isKDE = False
+    
+    desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower() or \
+        os.environ.get("XDG_SESSION_DESKTOP", "").lower() or \
+        os.environ.get("DESKTOP_SESSION", "").lower()
+    if desktop:
+        isKDE = "kde" in desktop or "plasma" in desktop
+    else:
+        isKDE = bool(os.environ.get("KDE_FULL_SESSION", ""))
+    
+    return isKDE
+
+
+def isGnomeDesktop():
+    """
+    Function to check, if the current session is a Gnome desktop (Linux only).
+    
+    @return flag indicating a Gnome desktop
+    @rtype bool
+    """
+    if not isLinuxPlatform():
+        return False
+    
+    isGnome = False
+    
+    desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower() or \
+        os.environ.get("XDG_SESSION_DESKTOP", "").lower() or \
+        os.environ.get("GDMSESSION", "").lower()
+    if desktop:
+        isGnome = "gnome" in desktop
+    else:
+        isGnome = bool(os.environ.get("GNOME_DESKTOP_SESSION_ID", ""))
+    
+    return isGnome
+
+
+def sessionType():
+    """
+    Function to determine the name of the running session (Linux only).
+    
+    @return name of the desktop environment
+    @rtype str
+    """
+    if not isLinuxPlatform():
+        return ""
+    
+    sessionType = os.environ.get("XDG_SESSION_TYPE").lower()
+    if "x11" in sessionType:
+        return "X11"
+    elif "wayland" in sessionType:
+        return "Wayland"
+    
+    sessionType = os.environ.get("WAYLAND_DISPLAY", "").lower()
+    if "wayland" in sessionType:
+        return "Wayland"
+    
+    return ""
+
+
+def isWaylandSession():
+    """
+    Function to check, if the current session is a wayland session.
+    
+    @return flag indicating a wayland session
+    @rtype bool
+    """
+    return sessionType() == "Wayland"
+
+
 def checkBlacklistedVersions():
     """
     Module functions to check for blacklisted versions of the prerequisites.
--- a/Snapshot/SnapWidget.py	Fri Apr 05 18:59:36 2019 +0200
+++ b/Snapshot/SnapWidget.py	Fri Apr 05 19:19:43 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,19 +27,14 @@
 import UI.PixmapCache
 import Preferences
 import Globals
-from Globals import qVersionTuple
+
+from .SnapshotModes import SnapshotModes
 
 
 class SnapWidget(QWidget, Ui_SnapWidget):
     """
     Class implementing the snapshot widget.
     """
-    ModeFullscreen = 0
-    ModeScreen = 1
-    ModeRectangle = 2
-    ModeFreehand = 3
-    ModeEllipse = 4
-    
     def __init__(self, parent=None):
         """
         Constructor
@@ -55,31 +50,49 @@
         self.copyPreviewButton.setIcon(UI.PixmapCache.getIcon("editCopy.png"))
         self.setWindowIcon(UI.PixmapCache.getIcon("ericSnap.png"))
         
-        self.modeCombo.addItem(self.tr("Fullscreen"),
-                               SnapWidget.ModeFullscreen)
-        if qVersionTuple() >= (5, 10, 0):
-            if len(QApplication.screens()) > 1:
-                self.modeCombo.addItem(self.tr("Select Screen"),
-                                       SnapWidget.ModeScreen)
+        if Globals.isWaylandSession():
+            from .SnapshotWaylandGrabber import SnapshotWaylandGrabber
+            self.__grabber = SnapshotWaylandGrabber(self)
         else:
-            if QApplication.desktop().screenCount() > 1:
-                self.modeCombo.addItem(self.tr("Select Screen"),
-                                       SnapWidget.ModeScreen)
-        self.modeCombo.addItem(self.tr("Rectangular Selection"),
-                               SnapWidget.ModeRectangle)
-        self.modeCombo.addItem(self.tr("Elliptical Selection"),
-                               SnapWidget.ModeEllipse)
-        self.modeCombo.addItem(self.tr("Freehand Selection"),
-                               SnapWidget.ModeFreehand)
-        self.__mode = int(Preferences.Prefs.settings.value("Snapshot/Mode", 0))
-        index = self.modeCombo.findData(self.__mode)
+            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:
+            if Globals.qVersionTuple() >= (5, 10, 0):
+                if len(QApplication.screens()) > 1:
+                    self.modeCombo.addItem(self.tr("Select Screen"),
+                                           SnapshotModes.SelectedScreen)
+            else:
+                if QApplication.desktop().screenCount() > 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.Prefs.settings.value("Snapshot/Mode", 0))
+        index = self.modeCombo.findData(SnapshotModes(mode))
         if index == -1:
             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
@@ -94,25 +107,16 @@
             os.path.join(picturesLocation,
                          self.tr("snapshot") + "1.png"))
         
-        self.__grabber = None
         self.__snapshot = QPixmap()
         self.__savedPosition = QPoint()
         self.__modified = False
         self.__locale = QLocale()
         
-        self.__grabberWidget = QWidget(None, Qt.X11BypassWindowManagerHint)
-        self.__grabberWidget.move(-10000, -10000)
-        self.__grabberWidget.installEventFilter(self)
-        
         self.__initFileFilters()
-        
         self.__initShortcuts()
         
         self.preview.startDrag.connect(self.__dragSnapshot)
         
-        from .SnapshotTimer import SnapshotTimer
-        self.__grabTimer = SnapshotTimer()
-        self.__grabTimer.timeout.connect(self.__grabTimerTimeout)
         self.__updateTimer = QTimer()
         self.__updateTimer.setSingleShot(True)
         self.__updateTimer.timeout.connect(self.__updatePreview)
@@ -301,113 +305,15 @@
         """
         Private slot to take a snapshot.
         """
-        self.__mode = self.modeCombo.itemData(self.modeCombo.currentIndex())
-        self.__delay = self.delaySpin.value()
-        
         self.__savedPosition = self.pos()
         self.hide()
         
-        if self.__delay:
-            self.__grabTimer.start(self.__delay)
-        else:
-            QTimer.singleShot(200, self.__startUndelayedGrab)
-    
-    def __grabTimerTimeout(self):
-        """
-        Private slot to perform a delayed grab operation.
-        """
-        if self.__mode == SnapWidget.ModeRectangle:
-            self.__grabRectangle()
-        elif self.__mode == SnapWidget.ModeEllipse:
-            self.__grabEllipse()
-        elif self.__mode == SnapWidget.ModeFreehand:
-            self.__grabFreehand()
-        else:
-            self.__performGrab()
-    
-    def __startUndelayedGrab(self):
-        """
-        Private slot to perform an undelayed grab operation.
-        """
-        if self.__mode == SnapWidget.ModeRectangle:
-            self.__grabRectangle()
-        elif self.__mode == SnapWidget.ModeEllipse:
-            self.__grabEllipse()
-        elif self.__mode == SnapWidget.ModeFreehand:
-            self.__grabFreehand()
-        else:
-            if Globals.isMacPlatform():
-                self.__performGrab()
-            else:
-                self.__grabberWidget.show()
-                self.__grabberWidget.grabMouse(Qt.CrossCursor)
-    
-    def __grabRectangle(self):
-        """
-        Private method to grab a rectangular screen region.
-        """
-        from .SnapshotRegionGrabber import SnapshotRegionGrabber
-        self.__grabber = SnapshotRegionGrabber(
-            mode=SnapshotRegionGrabber.Rectangle)
-        self.__grabber.grabbed.connect(self.__captured)
-    
-    def __grabEllipse(self):
-        """
-        Private method to grab an elliptical screen region.
-        """
-        from .SnapshotRegionGrabber import SnapshotRegionGrabber
-        self.__grabber = SnapshotRegionGrabber(
-            mode=SnapshotRegionGrabber.Ellipse)
-        self.__grabber.grabbed.connect(self.__captured)
-    
-    def __grabFreehand(self):
-        """
-        Private method to grab a non-rectangular screen region.
-        """
-        from .SnapshotFreehandGrabber import SnapshotFreehandGrabber
-        self.__grabber = SnapshotFreehandGrabber()
-        self.__grabber.grabbed.connect(self.__captured)
-    
-    def __performGrab(self):
-        """
-        Private method to perform a screen grab other than a selected region.
-        """
-        self.__grabberWidget.releaseMouse()
-        self.__grabberWidget.hide()
-        self.__grabTimer.stop()
-        
-        if self.__mode == SnapWidget.ModeFullscreen:
-            desktop = QApplication.desktop()
-            if qVersionTuple() >= (5, 0, 0):
-                self.__snapshot = QApplication.screens()[0].grabWindow(
-                    desktop.winId(), desktop.x(), desktop.y(),
-                    desktop.width(), desktop.height())
-            else:
-                self.__snapshot = QPixmap.grabWindow(
-                    desktop.winId(), desktop.x(), desktop.y(),
-                    desktop.width(), desktop.height())
-        elif self.__mode == SnapWidget.ModeScreen:
-            desktop = QApplication.desktop()
-            if qVersionTuple() >= (5, 10, 0):
-                screen = QApplication.screenAt(QCursor.pos())
-                geom = screen.geometry()
-            else:
-                screenId = desktop.screenNumber(QCursor.pos())
-                geom = desktop.screenGeometry(screenId)
-            x = geom.x()
-            y = geom.y()
-            if qVersionTuple() >= (5, 0, 0):
-                self.__snapshot = QApplication.screens()[0].grabWindow(
-                    desktop.winId(), x, y, geom.width(), geom.height())
-            else:
-                self.__snapshot = QPixmap.grabWindow(
-                    desktop.winId(), x, y, geom.width(), geom.height())
-        else:
-            self.__snapshot = QPixmap()
-        
-        self.__redisplay()
-        self.__modified = True
-        self.__updateCaption()
+        self.__grabber.grab(
+            self.modeCombo.itemData(self.modeCombo.currentIndex()),
+            self.delaySpin.value(),
+            self.mouseCursorCheckBox.isChecked(),
+            self.decorationsCheckBox.isChecked(),
+        )
     
     def __redisplay(self):
         """
@@ -445,14 +351,10 @@
         
         @param pixmap pixmap of the snapshot (QPixmap)
         """
-        self.__grabber.close()
         self.__snapshot = QPixmap(pixmap)
         
-        self.__grabber.grabbed.disconnect(self.__captured)
-        self.__grabber = None
-        
         self.__redisplay()
-        self.__modified = True
+        self.__modified = not pixmap.isNull()
         self.__updateCaption()
     
     def __updatePreview(self):
@@ -486,23 +388,6 @@
         drag.setPixmap(self.preview.pixmap())
         drag.exec_(Qt.CopyAction)
     
-    def eventFilter(self, obj, evt):
-        """
-        Public method to handle event for other objects.
-        
-        @param obj reference to the object (QObject)
-        @param evt reference to the event (QEvent)
-        @return flag indicating that the event should be filtered out (boolean)
-        """
-        if obj == self.__grabberWidget and \
-                evt.type() == QEvent.MouseButtonPress:
-            if QWidget.mouseGrabber() != self.__grabberWidget:
-                return False
-            if evt.button() == Qt.LeftButton:
-                self.__performGrab()
-        
-        return False
-    
     def closeEvent(self, evt):
         """
         Protected method handling the close event.
@@ -527,9 +412,11 @@
         
         Preferences.Prefs.settings.setValue(
             "Snapshot/Delay", self.delaySpin.value())
-        Preferences.Prefs.settings.setValue(
-            "Snapshot/Mode",
-            self.modeCombo.itemData(self.modeCombo.currentIndex()))
+        modeData = self.modeCombo.itemData(self.modeCombo.currentIndex())
+        if modeData is not None:
+            Preferences.Prefs.settings.setValue(
+                "Snapshot/Mode",
+                modeData.value)
         Preferences.Prefs.settings.setValue(
             "Snapshot/Filename", self.__filename)
         Preferences.Prefs.settings.sync()
@@ -543,3 +430,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	Fri Apr 05 18:59:36 2019 +0200
+++ b/Snapshot/SnapWidget.ui	Fri Apr 05 19:19:43 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/SnapshotDefaultGrabber.py	Fri Apr 05 19:19:43 2019 +0200
@@ -0,0 +1,211 @@
+# -*- 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
+
+from PyQt5.QtCore import pyqtSignal, Qt, QObject, QTimer, QEvent
+from PyQt5.QtGui import QPixmap, QCursor
+from PyQt5.QtWidgets import QWidget, QApplication
+
+from .SnapshotModes import SnapshotModes
+
+import Globals
+
+
+class SnapshotDefaultGrabber(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(SnapshotDefaultGrabber, self).__init__(parent)
+        
+        self.__grabber = None
+        self.__grabberWidget = QWidget(None, Qt.X11BypassWindowManagerHint)
+        self.__grabberWidget.move(-10000, -10000)
+        self.__grabberWidget.installEventFilter(self)
+        
+        from .SnapshotTimer import SnapshotTimer
+        self.__grabTimer = SnapshotTimer()
+        self.__grabTimer.timeout.connect(self.__grabTimerTimeout)
+    
+    def supportedModes(self):
+        """
+        Public method to get the supported screenshot modes.
+        
+        @return tuple of supported screenshot modes
+        @rtype tuple of SnapshotModes
+        """
+        return (
+            SnapshotModes.Fullscreen,
+            SnapshotModes.SelectedScreen,
+            SnapshotModes.Rectangle,
+            SnapshotModes.Freehand,
+            SnapshotModes.Ellipse,
+        )
+    
+    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
+            (not used)
+        @type bool
+        @param captureDecorations flag indicating to include the window
+            decorations (not used)
+        @type bool
+        """
+        self.__mode = mode
+        if delay:
+            self.__grabTimer.start(delay)
+        else:
+            QTimer.singleShot(200, self.__startUndelayedGrab)
+    
+    def __grabTimerTimeout(self):
+        """
+        Private slot to perform a delayed grab operation.
+        """
+        if self.__mode == SnapshotModes.Rectangle:
+            self.__grabRectangle()
+        elif self.__mode == SnapshotModes.Ellipse:
+            self.__grabEllipse()
+        elif self.__mode == SnapshotModes.Freehand:
+            self.__grabFreehand()
+        else:
+            self.__performGrab(self.__mode)
+    
+    def __startUndelayedGrab(self):
+        """
+        Private slot to perform an undelayed grab operation.
+        """
+        if self.__mode == SnapshotModes.Rectangle:
+            self.__grabRectangle()
+        elif self.__mode == SnapshotModes.Ellipse:
+            self.__grabEllipse()
+        elif self.__mode == SnapshotModes.Freehand:
+            self.__grabFreehand()
+        else:
+            if Globals.isMacPlatform():
+                self.__performGrab(self.__mode)
+            else:
+                self.__grabberWidget.show()
+                self.__grabberWidget.grabMouse(Qt.CrossCursor)
+    
+    def __grabRectangle(self):
+        """
+        Private method to grab a rectangular screen region.
+        """
+        from .SnapshotRegionGrabber import SnapshotRegionGrabber
+        self.__grabber = SnapshotRegionGrabber(
+            mode=SnapshotRegionGrabber.Rectangle)
+        self.__grabber.grabbed.connect(self.__captured)
+    
+    def __grabEllipse(self):
+        """
+        Private method to grab an elliptical screen region.
+        """
+        from .SnapshotRegionGrabber import SnapshotRegionGrabber
+        self.__grabber = SnapshotRegionGrabber(
+            mode=SnapshotRegionGrabber.Ellipse)
+        self.__grabber.grabbed.connect(self.__captured)
+    
+    def __grabFreehand(self):
+        """
+        Private method to grab a non-rectangular screen region.
+        """
+        from .SnapshotFreehandGrabber import SnapshotFreehandGrabber
+        self.__grabber = SnapshotFreehandGrabber()
+        self.__grabber.grabbed.connect(self.__captured)
+    
+    def __performGrab(self, mode):
+        """
+        Private method to perform a screen grab other than a selected region.
+        
+        @param mode screenshot mode
+        @type SnapshotModes
+        """
+        self.__grabberWidget.releaseMouse()
+        self.__grabberWidget.hide()
+        self.__grabTimer.stop()
+        
+        if mode == SnapshotModes.Fullscreen:
+            desktop = QApplication.desktop()
+            if Globals.qVersionTuple() >= (5, 0, 0):
+                snapshot = QApplication.screens()[0].grabWindow(
+                    desktop.winId(), desktop.x(), desktop.y(),
+                    desktop.width(), desktop.height())
+            else:
+                snapshot = QPixmap.grabWindow(
+                    desktop.winId(), desktop.x(), desktop.y(),
+                    desktop.width(), desktop.height())
+        elif mode == SnapshotModes.SelectedScreen:
+            desktop = QApplication.desktop()
+            if Globals.qVersionTuple() >= (5, 10, 0):
+                screen = QApplication.screenAt(QCursor.pos())
+                geom = screen.geometry()
+            else:
+                screenId = desktop.screenNumber(QCursor.pos())
+                geom = desktop.screenGeometry(screenId)
+            x = geom.x()
+            y = geom.y()
+            if Globals.qVersionTuple() >= (5, 0, 0):
+                snapshot = QApplication.screens()[0].grabWindow(
+                    desktop.winId(), x, y, geom.width(), geom.height())
+            else:
+                snapshot = QPixmap.grabWindow(
+                    desktop.winId(), x, y, geom.width(), geom.height())
+        else:
+            snapshot = QPixmap()
+        
+        self.grabbed.emit(snapshot)
+    
+    def __captured(self, pixmap):
+        """
+        Private slot to show a preview of the snapshot.
+        
+        @param pixmap pixmap of the snapshot (QPixmap)
+        """
+        self.__grabber.close()
+        snapshot = QPixmap(pixmap)
+        
+        self.__grabber.grabbed.disconnect(self.__captured)
+        self.__grabber = None
+        
+        self.grabbed.emit(snapshot)
+    
+    def eventFilter(self, obj, evt):
+        """
+        Public method to handle event for other objects.
+        
+        @param obj reference to the object (QObject)
+        @param evt reference to the event (QEvent)
+        @return flag indicating that the event should be filtered out (boolean)
+        """
+        if obj == self.__grabberWidget and \
+                evt.type() == QEvent.MouseButtonPress:
+            if QWidget.mouseGrabber() != self.__grabberWidget:
+                return False
+            if evt.button() == Qt.LeftButton:
+                self.__performGrab(self.__mode)
+        
+        return False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Snapshot/SnapshotModes.py	Fri Apr 05 19:19:43 2019 +0200
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the snapshot mode enumeration.
+"""
+
+from __future__ import unicode_literals
+
+try:
+    from enum import Enum
+except ImportError:
+    from ThirdParty.enum import Enum
+
+
+class SnapshotModes(Enum):
+    """
+    Class implementing the snapshot modes.
+    """
+    Fullscreen = 0
+    SelectedScreen = 1
+    Rectangle = 2
+    Freehand = 3
+    Ellipse = 4
+    SelectedWindow = 5
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Snapshot/SnapshotWaylandGrabber.py	Fri Apr 05 19:19:43 2019 +0200
@@ -0,0 +1,399 @@
+# -*- 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)
+        @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 == 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/changelog	Fri Apr 05 18:59:36 2019 +0200
+++ b/changelog	Fri Apr 05 19:19:43 2019 +0200
@@ -1,5 +1,10 @@
 Change Log
 ----------
+Version 19.05:
+- bug fixes
+- Snapshot Tool
+  -- added code to support screenshots on Wayland desktops (KDE and Gnome)
+
 Version 19.04:
 - bug fixes
 - Code Style Checker
--- a/eric6.e4p	Fri Apr 05 18:59:36 2019 +0200
+++ b/eric6.e4p	Fri Apr 05 19:19:43 2019 +0200
@@ -1037,10 +1037,13 @@
     <Source>QScintilla/ZoomDialog.py</Source>
     <Source>QScintilla/__init__.py</Source>
     <Source>Snapshot/SnapWidget.py</Source>
+    <Source>Snapshot/SnapshotDefaultGrabber.py</Source>
     <Source>Snapshot/SnapshotFreehandGrabber.py</Source>
+    <Source>Snapshot/SnapshotModes.py</Source>
     <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>
@@ -2259,14 +2262,14 @@
   </Resources>
   <Others>
     <Other>.hgignore</Other>
-    <Other>APIs/Python/zope-2.10.7.api</Other>
-    <Other>APIs/Python/zope-2.11.2.api</Other>
-    <Other>APIs/Python/zope-3.3.1.api</Other>
     <Other>APIs/Python3/PyQt4.bas</Other>
     <Other>APIs/Python3/PyQt5.bas</Other>
     <Other>APIs/Python3/QScintilla2.bas</Other>
     <Other>APIs/Python3/eric6.api</Other>
     <Other>APIs/Python3/eric6.bas</Other>
+    <Other>APIs/Python/zope-2.10.7.api</Other>
+    <Other>APIs/Python/zope-2.11.2.api</Other>
+    <Other>APIs/Python/zope-3.3.1.api</Other>
     <Other>APIs/QSS/qss.api</Other>
     <Other>APIs/Ruby/Ruby-1.8.7.api</Other>
     <Other>APIs/Ruby/Ruby-1.8.7.bas</Other>

eric ide

mercurial