src/eric7/Snapshot/SnapshotWaylandGrabber.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a grabber object for non-Wayland desktops.
8 """
9
10 import os
11 import uuid
12 import contextlib
13
14 from PyQt6.QtCore import pyqtSignal, QObject, QTimer
15 from PyQt6.QtGui import QPixmap, QCursor
16 from PyQt6.QtWidgets import QApplication
17
18 try:
19 from PyQt6.QtDBus import QDBusInterface, QDBusMessage
20 DBusAvailable = True
21 except ImportError:
22 DBusAvailable = False
23
24 from EricWidgets import EricMessageBox
25
26 from .SnapshotModes import SnapshotModes
27
28 import Globals
29
30
31 class SnapshotWaylandGrabber(QObject):
32 """
33 Class implementing a grabber object for non-Wayland desktops.
34
35 @signal grabbed(QPixmap) emitted after the grab operation is finished
36 """
37 grabbed = pyqtSignal(QPixmap)
38
39 def __init__(self, parent=None):
40 """
41 Constructor
42
43 @param parent reference to the parent object
44 @type QObject
45 """
46 super().__init__(parent)
47
48 from .SnapshotTimer import SnapshotTimer
49 self.__grabTimer = SnapshotTimer()
50 self.__grabTimer.timeout.connect(self.__performGrab)
51
52 def supportedModes(self):
53 """
54 Public method to get the supported screenshot modes.
55
56 @return tuple of supported screenshot modes
57 @rtype tuple of SnapshotModes
58 """
59 if DBusAvailable and Globals.isKdeDesktop():
60 # __IGNORE_WARNING_Y114__
61 return (
62 SnapshotModes.FULLSCREEN,
63 SnapshotModes.SELECTEDSCREEN,
64 SnapshotModes.SELECTEDWINDOW,
65 )
66 elif DBusAvailable and Globals.isGnomeDesktop():
67 return (
68 SnapshotModes.FULLSCREEN,
69 SnapshotModes.SELECTEDSCREEN,
70 SnapshotModes.SELECTEDWINDOW,
71 SnapshotModes.RECTANGLE,
72 )
73 else:
74 return ()
75
76 def grab(self, mode, delay=0, captureCursor=False,
77 captureDecorations=False):
78 """
79 Public method to perform a grab operation potentially after a delay.
80
81 @param mode screenshot mode
82 @type ScreenshotModes
83 @param delay delay in seconds
84 @type int
85 @param captureCursor flag indicating to include the mouse cursor
86 @type bool
87 @param captureDecorations flag indicating to include the window
88 decorations (only used for mode SnapshotModes.SELECTEDWINDOW)
89 @type bool
90 """
91 if not DBusAvailable:
92 # just to play it safe
93 self.grabbed.emit(QPixmap())
94 return
95
96 self.__mode = mode
97 self.__captureCursor = captureCursor
98 self.__captureDecorations = captureDecorations
99 if delay:
100 self.__grabTimer.start(delay)
101 else:
102 QTimer.singleShot(200, self.__performGrab)
103
104 def __performGrab(self):
105 """
106 Private method to perform the grab operations.
107
108 @exception RuntimeError raised to indicate an unsupported grab mode
109 """
110 if self.__mode not in (
111 SnapshotModes.FULLSCREEN, SnapshotModes.SELECTEDSCREEN,
112 SnapshotModes.SELECTEDWINDOW, SnapshotModes.RECTANGLE,
113 ):
114 raise RuntimeError("unsupported grab mode given")
115
116 if self.__mode == SnapshotModes.FULLSCREEN:
117 self.__grabFullscreen()
118 elif self.__mode == SnapshotModes.SELECTEDSCREEN:
119 self.__grabSelectedScreen()
120 elif self.__mode == SnapshotModes.SELECTEDWINDOW:
121 self.__grabSelectedWindow()
122 else:
123 self.__grabRectangle()
124
125 def __grabFullscreen(self):
126 """
127 Private method to grab the complete desktop.
128 """
129 snapshot = QPixmap()
130
131 if Globals.isKdeDesktop():
132 interface = QDBusInterface(
133 "org.kde.KWin",
134 "/Screenshot",
135 "org.kde.kwin.Screenshot"
136 )
137 reply = interface.call(
138 "screenshotFullscreen",
139 self.__captureCursor
140 )
141 if self.__checkReply(reply, 1):
142 filename = reply.arguments()[0]
143 if filename:
144 snapshot = QPixmap(filename)
145 with contextlib.suppress(OSError):
146 os.remove(filename)
147 elif Globals.isGnomeDesktop():
148 path = self.__temporaryFilename()
149 interface = QDBusInterface(
150 "org.gnome.Shell",
151 "/org/gnome/Shell/Screenshot",
152 "org.gnome.Shell.Screenshot"
153 )
154 reply = interface.call(
155 "Screenshot",
156 self.__captureCursor,
157 False,
158 path
159 )
160 if self.__checkReply(reply, 2):
161 filename = reply.arguments()[1]
162 if filename:
163 snapshot = QPixmap(filename)
164 with contextlib.suppress(OSError):
165 os.remove(filename)
166
167 self.grabbed.emit(snapshot)
168
169 def __grabSelectedScreen(self):
170 """
171 Private method to grab a selected screen.
172 """
173 snapshot = QPixmap()
174
175 if Globals.isKdeDesktop():
176 screen = QApplication.screenAt(QCursor.pos())
177 try:
178 screenId = QApplication.screens().index(screen)
179 except ValueError:
180 # default to screen 0
181 screenId = 0
182
183 # Step 2: grab the screen
184 interface = QDBusInterface(
185 "org.kde.KWin",
186 "/Screenshot",
187 "org.kde.kwin.Screenshot"
188 )
189 reply = interface.call(
190 "screenshotScreen",
191 screenId,
192 self.__captureCursor
193 )
194 if self.__checkReply(reply, 1):
195 filename = reply.arguments()[0]
196 if filename:
197 snapshot = QPixmap(filename)
198 with contextlib.suppress(OSError):
199 os.remove(filename)
200 elif Globals.isGnomeDesktop():
201 # Step 1: grab entire desktop
202 path = self.__temporaryFilename()
203 interface = QDBusInterface(
204 "org.gnome.Shell",
205 "/org/gnome/Shell/Screenshot",
206 "org.gnome.Shell.Screenshot"
207 )
208 reply = interface.call(
209 "ScreenshotWindow",
210 self.__captureDecorations,
211 self.__captureCursor,
212 False,
213 path
214 )
215 if self.__checkReply(reply, 2):
216 filename = reply.arguments()[1]
217 if filename:
218 snapshot = QPixmap(filename)
219 with contextlib.suppress(OSError):
220 os.remove(filename)
221
222 # Step 2: extract the area of the screen containing
223 # the cursor
224 if not snapshot.isNull():
225 screen = QApplication.screenAt(QCursor.pos())
226 geom = screen.geometry()
227 snapshot = snapshot.copy(geom)
228
229 self.grabbed.emit(snapshot)
230
231 def __grabSelectedWindow(self):
232 """
233 Private method to grab a selected window.
234 """
235 snapshot = QPixmap()
236
237 if Globals.isKdeDesktop():
238 mask = 0
239 if self.__captureDecorations:
240 mask |= 1
241 if self.__captureCursor:
242 mask |= 2
243 interface = QDBusInterface(
244 "org.kde.KWin",
245 "/Screenshot",
246 "org.kde.kwin.Screenshot"
247 )
248 reply = interface.call(
249 "interactive",
250 mask
251 )
252 if self.__checkReply(reply, 1):
253 filename = reply.arguments()[0]
254 if filename:
255 snapshot = QPixmap(filename)
256 with contextlib.suppress(OSError):
257 os.remove(filename)
258 elif Globals.isGnomeDesktop():
259 path = self.__temporaryFilename()
260 interface = QDBusInterface(
261 "org.gnome.Shell",
262 "/org/gnome/Shell/Screenshot",
263 "org.gnome.Shell.Screenshot"
264 )
265 reply = interface.call(
266 "ScreenshotWindow",
267 self.__captureDecorations,
268 self.__captureCursor,
269 False,
270 path
271 )
272 if self.__checkReply(reply, 2):
273 filename = reply.arguments()[1]
274 if filename:
275 snapshot = QPixmap(filename)
276 with contextlib.suppress(OSError):
277 os.remove(filename)
278
279 self.grabbed.emit(snapshot)
280
281 def __grabRectangle(self):
282 """
283 Private method to grab a rectangular desktop area.
284 """
285 snapshot = QPixmap()
286
287 if Globals.isGnomeDesktop():
288 # Step 1: let the user select the area
289 interface = QDBusInterface(
290 "org.gnome.Shell",
291 "/org/gnome/Shell/Screenshot",
292 "org.gnome.Shell.Screenshot"
293 )
294 reply = interface.call("SelectArea")
295 if self.__checkReply(reply, 4):
296 x, y, width, height = reply.arguments()[:4]
297
298 # Step 2: grab the selected area
299 path = self.__temporaryFilename()
300 reply = interface.call(
301 "ScreenshotArea",
302 x, y, width, height,
303 False,
304 path
305 )
306 if self.__checkReply(reply, 2):
307 filename = reply.arguments()[1]
308 if filename:
309 snapshot = QPixmap(filename)
310 with contextlib.suppress(OSError):
311 os.remove(filename)
312
313 self.grabbed.emit(snapshot)
314
315 def __checkReply(self, reply, argumentsCount):
316 """
317 Private method to check, if a reply is valid.
318
319 @param reply reference to the reply message
320 @type QDBusMessage
321 @param argumentsCount number of expected arguments
322 @type int
323 @return flag indicating validity
324 @rtype bool
325 """
326 if reply.type() == QDBusMessage.MessageType.ReplyMessage:
327 if len(reply.arguments()) == argumentsCount:
328 return True
329
330 EricMessageBox.warning(
331 None,
332 self.tr("Screenshot Error"),
333 self.tr("<p>Received an unexpected number of reply arguments."
334 " Expected {0} but got {1}</p>").format(
335 argumentsCount,
336 len(reply.arguments()),
337 ))
338
339 elif reply.type() == QDBusMessage.MessageType.ErrorMessage:
340 EricMessageBox.warning(
341 None,
342 self.tr("Screenshot Error"),
343 self.tr("<p>Received error <b>{0}</b> from DBus while"
344 " performing screenshot.</p><p>{1}</p>").format(
345 reply.errorName(),
346 reply.errorMessage(),
347 ))
348
349 elif reply.type() == QDBusMessage.MessageType.InvalidMessage:
350 EricMessageBox.warning(
351 None,
352 self.tr("Screenshot Error"),
353 self.tr("Received an invalid reply."))
354
355 else:
356 EricMessageBox.warning(
357 None,
358 self.tr("Screenshot Error"),
359 self.tr("Received an unexpected reply."))
360
361 return False
362
363 def __temporaryFilename(self):
364 """
365 Private method to generate a temporary filename.
366
367 @return path name for a unique, temporary file
368 @rtype str
369 """
370 return "/tmp/eric-snap-{0}.png".format(uuid.uuid4().hex) # secok

eric ide

mercurial