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 CaptureMode &captureMode, bool includePointer, bool includeDecorations) | ||||
60 | { | ||||
61 | mCaptureMode = captureMode; | ||||
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(mCaptureMode, mIncludePointer, mIncludeDecorations); | ||||
83 | }); | ||||
84 | } else if (lSecondEvent->detail < 4) { | ||||
85 | emit mPlatformPtr->newScreenshotFailed(); | ||||
86 | } else { | ||||
87 | QTimer::singleShot(0, [this]() { | ||||
88 | mPlatformPtr->doGrabOnClick(mCaptureMode, 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 | CaptureModes XcbKWinScreenshotPlugin::supportedCaptureModes() const | ||||
111 | { | ||||
112 | return CaptureModes({CaptureMode::ActiveWindow, CaptureMode::CurrentScreen, CaptureMode::WindowUnderCursor }); | ||||
113 | } | ||||
114 | | ||||
115 | ShutterModes XcbKWinScreenshotPlugin::supportedShutterModes() const | ||||
116 | { | ||||
117 | return { ShutterMode::Immediate | ShutterMode::OnClick }; | ||||
118 | } | ||||
119 | | ||||
120 | QString XcbKWinScreenshotPlugin::name() const | ||||
121 | { | ||||
122 | return QStringLiteral("Xcb KWin Screenshot Backend"); | ||||
123 | } | ||||
124 | | ||||
125 | Platform XcbKWinScreenshotPlugin::platform() const | ||||
126 | { | ||||
127 | return Platform::Xcb; | ||||
128 | } | ||||
129 | | ||||
130 | // TO DO: Real condition | ||||
131 | bool XcbKWinScreenshotPlugin::requirementsComplied() const | ||||
132 | { | ||||
133 | if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.KWin"))) { | ||||
134 | QDBusInterface lIface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Effects"), QStringLiteral("org.kde.kwin.Effects")); | ||||
135 | QDBusReply<bool> lReply = lIface.call(QStringLiteral("isEffectLoaded"), QStringLiteral("screenshot")); | ||||
136 | return lReply.value(); | ||||
137 | } | ||||
138 | return false; | ||||
139 | } | ||||
140 | | ||||
141 | void XcbKWinScreenshotPlugin::doGrab(ShutterMode shutterMode, CaptureMode captureMode, bool includePointer, bool includeDecorations) | ||||
142 | { | ||||
143 | switch (shutterMode) { | ||||
144 | case ShutterMode::Immediate: | ||||
145 | return doGrabNow(captureMode, includePointer, includeDecorations); | ||||
146 | case ShutterMode::OnClick: | ||||
147 | return doGrabOnClick(captureMode, includePointer, includeDecorations); | ||||
148 | } | ||||
149 | } | ||||
150 | | ||||
151 | void XcbKWinScreenshotPlugin::doGrabNow(const CaptureMode &captureMode, bool includePointer, bool includeDecorations) | ||||
152 | { | ||||
153 | switch (captureMode) { | ||||
154 | case CaptureMode::CurrentScreen: | ||||
155 | grabCurrentScreen(includePointer); | ||||
156 | break; | ||||
157 | case CaptureMode::ActiveWindow: | ||||
158 | grabActiveWindow(includePointer, includeDecorations); | ||||
159 | break; | ||||
160 | case CaptureMode::WindowUnderCursor: | ||||
161 | grabWindowUnderCursor(includePointer, includeDecorations); | ||||
162 | break; | ||||
163 | default: | ||||
164 | emit newScreenshotFailed(); | ||||
165 | } | ||||
166 | } | ||||
167 | | ||||
168 | void XcbKWinScreenshotPlugin::doGrabOnClick(const CaptureMode &captureMode, bool includePointer, bool includeDecorations) | ||||
169 | { | ||||
170 | // get the cursor image | ||||
171 | xcb_cursor_t lXcbCursor = XCB_CURSOR_NONE; | ||||
172 | xcb_cursor_context_t *lXcbCursorCtx = nullptr; | ||||
173 | xcb_screen_t *lXcbAppScreen = xcb_aux_get_screen(QX11Info::connection(), QX11Info::appScreen()); | ||||
174 | | ||||
175 | if (xcb_cursor_context_new(QX11Info::connection(), lXcbAppScreen, &lXcbCursorCtx) >= 0) { | ||||
176 | QVector<QByteArray> lCursorNames = { | ||||
177 | QByteArrayLiteral("cross"), | ||||
178 | QByteArrayLiteral("crosshair"), | ||||
179 | QByteArrayLiteral("diamond-cross"), | ||||
180 | QByteArrayLiteral("cross-reverse") | ||||
181 | }; | ||||
182 | | ||||
183 | for (const auto &lCursorName : lCursorNames) { | ||||
184 | xcb_cursor_t lCursor = xcb_cursor_load_cursor(lXcbCursorCtx, lCursorName.constData()); | ||||
185 | if (lCursor != XCB_CURSOR_NONE) { | ||||
186 | lXcbCursor = lCursor; | ||||
187 | break; | ||||
188 | } | ||||
189 | } | ||||
190 | } | ||||
191 | | ||||
192 | // grab the cursor | ||||
193 | xcb_grab_pointer_cookie_t grabPointerCookie = xcb_grab_pointer_unchecked( | ||||
194 | QX11Info::connection(), // xcb connection | ||||
195 | 0, // deliver events to owner? nope | ||||
196 | QX11Info::appRootWindow(), // window to grab pointer for (root) | ||||
197 | XCB_EVENT_MASK_BUTTON_RELEASE, // which events do I want | ||||
198 | XCB_GRAB_MODE_SYNC, // pointer grab mode | ||||
199 | XCB_GRAB_MODE_ASYNC, // keyboard grab mode (why is this even here) | ||||
200 | XCB_NONE, // confine pointer to which window (none) | ||||
201 | lXcbCursor, // cursor to change to for the duration of grab | ||||
202 | XCB_TIME_CURRENT_TIME // do this right now | ||||
203 | ); | ||||
204 | std::unique_ptr<xcb_grab_pointer_reply_t> lGrabPointerReply(xcb_grab_pointer_reply(QX11Info::connection(), grabPointerCookie, nullptr)); | ||||
205 | | ||||
206 | // if the grab failed, take the screenshot right away | ||||
207 | if (lGrabPointerReply->status != XCB_GRAB_STATUS_SUCCESS) { | ||||
208 | doGrabNow(captureMode, includePointer, includeDecorations); | ||||
209 | return; | ||||
210 | } | ||||
211 | | ||||
212 | // fix things if our pointer grab causes a lockup and install our event filter | ||||
213 | mNativeEventFilter->setCaptureOptions(captureMode, includePointer, includeDecorations); | ||||
214 | xcb_allow_events(QX11Info::connection(), XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); | ||||
215 | qApp->installNativeEventFilter(mNativeEventFilter); | ||||
216 | | ||||
217 | // done. clean stuff up | ||||
218 | xcb_cursor_context_free(lXcbCursorCtx); | ||||
219 | xcb_free_cursor(QX11Info::connection(), lXcbCursor); | ||||
220 | } | ||||
221 | | ||||
222 | void XcbKWinScreenshotPlugin::handleKWinScreenshotReply(quint64 drawable) | ||||
223 | { | ||||
224 | // obtain width and height and grab an image (x and y are always zero for pixmaps) | ||||
225 | auto lDrawable = static_cast<xcb_drawable_t>(drawable); | ||||
226 | auto rect = getDrawableGeometry(lDrawable); | ||||
227 | auto pixmap = getPixmapFromDrawable(lDrawable, rect); | ||||
228 | | ||||
229 | if (!pixmap.isNull()) { | ||||
230 | emit newScreenshotTaken(pixmap); | ||||
231 | return; | ||||
232 | } | ||||
233 | emit newScreenshotFailed(); | ||||
234 | } | ||||
235 | | ||||
236 | QRect XcbKWinScreenshotPlugin::getDrawableGeometry(xcb_drawable_t drawable) | ||||
237 | { | ||||
238 | auto xcbConn = QX11Info::connection(); | ||||
239 | auto geoCookie = xcb_get_geometry_unchecked(xcbConn, drawable); | ||||
240 | std::unique_ptr<xcb_get_geometry_reply_t> geoReply(xcb_get_geometry_reply(xcbConn, geoCookie, nullptr)); | ||||
241 | if (!geoReply) { | ||||
242 | return QRect(); | ||||
243 | } | ||||
244 | return QRect(geoReply->x, geoReply->y, geoReply->width, geoReply->height); | ||||
245 | } | ||||
246 | | ||||
247 | QPixmap XcbKWinScreenshotPlugin::getPixmapFromDrawable(xcb_drawable_t xcbDrawable, const QRect &rect) | ||||
248 | { | ||||
249 | auto xcbConn = QX11Info::connection(); | ||||
250 | | ||||
251 | // proceed to get an image based on the geometry (in device pixels) | ||||
252 | XcbImagePtr xcbImage( | ||||
253 | xcb_image_get( | ||||
254 | xcbConn, | ||||
255 | xcbDrawable, | ||||
256 | rect.x(), | ||||
257 | rect.y(), | ||||
258 | rect.width(), | ||||
259 | rect.height(), | ||||
260 | ~0, | ||||
261 | XCB_IMAGE_FORMAT_Z_PIXMAP | ||||
262 | ) | ||||
263 | ); | ||||
264 | | ||||
265 | // too bad, the capture failed. | ||||
266 | if (!xcbImage) { | ||||
267 | return QPixmap(); | ||||
268 | } | ||||
269 | | ||||
270 | // now process the image | ||||
271 | return convertFromNative(xcbImage.get()); | ||||
272 | } | ||||
273 | | ||||
274 | QPixmap XcbKWinScreenshotPlugin::convertFromNative(xcb_image_t *xcbImage) | ||||
275 | { | ||||
276 | auto imageFormat = QImage::Format_Invalid; | ||||
277 | switch (xcbImage->depth) { | ||||
278 | case 1: | ||||
279 | imageFormat = QImage::Format_MonoLSB; | ||||
280 | break; | ||||
281 | case 16: | ||||
282 | imageFormat = QImage::Format_RGB16; | ||||
283 | break; | ||||
284 | case 24: | ||||
285 | imageFormat = QImage::Format_RGB32; | ||||
286 | break; | ||||
287 | case 30: | ||||
288 | imageFormat = QImage::Format_BGR30; | ||||
289 | break; | ||||
290 | case 32: | ||||
291 | imageFormat = QImage::Format_ARGB32_Premultiplied; | ||||
292 | break; | ||||
293 | default: | ||||
294 | return QPixmap(); // we don't know | ||||
295 | } | ||||
296 | | ||||
297 | // the RGB32 format requires data format 0xffRRGGBB, ensure that this fourth byte really is 0xff | ||||
298 | if (imageFormat == QImage::Format_RGB32) { | ||||
299 | auto data = reinterpret_cast<quint32 *>(xcbImage->data); | ||||
300 | for (size_t iIter = 0; iIter < xcbImage->width * xcbImage->height; iIter++) { | ||||
301 | data[iIter] |= 0xff000000; | ||||
302 | } | ||||
303 | } | ||||
304 | | ||||
305 | QImage image(xcbImage->data, xcbImage->width, xcbImage->height, imageFormat); | ||||
306 | if (image.isNull()) { | ||||
307 | return QPixmap(); | ||||
308 | } | ||||
309 | | ||||
310 | // work around an abort in QImage::color | ||||
311 | if (image.format() == QImage::Format_MonoLSB) { | ||||
312 | image.setColorCount(2); | ||||
313 | image.setColor(0, QColor(Qt::white).rgb()); | ||||
314 | image.setColor(1, QColor(Qt::black).rgb()); | ||||
315 | } | ||||
316 | | ||||
317 | // the image is ready. Since the backing data from xcbImage could be freed | ||||
318 | // before the QPixmap goes away, a deep copy is necessary. | ||||
319 | return QPixmap::fromImage(image).copy(); | ||||
320 | } | ||||
321 | | ||||
322 | | ||||
323 | void XcbKWinScreenshotPlugin::updateWindowTitle(xcb_window_t window) | ||||
324 | { | ||||
325 | auto title = KWindowSystem::readNameProperty(window, XA_WM_NAME); | ||||
326 | emit windowTitleChanged(title); | ||||
327 | } | ||||
328 | | ||||
329 | void XcbKWinScreenshotPlugin::grabAllScreens(bool includePointer) | ||||
330 | { | ||||
331 | callDBus(QStringLiteral("screenshotFullscreen"), { includePointer }); | ||||
332 | } | ||||
333 | | ||||
334 | void XcbKWinScreenshotPlugin::grabCurrentScreen(bool includePointer) | ||||
335 | { | ||||
336 | QScreen *currentScreen = QGuiApplication::screenAt(QCursor::pos()); | ||||
337 | QList<QScreen *> screenList = QGuiApplication::screens(); | ||||
338 | | ||||
339 | if (!currentScreen) { | ||||
340 | //TO DO: Error handling | ||||
341 | } | ||||
342 | | ||||
343 | callDBus(QStringLiteral("screenshotScreen"), { screenList.indexOf(currentScreen), includePointer }); | ||||
344 | } | ||||
345 | | ||||
346 | void XcbKWinScreenshotPlugin::grabActiveWindow(bool includePointer, bool includeDecorations) | ||||
347 | { | ||||
348 | //TO DO: Why is this not handled by KWin directly | ||||
349 | auto activeWindow = KWindowSystem::activeWindow(); | ||||
350 | updateWindowTitle(activeWindow); | ||||
351 | | ||||
352 | int optionMask = includeDecorations ? 1 : 0; | ||||
353 | if (includePointer) { | ||||
354 | optionMask |= 1 << 1; | ||||
355 | } | ||||
356 | | ||||
357 | callDBus(QStringLiteral("screenshotForWindow"), { static_cast<quint64>(activeWindow), optionMask }); | ||||
358 | } | ||||
359 | | ||||
360 | void XcbKWinScreenshotPlugin::grabWindowUnderCursor(bool includePointer, bool includeDecorations) | ||||
361 | { | ||||
362 | int optionMask = includeDecorations ? 1 : 0; | ||||
363 | if (includePointer) { | ||||
364 | optionMask |= 1 << 1; | ||||
365 | } | ||||
366 | | ||||
367 | callDBus(QStringLiteral("screenshotWindowUnderCursor"), { optionMask }); | ||||
368 | } | ||||
369 | | ||||
370 | void XcbKWinScreenshotPlugin::callDBus(const QString &method, const QList<QVariant> &args) { | ||||
371 | QDBusConnection::sessionBus().connect(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot"), QStringLiteral("screenshotCreated"), this, SLOT(handleKWinScreenshotReply(quint64))); | ||||
372 | QDBusInterface interface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot")); | ||||
373 | interface.callWithArgumentList(QDBus::NoBlock, method, args); | ||||
374 | } | ||||
375 | | ||||
376 | #include "moc_XcbKWinScreenshotPlugin.cpp" | ||||
377 | No newline at end of file |