File indexing completed on 2025-02-02 04:26:13
0001 /* 0002 SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "ImagePlatformKWin.h" 0008 #include "ExportManager.h" 0009 0010 #include <KWindowSystem> 0011 0012 #include <QDBusConnection> 0013 #include <QDBusConnectionInterface> 0014 #include <QDBusPendingCall> 0015 #include <QDBusPendingCallWatcher> 0016 #include <QDBusReply> 0017 #include <QDBusUnixFileDescriptor> 0018 #include <QFile> 0019 #include <QFileDevice> 0020 #include <QFuture> 0021 #include <QFutureWatcher> 0022 #include <QGuiApplication> 0023 #include <QPixmap> 0024 #include <QScreen> 0025 #include <QTimer> 0026 #include <QtConcurrentRun> 0027 0028 #include <errno.h> 0029 #include <fcntl.h> 0030 #include <string.h> 0031 #include <unistd.h> 0032 0033 using namespace Qt::StringLiterals; 0034 0035 static QVariantMap screenShotFlagsToVardict(ImagePlatformKWin::ScreenShotFlags flags) 0036 { 0037 QVariantMap options; 0038 0039 if (flags & ImagePlatformKWin::ScreenShotFlag::IncludeCursor) { 0040 options.insert(u"include-cursor"_s, true); 0041 } 0042 if (flags & ImagePlatformKWin::ScreenShotFlag::IncludeDecoration) { 0043 options.insert(u"include-decoration"_s, true); 0044 } 0045 0046 bool includeShadow = flags & ImagePlatformKWin::ScreenShotFlag::IncludeShadow; 0047 options.insert(u"include-shadow"_s, includeShadow); 0048 0049 if (flags & ImagePlatformKWin::ScreenShotFlag::NativeSize) { 0050 options.insert(u"native-resolution"_s, true); 0051 } 0052 0053 return options; 0054 } 0055 0056 static const QString s_screenShotService = u"org.kde.KWin.ScreenShot2"_s; 0057 static const QString s_screenShotObjectPath = u"/org/kde/KWin/ScreenShot2"_s; 0058 static const QString s_screenShotInterface = u"org.kde.KWin.ScreenShot2"_s; 0059 0060 template<typename... ArgType> 0061 ScreenShotSource2::ScreenShotSource2(const QString &methodName, ArgType... arguments) 0062 { 0063 // Do not set the O_NONBLOCK flag. Code that reads data from the pipe assumes 0064 // that read() will block if there is no any data yet. 0065 int pipeFds[2]; 0066 if (pipe2(pipeFds, O_CLOEXEC) == -1) { 0067 QTimer::singleShot(0, this, &ScreenShotSource2::errorOccurred); 0068 qWarning() << "pipe2() failed:" << strerror(errno); 0069 return; 0070 } 0071 0072 QDBusMessage message = QDBusMessage::createMethodCall(s_screenShotService, s_screenShotObjectPath, s_screenShotInterface, methodName); 0073 0074 QVariantList dbusArguments{arguments...}; 0075 dbusArguments.append(QVariant::fromValue(QDBusUnixFileDescriptor(pipeFds[1]))); 0076 message.setArguments(dbusArguments); 0077 0078 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); 0079 close(pipeFds[1]); 0080 m_pipeFileDescriptor = pipeFds[0]; 0081 0082 auto watcher = new QDBusPendingCallWatcher(pendingCall, this); 0083 connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher]() { 0084 watcher->deleteLater(); 0085 const QDBusPendingReply<QVariantMap> reply = *watcher; 0086 0087 if (reply.isError()) { 0088 qWarning() << "Screenshot request failed:" << reply.error().message(); 0089 if (reply.error().name() == u"org.kde.KWin.ScreenShot2.Error.Cancelled"_s) { 0090 // don't show error on user cancellation 0091 Q_EMIT finished(m_result); 0092 } else { 0093 Q_EMIT errorOccurred(); 0094 } 0095 } else { 0096 handleMetaDataReceived(reply); 0097 } 0098 }); 0099 } 0100 0101 ScreenShotSource2::~ScreenShotSource2() 0102 { 0103 if (m_pipeFileDescriptor != -1) { 0104 close(m_pipeFileDescriptor); 0105 } 0106 } 0107 0108 QImage ScreenShotSource2::result() const 0109 { 0110 return m_result; 0111 } 0112 0113 static QImage allocateImage(const QVariantMap &metadata) 0114 { 0115 bool ok; 0116 0117 const uint width = metadata.value(u"width"_s).toUInt(&ok); 0118 if (!ok) { 0119 return QImage(); 0120 } 0121 0122 const uint height = metadata.value(u"height"_s).toUInt(&ok); 0123 if (!ok) { 0124 return QImage(); 0125 } 0126 0127 const uint format = metadata.value(u"format"_s).toUInt(&ok); 0128 if (!ok) { 0129 return QImage(); 0130 } 0131 0132 return QImage(width, height, QImage::Format(format)); 0133 } 0134 0135 static QImage readImage(int fileDescriptor, const QVariantMap &metadata) 0136 { 0137 QFile file; 0138 if (!file.open(fileDescriptor, QFileDevice::ReadOnly, QFileDevice::AutoCloseHandle)) { 0139 close(fileDescriptor); 0140 return QImage(); 0141 } 0142 0143 QImage result = allocateImage(metadata); 0144 if (result.isNull()) { 0145 return QImage(); 0146 } 0147 0148 const auto windowId = metadata.value(u"windowId"_s).toString(); 0149 // No point in storing the windowId in the image since it means nothing to users 0150 // and can't be used if the window is closed. 0151 if (!windowId.isEmpty()) { 0152 QDBusMessage message = QDBusMessage::createMethodCall(u"org.kde.KWin"_s, 0153 u"/KWin"_s, 0154 u"org.kde.KWin"_s, 0155 u"getWindowInfo"_s); 0156 message.setArguments({windowId}); 0157 const QDBusReply<QVariantMap> reply = QDBusConnection::sessionBus().call(message); 0158 if (reply.isValid()) { 0159 const auto &windowTitle = reply.value().value(u"caption"_s).toString(); 0160 if (!windowTitle.isEmpty()) { 0161 result.setText(u"windowTitle"_s, windowTitle); 0162 ExportManager::instance()->setWindowTitle(windowTitle); 0163 } 0164 } 0165 } 0166 0167 bool ok = false; 0168 qreal scale = metadata.value(u"scale"_s).toReal(&ok); 0169 if (ok) { 0170 // NOTE: KWin X11Output DPR is always 1. This is intentional. 0171 // https://bugs.kde.org/show_bug.cgi?id=474778 0172 if (KWindowSystem::isPlatformX11() && scale == 1) { 0173 scale = qGuiApp->devicePixelRatio(); 0174 } 0175 result.setDevicePixelRatio(scale); 0176 } 0177 0178 const auto screen = metadata.value(u"screen"_s).toString(); 0179 if (!screen.isEmpty()) { 0180 result.setText(u"screen"_s, screen); 0181 } 0182 0183 QDataStream stream(&file); 0184 stream.readRawData(reinterpret_cast<char *>(result.bits()), result.sizeInBytes()); 0185 0186 return result; 0187 } 0188 0189 void ScreenShotSource2::handleMetaDataReceived(const QVariantMap &metadata) 0190 { 0191 const QString type = metadata.value(u"type"_s).toString(); 0192 if (type != "raw"_L1) { 0193 qWarning() << "Unsupported metadata type:" << type; 0194 return; 0195 } 0196 0197 auto watcher = new QFutureWatcher<QImage>(this); 0198 connect(watcher, &QFutureWatcher<QImage>::finished, this, [this, watcher]() { 0199 watcher->deleteLater(); 0200 m_result = watcher->result(); 0201 if (m_result.isNull()) { 0202 Q_EMIT errorOccurred(); 0203 } else { 0204 Q_EMIT finished(m_result); 0205 } 0206 }); 0207 watcher->setFuture(QtConcurrent::run(readImage, m_pipeFileDescriptor, metadata)); 0208 0209 // The ownership of the pipe file descriptor has been moved to the worker thread. 0210 m_pipeFileDescriptor = -1; 0211 } 0212 0213 ScreenShotSourceArea2::ScreenShotSourceArea2(const QRect &area, ImagePlatformKWin::ScreenShotFlags flags) 0214 : ScreenShotSource2(u"CaptureArea"_s, 0215 qint32(area.x()), 0216 qint32(area.y()), 0217 quint32(area.width()), 0218 quint32(area.height()), 0219 screenShotFlagsToVardict(flags)) 0220 { 0221 } 0222 0223 ScreenShotSourceInteractive2::ScreenShotSourceInteractive2(ImagePlatformKWin::InteractiveKind kind, ImagePlatformKWin::ScreenShotFlags flags) 0224 : ScreenShotSource2(u"CaptureInteractive"_s, quint32(kind), screenShotFlagsToVardict(flags)) 0225 { 0226 } 0227 0228 ScreenShotSourceScreen2::ScreenShotSourceScreen2(const QScreen *screen, ImagePlatformKWin::ScreenShotFlags flags) 0229 // NOTE: As of Qt 6.4, QScreen::name() is not guaranteed to match the result of any native APIs. 0230 // It should not be used to uniquely identify a screen, but it happens to work on X11 and Wayland. 0231 // KWin's ScreenShot2 DBus API uses QScreen::name() as identifiers for screens. 0232 : ScreenShotSource2(u"CaptureScreen"_s, screen->name(), screenShotFlagsToVardict(flags)) 0233 { 0234 } 0235 0236 ScreenShotSourceActiveWindow2::ScreenShotSourceActiveWindow2(ImagePlatformKWin::ScreenShotFlags flags) 0237 : ScreenShotSource2(u"CaptureActiveWindow"_s, screenShotFlagsToVardict(flags)) 0238 { 0239 } 0240 0241 ScreenShotSourceActiveScreen2::ScreenShotSourceActiveScreen2(ImagePlatformKWin::ScreenShotFlags flags) 0242 : ScreenShotSource2(u"CaptureActiveScreen"_s, screenShotFlagsToVardict(flags)) 0243 { 0244 } 0245 0246 ScreenShotSourceWorkspace2::ScreenShotSourceWorkspace2(ImagePlatformKWin::ScreenShotFlags flags) 0247 : ScreenShotSource2(u"CaptureWorkspace"_s, screenShotFlagsToVardict(flags)) 0248 { 0249 } 0250 0251 ImagePlatformKWin::ImagePlatformKWin(QObject *parent) 0252 : ImagePlatform(parent) 0253 { 0254 auto message = QDBusMessage::createMethodCall(u"org.kde.KWin.ScreenShot2"_s, 0255 u"/org/kde/KWin/ScreenShot2"_s, 0256 u"org.freedesktop.DBus.Properties"_s, 0257 u"Get"_s); 0258 message.setArguments({u"org.kde.KWin.ScreenShot2"_s, u"Version"_s}); 0259 0260 const QDBusMessage reply = QDBusConnection::sessionBus().call(message); 0261 if (reply.type() == QDBusMessage::ReplyMessage) { 0262 m_apiVersion = reply.arguments().constFirst().value<QDBusVariant>().variant().toUInt(); 0263 } 0264 0265 updateSupportedGrabModes(); 0266 connect(qGuiApp, &QGuiApplication::screenAdded, this, &ImagePlatformKWin::updateSupportedGrabModes); 0267 connect(qGuiApp, &QGuiApplication::screenRemoved, this, &ImagePlatformKWin::updateSupportedGrabModes); 0268 } 0269 0270 ImagePlatform::GrabModes ImagePlatformKWin::supportedGrabModes() const 0271 { 0272 return m_grabModes; 0273 } 0274 0275 void ImagePlatformKWin::updateSupportedGrabModes() 0276 { 0277 ImagePlatform::GrabModes grabModes = GrabMode::AllScreens | GrabMode::WindowUnderCursor | GrabMode::PerScreenImageNative; 0278 0279 if (m_apiVersion >= 2) { 0280 grabModes |= GrabMode::ActiveWindow; 0281 } 0282 0283 if (QGuiApplication::screens().count() > 1) { 0284 grabModes |= GrabMode::CurrentScreen | GrabMode::AllScreensScaled; 0285 } 0286 0287 if (m_grabModes != grabModes) { 0288 m_grabModes = grabModes; 0289 Q_EMIT supportedGrabModesChanged(); 0290 } 0291 } 0292 0293 ImagePlatform::ShutterModes ImagePlatformKWin::supportedShutterModes() const 0294 { 0295 return ShutterMode::Immediate; 0296 } 0297 0298 void ImagePlatformKWin::doGrab(ShutterMode, GrabMode grabMode, bool includePointer, bool includeDecorations, bool includeShadow) 0299 { 0300 ScreenShotFlags flags = ScreenShotFlag::NativeSize; 0301 0302 flags.setFlag(ScreenShotFlag::IncludeShadow, includeShadow); 0303 0304 if (includeDecorations) { 0305 flags |= ScreenShotFlag::IncludeDecoration; 0306 } 0307 if (includePointer) { 0308 flags |= ScreenShotFlag::IncludeCursor; 0309 } 0310 0311 switch (grabMode) { 0312 case GrabMode::AllScreens: 0313 takeScreenShotWorkspace(flags); 0314 break; 0315 case GrabMode::CurrentScreen: 0316 if (m_apiVersion >= 2) { 0317 takeScreenShotActiveScreen(flags); 0318 } else { 0319 takeScreenShotInteractive(InteractiveKind::Screen, flags); 0320 } 0321 break; 0322 case GrabMode::ActiveWindow: 0323 takeScreenShotActiveWindow(flags); 0324 break; 0325 case GrabMode::TransientWithParent: 0326 case GrabMode::WindowUnderCursor: 0327 takeScreenShotInteractive(InteractiveKind::Window, flags); 0328 break; 0329 case GrabMode::AllScreensScaled: 0330 takeScreenShotWorkspace(flags & ~ScreenShotFlags(ScreenShotFlag::NativeSize)); 0331 break; 0332 case GrabMode::PerScreenImageNative: 0333 takeScreenShotCroppable(flags); 0334 break; 0335 case GrabMode::NoGrabModes: 0336 Q_EMIT newScreenshotFailed(); 0337 break; 0338 } 0339 } 0340 0341 void ImagePlatformKWin::trackSource(ScreenShotSource2 *source) 0342 { 0343 connect(source, &ScreenShotSource2::finished, this, [this, source](const QImage &image) { 0344 source->deleteLater(); 0345 Q_EMIT newScreenshotTaken(image); 0346 }); 0347 connect(source, &ScreenShotSource2::errorOccurred, this, [this, source]() { 0348 source->deleteLater(); 0349 Q_EMIT newScreenshotFailed(); 0350 }); 0351 } 0352 0353 void ImagePlatformKWin::trackCroppableSource(ScreenShotSourceWorkspace2 *source) 0354 { 0355 connect(source, &ScreenShotSourceWorkspace2::finished, this, [this, source](const QImage &image) { 0356 source->deleteLater(); 0357 Q_EMIT newCroppableScreenshotTaken(image); 0358 }); 0359 connect(source, &ScreenShotSourceWorkspace2::errorOccurred, this, [this, source]() { 0360 source->deleteLater(); 0361 Q_EMIT newScreenshotFailed(); 0362 }); 0363 } 0364 0365 void ImagePlatformKWin::takeScreenShotArea(const QRect &area, ScreenShotFlags flags) 0366 { 0367 trackSource(new ScreenShotSourceArea2(area, flags)); 0368 } 0369 0370 void ImagePlatformKWin::takeScreenShotInteractive(InteractiveKind kind, ScreenShotFlags flags) 0371 { 0372 trackSource(new ScreenShotSourceInteractive2(kind, flags)); 0373 } 0374 0375 void ImagePlatformKWin::takeScreenShotActiveWindow(ScreenShotFlags flags) 0376 { 0377 trackSource(new ScreenShotSourceActiveWindow2(flags)); 0378 } 0379 0380 void ImagePlatformKWin::takeScreenShotActiveScreen(ScreenShotFlags flags) 0381 { 0382 trackSource(new ScreenShotSourceActiveScreen2(flags)); 0383 } 0384 0385 void ImagePlatformKWin::takeScreenShotWorkspace(ScreenShotFlags flags) 0386 { 0387 trackSource(new ScreenShotSourceWorkspace2(flags)); 0388 } 0389 0390 void ImagePlatformKWin::takeScreenShotCroppable(ScreenShotFlags flags) 0391 { 0392 trackCroppableSource(new ScreenShotSourceWorkspace2(flags)); 0393 } 0394 0395 #include "moc_ImagePlatformKWin.cpp"