Changeset View
Changeset View
Standalone View
Standalone View
src/BackendPlugins/XcbKWinScreenshotPlugin.cpp
- This file was added.
1 | /* This file is part of Spectacle, the KDE screenshot utility | ||||
---|---|---|---|---|---|
2 | * Copyright (C) 2019 Leon De Andrade <leondeandrade@hotmail.com> | ||||
3 | * | ||||
4 | * This program is free software; you can redistribute it and/or modify | ||||
5 | * it under the terms of the GNU Lesser General Public License as published by | ||||
6 | * the Free Software Foundation; either version 2 of the License, or | ||||
7 | * (at your option) any later version. | ||||
8 | * | ||||
9 | * This program is distributed in the hope that it will be useful, | ||||
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
12 | * GNU General Public License for more details. | ||||
13 | * | ||||
14 | * You should have received a copy of the GNU Lesser General Public License | ||||
15 | * along with this program; if not, write to the Free Software | ||||
16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, | ||||
17 | * Boston, MA 02110-1301, USA. | ||||
18 | * | ||||
19 | * SPDX-License-Identifier: LGPL-2.0-or-later | ||||
20 | */ | ||||
21 | | ||||
22 | | ||||
23 | #include "XcbKWinScreenshotPlugin.h" | ||||
24 | | ||||
25 | #include <xcb/xcb.h> | ||||
26 | #include <xcb/xcb_util.h> | ||||
27 | #include <xcb/xcb_cursor.h> | ||||
28 | #include <xcb/xcb_image.h> | ||||
29 | | ||||
30 | #include <X11/Xdefs.h> | ||||
31 | #include <X11/Xatom.h> | ||||
32 | | ||||
33 | #include <KWindowSystem> | ||||
34 | | ||||
35 | #include <QApplication> | ||||
36 | #include <QTimer> | ||||
37 | #include <QX11Info> | ||||
38 | #include <QRect> | ||||
39 | #include <QPixmap> | ||||
40 | #include <QDBusInterface> | ||||
41 | #include <QDBusConnection> | ||||
42 | #include <QDBusConnectionInterface> | ||||
43 | | ||||
44 | #include <memory> | ||||
45 | | ||||
46 | struct XcbImagePtrDeleter { | ||||
47 | void operator()(xcb_image_t *theXcbImage) const | ||||
48 | { | ||||
49 | if (theXcbImage) { | ||||
50 | xcb_image_destroy(theXcbImage); | ||||
51 | } | ||||
52 | } | ||||
53 | }; | ||||
54 | using XcbImagePtr = std::unique_ptr<xcb_image_t, XcbImagePtrDeleter>; | ||||
55 | | ||||
56 | XcbKWinScreenshotPlugin::OnClickEventFilter::OnClickEventFilter(XcbKWinScreenshotPlugin *platformPtr) | ||||
57 | : mPlatformPtr(platformPtr) {} | ||||
58 | | ||||
59 | void XcbKWinScreenshotPlugin::OnClickEventFilter::setCaptureOptions(const GrabMode &grabMode, bool includePointer, bool includeDecorations) | ||||
60 | { | ||||
61 | mGrabMode = grabMode; | ||||
62 | mIncludePointer = includePointer; | ||||
63 | mIncludeDecorations = includeDecorations; | ||||
64 | } | ||||
65 | | ||||
66 | bool XcbKWinScreenshotPlugin::OnClickEventFilter::nativeEventFilter(const QByteArray &eventType, void *message, long *) | ||||
67 | { | ||||
68 | if (eventType == "xcb_generic_event_t") { | ||||
69 | auto lFirstEvent = static_cast<xcb_generic_event_t *>(message); | ||||
70 | | ||||
71 | switch (lFirstEvent->response_type & ~0x80) { | ||||
72 | case XCB_BUTTON_RELEASE: { | ||||
73 | // uninstall the eventfilter and release the mouse | ||||
74 | qApp->removeNativeEventFilter(this); | ||||
75 | xcb_ungrab_pointer(QX11Info::connection(), XCB_TIME_CURRENT_TIME); | ||||
76 | | ||||
77 | // decide whether to grab or abort. regrab the mouse on mouse-wheel events | ||||
78 | { | ||||
79 | auto lSecondEvent = static_cast<xcb_button_release_event_t *>(message); | ||||
80 | if (lSecondEvent->detail == 1) { | ||||
81 | QTimer::singleShot(0, [this]() { | ||||
82 | mPlatformPtr->doGrabNow(mGrabMode, mIncludePointer, mIncludeDecorations); | ||||
83 | }); | ||||
84 | } else if (lSecondEvent->detail < 4) { | ||||
85 | emit mPlatformPtr->newScreenshotFailed(); | ||||
86 | } else { | ||||
87 | QTimer::singleShot(0, [this]() { | ||||
88 | mPlatformPtr->doGrabOnClick(mGrabMode, mIncludePointer, mIncludeDecorations); | ||||
89 | }); | ||||
90 | } | ||||
91 | } | ||||
92 | return true; | ||||
93 | } | ||||
94 | default: | ||||
95 | return false; | ||||
96 | } | ||||
97 | } | ||||
98 | return false; | ||||
99 | } | ||||
100 | | ||||
101 | XcbKWinScreenshotPlugin::XcbKWinScreenshotPlugin(QObject *parent) | ||||
102 | : ScreenshotInterface(parent), | ||||
103 | mNativeEventFilter(new OnClickEventFilter(this)) {} | ||||
104 | | ||||
105 | XcbKWinScreenshotPlugin::~XcbKWinScreenshotPlugin() | ||||
106 | { | ||||
107 | delete mNativeEventFilter; | ||||
108 | } | ||||
109 | | ||||
110 | GrabModes XcbKWinScreenshotPlugin::supportedGrabModes() const | ||||
111 | { | ||||
112 | GrabModes supportedModes({ GrabMode::AllScreens, GrabMode::ActiveWindow, GrabMode::WindowUnderCursor }); | ||||
113 | //TO DO: This should really be not determined by the platform | ||||
114 | if (QApplication::screens().count() > 1) { | ||||
115 | supportedModes |= GrabMode::CurrentScreen; | ||||
116 | } | ||||
117 | return supportedModes; | ||||
118 | } | ||||
119 | | ||||
120 | ShutterModes XcbKWinScreenshotPlugin::supportedShutterModes() const | ||||
121 | { | ||||
122 | return { ShutterMode::Immediate | ShutterMode::OnClick }; | ||||
123 | } | ||||
124 | | ||||
125 | QString XcbKWinScreenshotPlugin::name() const | ||||
126 | { | ||||
127 | return QStringLiteral("Xcb KWin Screenshot Backend"); | ||||
128 | } | ||||
129 | | ||||
130 | Platform XcbKWinScreenshotPlugin::platform() const | ||||
131 | { | ||||
132 | return Platform::Xcb; | ||||
133 | } | ||||
134 | | ||||
135 | // TO DO: Real condition | ||||
136 | bool XcbKWinScreenshotPlugin::requirementsComplied() const | ||||
137 | { | ||||
138 | if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.KWin"))) { | ||||
139 | QDBusInterface lIface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Effects"), QStringLiteral("org.kde.kwin.Effects")); | ||||
140 | QDBusReply<bool> lReply = lIface.call(QStringLiteral("isEffectLoaded"), QStringLiteral("screenshot")); | ||||
141 | return lReply.value(); | ||||
142 | } | ||||
143 | return false; | ||||
144 | } | ||||
145 | | ||||
146 | void XcbKWinScreenshotPlugin::doGrab(ShutterMode shutterMode, GrabMode grabMode, bool includePointer, bool includeDecorations) | ||||
147 | { | ||||
148 | switch (shutterMode) { | ||||
149 | case ShutterMode::Immediate: | ||||
150 | return doGrabNow(grabMode, includePointer, includeDecorations); | ||||
151 | case ShutterMode::OnClick: | ||||
152 | return doGrabOnClick(grabMode, includePointer, includeDecorations); | ||||
153 | } | ||||
154 | } | ||||
155 | | ||||
156 | void XcbKWinScreenshotPlugin::doGrabNow(const GrabMode &grabMode, bool includePointer, bool includeDecorations) | ||||
157 | { | ||||
158 | switch (grabMode) { | ||||
159 | case GrabMode::AllScreens: | ||||
160 | grabAllScreens(includePointer); | ||||
161 | break; | ||||
162 | case GrabMode::CurrentScreen: | ||||
163 | grabCurrentScreen(includePointer); | ||||
164 | break; | ||||
165 | case GrabMode::ActiveWindow: | ||||
166 | grabActiveWindow(includePointer, includeDecorations); | ||||
167 | break; | ||||
168 | case GrabMode::WindowUnderCursor: | ||||
169 | grabWindowUnderCursor(includePointer, includeDecorations); | ||||
170 | break; | ||||
171 | case GrabMode::TransientWithParent: | ||||
172 | case GrabMode::InvalidChoice: | ||||
173 | emit newScreenshotFailed(); | ||||
174 | } | ||||
175 | } | ||||
176 | | ||||
177 | void XcbKWinScreenshotPlugin::doGrabOnClick(const GrabMode &grabMode, bool includePointer, bool includeDecorations) | ||||
178 | { | ||||
179 | // get the cursor image | ||||
180 | xcb_cursor_t lXcbCursor = XCB_CURSOR_NONE; | ||||
181 | xcb_cursor_context_t *lXcbCursorCtx = nullptr; | ||||
182 | xcb_screen_t *lXcbAppScreen = xcb_aux_get_screen(QX11Info::connection(), QX11Info::appScreen()); | ||||
183 | | ||||
184 | if (xcb_cursor_context_new(QX11Info::connection(), lXcbAppScreen, &lXcbCursorCtx) >= 0) { | ||||
185 | QVector<QByteArray> lCursorNames = { | ||||
186 | QByteArrayLiteral("cross"), | ||||
187 | QByteArrayLiteral("crosshair"), | ||||
188 | QByteArrayLiteral("diamond-cross"), | ||||
189 | QByteArrayLiteral("cross-reverse") | ||||
190 | }; | ||||
191 | | ||||
192 | for (const auto &lCursorName : lCursorNames) { | ||||
193 | xcb_cursor_t lCursor = xcb_cursor_load_cursor(lXcbCursorCtx, lCursorName.constData()); | ||||
194 | if (lCursor != XCB_CURSOR_NONE) { | ||||
195 | lXcbCursor = lCursor; | ||||
196 | break; | ||||
197 | } | ||||
198 | } | ||||
199 | } | ||||
200 | | ||||
201 | // grab the cursor | ||||
202 | xcb_grab_pointer_cookie_t grabPointerCookie = xcb_grab_pointer_unchecked( | ||||
203 | QX11Info::connection(), // xcb connection | ||||
204 | 0, // deliver events to owner? nope | ||||
205 | QX11Info::appRootWindow(), // window to grab pointer for (root) | ||||
206 | XCB_EVENT_MASK_BUTTON_RELEASE, // which events do I want | ||||
207 | XCB_GRAB_MODE_SYNC, // pointer grab mode | ||||
208 | XCB_GRAB_MODE_ASYNC, // keyboard grab mode (why is this even here) | ||||
209 | XCB_NONE, // confine pointer to which window (none) | ||||
210 | lXcbCursor, // cursor to change to for the duration of grab | ||||
211 | XCB_TIME_CURRENT_TIME // do this right now | ||||
212 | ); | ||||
213 | std::unique_ptr<xcb_grab_pointer_reply_t> lGrabPointerReply(xcb_grab_pointer_reply(QX11Info::connection(), grabPointerCookie, nullptr)); | ||||
214 | | ||||
215 | // if the grab failed, take the screenshot right away | ||||
216 | if (lGrabPointerReply->status != XCB_GRAB_STATUS_SUCCESS) { | ||||
217 | doGrabNow(grabMode, includePointer, includeDecorations); | ||||
218 | return; | ||||
219 | } | ||||
220 | | ||||
221 | // fix things if our pointer grab causes a lockup and install our event filter | ||||
222 | mNativeEventFilter->setCaptureOptions(grabMode, includePointer, includeDecorations); | ||||
223 | xcb_allow_events(QX11Info::connection(), XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); | ||||
224 | qApp->installNativeEventFilter(mNativeEventFilter); | ||||
225 | | ||||
226 | // done. clean stuff up | ||||
227 | xcb_cursor_context_free(lXcbCursorCtx); | ||||
228 | xcb_free_cursor(QX11Info::connection(), lXcbCursor); | ||||
229 | } | ||||
230 | | ||||
231 | void XcbKWinScreenshotPlugin::handleKWinScreenshotReply(quint64 drawable) | ||||
232 | { | ||||
233 | // obtain width and height and grab an image (x and y are always zero for pixmaps) | ||||
234 | auto lDrawable = static_cast<xcb_drawable_t>(drawable); | ||||
235 | auto rect = getDrawableGeometry(lDrawable); | ||||
236 | auto pixmap = getPixmapFromDrawable(lDrawable, rect); | ||||
237 | | ||||
238 | if (!pixmap.isNull()) { | ||||
239 | emit newScreenshotTaken(pixmap); | ||||
240 | return; | ||||
241 | } | ||||
242 | emit newScreenshotFailed(); | ||||
243 | } | ||||
244 | | ||||
245 | QRect XcbKWinScreenshotPlugin::getDrawableGeometry(xcb_drawable_t drawable) | ||||
246 | { | ||||
247 | auto xcbConn = QX11Info::connection(); | ||||
248 | auto geoCookie = xcb_get_geometry_unchecked(xcbConn, drawable); | ||||
249 | std::unique_ptr<xcb_get_geometry_reply_t> geoReply(xcb_get_geometry_reply(xcbConn, geoCookie, nullptr)); | ||||
250 | if (!geoReply) { | ||||
251 | return QRect(); | ||||
252 | } | ||||
253 | return QRect(geoReply->x, geoReply->y, geoReply->width, geoReply->height); | ||||
254 | } | ||||
255 | | ||||
256 | QPixmap XcbKWinScreenshotPlugin::getPixmapFromDrawable(xcb_drawable_t xcbDrawable, const QRect &rect) | ||||
257 | { | ||||
258 | auto xcbConn = QX11Info::connection(); | ||||
259 | | ||||
260 | // proceed to get an image based on the geometry (in device pixels) | ||||
261 | XcbImagePtr xcbImage( | ||||
262 | xcb_image_get( | ||||
263 | xcbConn, | ||||
264 | xcbDrawable, | ||||
265 | rect.x(), | ||||
266 | rect.y(), | ||||
267 | rect.width(), | ||||
268 | rect.height(), | ||||
269 | ~0, | ||||
270 | XCB_IMAGE_FORMAT_Z_PIXMAP | ||||
271 | ) | ||||
272 | ); | ||||
273 | | ||||
274 | // too bad, the capture failed. | ||||
275 | if (!xcbImage) { | ||||
276 | return QPixmap(); | ||||
277 | } | ||||
278 | | ||||
279 | // now process the image | ||||
280 | return convertFromNative(xcbImage.get()); | ||||
281 | } | ||||
282 | | ||||
283 | QPixmap XcbKWinScreenshotPlugin::convertFromNative(xcb_image_t *xcbImage) | ||||
284 | { | ||||
285 | auto imageFormat = QImage::Format_Invalid; | ||||
286 | switch (xcbImage->depth) { | ||||
287 | case 1: | ||||
288 | imageFormat = QImage::Format_MonoLSB; | ||||
289 | break; | ||||
290 | case 16: | ||||
291 | imageFormat = QImage::Format_RGB16; | ||||
292 | break; | ||||
293 | case 24: | ||||
294 | imageFormat = QImage::Format_RGB32; | ||||
295 | break; | ||||
296 | case 30: | ||||
297 | imageFormat = QImage::Format_BGR30; | ||||
298 | break; | ||||
299 | case 32: | ||||
300 | imageFormat = QImage::Format_ARGB32_Premultiplied; | ||||
301 | break; | ||||
302 | default: | ||||
303 | return QPixmap(); // we don't know | ||||
304 | } | ||||
305 | | ||||
306 | // the RGB32 format requires data format 0xffRRGGBB, ensure that this fourth byte really is 0xff | ||||
307 | if (imageFormat == QImage::Format_RGB32) { | ||||
308 | auto data = reinterpret_cast<quint32 *>(xcbImage->data); | ||||
309 | for (size_t iIter = 0; iIter < xcbImage->width * xcbImage->height; iIter++) { | ||||
310 | data[iIter] |= 0xff000000; | ||||
311 | } | ||||
312 | } | ||||
313 | | ||||
314 | QImage image(xcbImage->data, xcbImage->width, xcbImage->height, imageFormat); | ||||
315 | if (image.isNull()) { | ||||
316 | return QPixmap(); | ||||
317 | } | ||||
318 | | ||||
319 | // work around an abort in QImage::color | ||||
320 | if (image.format() == QImage::Format_MonoLSB) { | ||||
321 | image.setColorCount(2); | ||||
322 | image.setColor(0, QColor(Qt::white).rgb()); | ||||
323 | image.setColor(1, QColor(Qt::black).rgb()); | ||||
324 | } | ||||
325 | | ||||
326 | // the image is ready. Since the backing data from xcbImage could be freed | ||||
327 | // before the QPixmap goes away, a deep copy is necessary. | ||||
328 | return QPixmap::fromImage(image).copy(); | ||||
329 | } | ||||
330 | | ||||
331 | | ||||
332 | void XcbKWinScreenshotPlugin::updateWindowTitle(xcb_window_t window) | ||||
333 | { | ||||
334 | auto title = KWindowSystem::readNameProperty(window, XA_WM_NAME); | ||||
335 | emit windowTitleChanged(title); | ||||
336 | } | ||||
337 | | ||||
338 | void XcbKWinScreenshotPlugin::grabAllScreens(bool includePointer) | ||||
339 | { | ||||
340 | callDBus(QStringLiteral("screenshotFullscreen"), { includePointer }); | ||||
341 | } | ||||
342 | | ||||
343 | void XcbKWinScreenshotPlugin::grabCurrentScreen(bool includePointer) | ||||
344 | { | ||||
345 | QScreen *currentScreen = QGuiApplication::screenAt(QCursor::pos()); | ||||
346 | QList<QScreen *> screenList = QGuiApplication::screens(); | ||||
347 | | ||||
348 | if (!currentScreen) { | ||||
349 | //TO DO: Error handling | ||||
350 | } | ||||
351 | | ||||
352 | callDBus(QStringLiteral("screenshotScreen"), { screenList.indexOf(currentScreen), includePointer }); | ||||
353 | } | ||||
354 | | ||||
355 | void XcbKWinScreenshotPlugin::grabActiveWindow(bool includePointer, bool includeDecorations) | ||||
356 | { | ||||
357 | //TO DO: Why is this not handled by KWin directly | ||||
358 | auto activeWindow = KWindowSystem::activeWindow(); | ||||
359 | updateWindowTitle(activeWindow); | ||||
360 | | ||||
361 | int optionMask = includeDecorations ? 1 : 0; | ||||
362 | if (includePointer) { | ||||
363 | optionMask |= 1 << 1; | ||||
364 | } | ||||
365 | | ||||
366 | callDBus(QStringLiteral("screenshotForWindow"), { static_cast<quint64>(activeWindow), optionMask }); | ||||
367 | } | ||||
368 | | ||||
369 | void XcbKWinScreenshotPlugin::grabWindowUnderCursor(bool includePointer, bool includeDecorations) | ||||
370 | { | ||||
371 | int optionMask = includeDecorations ? 1 : 0; | ||||
372 | if (includePointer) { | ||||
373 | optionMask |= 1 << 1; | ||||
374 | } | ||||
375 | | ||||
376 | callDBus(QStringLiteral("screenshotWindowUnderCursor"), { optionMask }); | ||||
377 | } | ||||
378 | | ||||
379 | void XcbKWinScreenshotPlugin::callDBus(const QString &method, const QList<QVariant> &args) { | ||||
380 | QDBusConnection::sessionBus().connect(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot"), QStringLiteral("screenshotCreated"), this, SLOT(handleKWinScreenshotReply(quint64))); | ||||
381 | QDBusInterface interface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot")); | ||||
382 | interface.callWithArgumentList(QDBus::NoBlock, method, args); | ||||
383 | } | ||||
384 | | ||||
385 | #include "moc_XcbKWinScreenshotPlugin.cpp" | ||||
386 | No newline at end of file |