Tue, 02 Apr 2019 19:52:36 +0200
Snapshot: implemented preliminary support for Wayland (needs some more testing).
--- 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>