1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qgenericunixservices_p.h" |
5 | #include <QtGui/private/qtguiglobal_p.h> |
6 | #include "qguiapplication.h" |
7 | #include "qwindow.h" |
8 | #include <QtGui/qpa/qplatformwindow_p.h> |
9 | #include <QtGui/qpa/qplatformwindow.h> |
10 | #include <QtGui/qpa/qplatformnativeinterface.h> |
11 | |
12 | #include <QtCore/QDebug> |
13 | #include <QtCore/QFile> |
14 | #if QT_CONFIG(process) |
15 | # include <QtCore/QProcess> |
16 | #endif |
17 | #if QT_CONFIG(settings) |
18 | #include <QtCore/QSettings> |
19 | #endif |
20 | #include <QtCore/QStandardPaths> |
21 | #include <QtCore/QUrl> |
22 | |
23 | #if QT_CONFIG(dbus) |
24 | // These QtCore includes are needed for xdg-desktop-portal support |
25 | #include <QtCore/private/qcore_unix_p.h> |
26 | |
27 | #include <QtCore/QFileInfo> |
28 | #include <QtCore/QUrlQuery> |
29 | |
30 | #include <QtDBus/QDBusConnection> |
31 | #include <QtDBus/QDBusMessage> |
32 | #include <QtDBus/QDBusPendingCall> |
33 | #include <QtDBus/QDBusPendingCallWatcher> |
34 | #include <QtDBus/QDBusPendingReply> |
35 | #include <QtDBus/QDBusUnixFileDescriptor> |
36 | |
37 | #include <fcntl.h> |
38 | |
39 | #endif // QT_CONFIG(dbus) |
40 | |
41 | #include <stdlib.h> |
42 | |
43 | QT_BEGIN_NAMESPACE |
44 | |
45 | using namespace Qt::StringLiterals; |
46 | |
47 | #if QT_CONFIG(multiprocess) |
48 | |
49 | enum { debug = 0 }; |
50 | |
51 | static inline QByteArray detectDesktopEnvironment() |
52 | { |
53 | const QByteArray xdgCurrentDesktop = qgetenv(varName: "XDG_CURRENT_DESKTOP" ); |
54 | if (!xdgCurrentDesktop.isEmpty()) |
55 | return xdgCurrentDesktop.toUpper(); // KDE, GNOME, UNITY, LXDE, MATE, XFCE... |
56 | |
57 | // Classic fallbacks |
58 | if (!qEnvironmentVariableIsEmpty(varName: "KDE_FULL_SESSION" )) |
59 | return QByteArrayLiteral("KDE" ); |
60 | if (!qEnvironmentVariableIsEmpty(varName: "GNOME_DESKTOP_SESSION_ID" )) |
61 | return QByteArrayLiteral("GNOME" ); |
62 | |
63 | // Fallback to checking $DESKTOP_SESSION (unreliable) |
64 | QByteArray desktopSession = qgetenv(varName: "DESKTOP_SESSION" ); |
65 | |
66 | // This can be a path in /usr/share/xsessions |
67 | int slash = desktopSession.lastIndexOf(ch: '/'); |
68 | if (slash != -1) { |
69 | #if QT_CONFIG(settings) |
70 | QSettings desktopFile(QFile::decodeName(localFileName: desktopSession + ".desktop" ), QSettings::IniFormat); |
71 | desktopFile.beginGroup(QStringLiteral("Desktop Entry" )); |
72 | QByteArray desktopName = desktopFile.value(QStringLiteral("DesktopNames" )).toByteArray(); |
73 | if (!desktopName.isEmpty()) |
74 | return desktopName; |
75 | #endif |
76 | |
77 | // try decoding just the basename |
78 | desktopSession = desktopSession.mid(index: slash + 1); |
79 | } |
80 | |
81 | if (desktopSession == "gnome" ) |
82 | return QByteArrayLiteral("GNOME" ); |
83 | else if (desktopSession == "xfce" ) |
84 | return QByteArrayLiteral("XFCE" ); |
85 | else if (desktopSession == "kde" ) |
86 | return QByteArrayLiteral("KDE" ); |
87 | |
88 | return QByteArrayLiteral("UNKNOWN" ); |
89 | } |
90 | |
91 | static inline bool checkExecutable(const QString &candidate, QString *result) |
92 | { |
93 | *result = QStandardPaths::findExecutable(executableName: candidate); |
94 | return !result->isEmpty(); |
95 | } |
96 | |
97 | static inline bool detectWebBrowser(const QByteArray &desktop, |
98 | bool checkBrowserVariable, |
99 | QString *browser) |
100 | { |
101 | const char *browsers[] = {"google-chrome" , "firefox" , "mozilla" , "opera" }; |
102 | |
103 | browser->clear(); |
104 | if (checkExecutable(QStringLiteral("xdg-open" ), result: browser)) |
105 | return true; |
106 | |
107 | if (checkBrowserVariable) { |
108 | QByteArray browserVariable = qgetenv(varName: "DEFAULT_BROWSER" ); |
109 | if (browserVariable.isEmpty()) |
110 | browserVariable = qgetenv(varName: "BROWSER" ); |
111 | if (!browserVariable.isEmpty() && checkExecutable(candidate: QString::fromLocal8Bit(ba: browserVariable), result: browser)) |
112 | return true; |
113 | } |
114 | |
115 | if (desktop == QByteArray("KDE" )) { |
116 | if (checkExecutable(QStringLiteral("kde-open5" ), result: browser)) |
117 | return true; |
118 | // Konqueror launcher |
119 | if (checkExecutable(QStringLiteral("kfmclient" ), result: browser)) { |
120 | browser->append(s: " exec"_L1 ); |
121 | return true; |
122 | } |
123 | } else if (desktop == QByteArray("GNOME" )) { |
124 | if (checkExecutable(QStringLiteral("gnome-open" ), result: browser)) |
125 | return true; |
126 | } |
127 | |
128 | for (size_t i = 0; i < sizeof(browsers)/sizeof(char *); ++i) |
129 | if (checkExecutable(candidate: QLatin1StringView(browsers[i]), result: browser)) |
130 | return true; |
131 | return false; |
132 | } |
133 | |
134 | static inline bool launch(const QString &launcher, const QUrl &url, |
135 | const QString &xdgActivationToken) |
136 | { |
137 | |
138 | const QString command = launcher + u' ' + QLatin1StringView(url.toEncoded()); |
139 | if (debug) |
140 | qDebug(msg: "Launching %s" , qPrintable(command)); |
141 | #if !QT_CONFIG(process) |
142 | if (!xdgActivationToken.isEmpty()) |
143 | qputenv("XDG_ACTIVATION_TOKEN" , xdgActivationToken.toUtf8()); |
144 | const bool ok = ::system(qPrintable(command + " &"_L1 )); |
145 | if (!xdgActivationToken.isEmpty()) |
146 | qunsetenv("XDG_ACTIVATION_TOKEN" ); |
147 | # else |
148 | QStringList args = QProcess::splitCommand(command); |
149 | bool ok = false; |
150 | if (!args.isEmpty()) { |
151 | QString program = args.takeFirst(); |
152 | QProcess process; |
153 | process.setProgram(program); |
154 | process.setArguments(args); |
155 | |
156 | if (!xdgActivationToken.isEmpty()) { |
157 | auto env = QProcessEnvironment::systemEnvironment(); |
158 | env.insert(name: u"XDG_ACTIVATION_TOKEN"_s , value: xdgActivationToken); |
159 | process.setEnvironment(env.toStringList()); |
160 | } |
161 | ok = process.startDetached(pid: nullptr); |
162 | } |
163 | # endif |
164 | if (!ok) |
165 | qWarning(msg: "Launch failed (%s)" , qPrintable(command)); |
166 | |
167 | |
168 | return ok; |
169 | } |
170 | |
171 | #if QT_CONFIG(dbus) |
172 | static inline bool checkNeedPortalSupport() |
173 | { |
174 | return QFileInfo::exists(file: "/.flatpak-info"_L1 ) || qEnvironmentVariableIsSet(varName: "SNAP" ); |
175 | } |
176 | |
177 | static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url, const QString &parentWindow, |
178 | const QString &xdgActivationToken) |
179 | { |
180 | // DBus signature: |
181 | // OpenFile (IN s parent_window, |
182 | // IN h fd, |
183 | // IN a{sv} options, |
184 | // OUT o handle) |
185 | // Options: |
186 | // handle_token (s) - A string that will be used as the last element of the @handle. |
187 | // writable (b) - Whether to allow the chosen application to write to the file. |
188 | |
189 | const int fd = qt_safe_open(pathname: QFile::encodeName(fileName: url.toLocalFile()), O_RDONLY); |
190 | if (fd != -1) { |
191 | QDBusMessage message = QDBusMessage::createMethodCall(destination: "org.freedesktop.portal.Desktop"_L1 , |
192 | path: "/org/freedesktop/portal/desktop"_L1 , |
193 | interface: "org.freedesktop.portal.OpenURI"_L1 , |
194 | method: "OpenFile"_L1 ); |
195 | |
196 | QDBusUnixFileDescriptor descriptor; |
197 | descriptor.giveFileDescriptor(fileDescriptor: fd); |
198 | |
199 | QVariantMap options = {}; |
200 | |
201 | if (!xdgActivationToken.isEmpty()) { |
202 | options.insert(key: "activation_token"_L1 , value: xdgActivationToken); |
203 | } |
204 | |
205 | message << parentWindow << QVariant::fromValue(value: descriptor) << options; |
206 | |
207 | return QDBusConnection::sessionBus().call(message); |
208 | } |
209 | |
210 | return QDBusMessage::createError(type: QDBusError::InternalError, msg: qt_error_string()); |
211 | } |
212 | |
213 | static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url, const QString &parentWindow, |
214 | const QString &xdgActivationToken) |
215 | { |
216 | // DBus signature: |
217 | // OpenURI (IN s parent_window, |
218 | // IN s uri, |
219 | // IN a{sv} options, |
220 | // OUT o handle) |
221 | // Options: |
222 | // handle_token (s) - A string that will be used as the last element of the @handle. |
223 | // writable (b) - Whether to allow the chosen application to write to the file. |
224 | // This key only takes effect the uri points to a local file that is exported in the document portal, |
225 | // and the chosen application is sandboxed itself. |
226 | |
227 | QDBusMessage message = QDBusMessage::createMethodCall(destination: "org.freedesktop.portal.Desktop"_L1 , |
228 | path: "/org/freedesktop/portal/desktop"_L1 , |
229 | interface: "org.freedesktop.portal.OpenURI"_L1 , |
230 | method: "OpenURI"_L1 ); |
231 | // FIXME parent_window_id and handle writable option |
232 | QVariantMap options; |
233 | |
234 | if (!xdgActivationToken.isEmpty()) { |
235 | options.insert(key: "activation_token"_L1 , value: xdgActivationToken); |
236 | } |
237 | |
238 | message << parentWindow << url.toString() << options; |
239 | |
240 | return QDBusConnection::sessionBus().call(message); |
241 | } |
242 | |
243 | static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url, const QString &parentWindow, |
244 | const QString &xdgActivationToken) |
245 | { |
246 | // DBus signature: |
247 | // ComposeEmail (IN s parent_window, |
248 | // IN a{sv} options, |
249 | // OUT o handle) |
250 | // Options: |
251 | // address (s) - The email address to send to. |
252 | // subject (s) - The subject for the email. |
253 | // body (s) - The body for the email. |
254 | // attachment_fds (ah) - File descriptors for files to attach. |
255 | |
256 | QUrlQuery urlQuery(url); |
257 | QVariantMap options; |
258 | options.insert(key: "address"_L1 , value: url.path()); |
259 | options.insert(key: "subject"_L1 , value: urlQuery.queryItemValue(key: "subject"_L1 )); |
260 | options.insert(key: "body"_L1 , value: urlQuery.queryItemValue(key: "body"_L1 )); |
261 | |
262 | // O_PATH seems to be present since Linux 2.6.39, which is not case of RHEL 6 |
263 | #ifdef O_PATH |
264 | QList<QDBusUnixFileDescriptor> attachments; |
265 | const QStringList attachmentUris = urlQuery.allQueryItemValues(key: "attachment"_L1 ); |
266 | |
267 | for (const QString &attachmentUri : attachmentUris) { |
268 | const int fd = qt_safe_open(pathname: QFile::encodeName(fileName: attachmentUri), O_PATH); |
269 | if (fd != -1) { |
270 | QDBusUnixFileDescriptor descriptor(fd); |
271 | attachments << descriptor; |
272 | qt_safe_close(fd); |
273 | } |
274 | } |
275 | |
276 | options.insert(key: "attachment_fds"_L1 , value: QVariant::fromValue(value: attachments)); |
277 | #endif |
278 | |
279 | if (!xdgActivationToken.isEmpty()) { |
280 | options.insert(key: "activation_token"_L1 , value: xdgActivationToken); |
281 | } |
282 | |
283 | QDBusMessage message = QDBusMessage::createMethodCall(destination: "org.freedesktop.portal.Desktop"_L1 , |
284 | path: "/org/freedesktop/portal/desktop"_L1 , |
285 | interface: "org.freedesktop.portal.Email"_L1 , |
286 | method: "ComposeEmail"_L1 ); |
287 | |
288 | message << parentWindow << options; |
289 | |
290 | return QDBusConnection::sessionBus().call(message); |
291 | } |
292 | |
293 | namespace { |
294 | struct XDGDesktopColor |
295 | { |
296 | double r = 0; |
297 | double g = 0; |
298 | double b = 0; |
299 | |
300 | QColor toQColor() const |
301 | { |
302 | constexpr auto rgbMax = 255; |
303 | return { static_cast<int>(r * rgbMax), static_cast<int>(g * rgbMax), |
304 | static_cast<int>(b * rgbMax) }; |
305 | } |
306 | }; |
307 | |
308 | const QDBusArgument &operator>>(const QDBusArgument &argument, XDGDesktopColor &myStruct) |
309 | { |
310 | argument.beginStructure(); |
311 | argument >> myStruct.r >> myStruct.g >> myStruct.b; |
312 | argument.endStructure(); |
313 | return argument; |
314 | } |
315 | |
316 | class XdgDesktopPortalColorPicker : public QPlatformServiceColorPicker |
317 | { |
318 | Q_OBJECT |
319 | public: |
320 | XdgDesktopPortalColorPicker(const QString &parentWindowId, QWindow *parent) |
321 | : QPlatformServiceColorPicker(parent), m_parentWindowId(parentWindowId) |
322 | { |
323 | } |
324 | |
325 | void pickColor() override |
326 | { |
327 | // DBus signature: |
328 | // PickColor (IN s parent_window, |
329 | // IN a{sv} options |
330 | // OUT o handle) |
331 | // Options: |
332 | // handle_token (s) - A string that will be used as the last element of the @handle. |
333 | |
334 | QDBusMessage message = QDBusMessage::createMethodCall( |
335 | destination: "org.freedesktop.portal.Desktop"_L1 , path: "/org/freedesktop/portal/desktop"_L1 , |
336 | interface: "org.freedesktop.portal.Screenshot"_L1 , method: "PickColor"_L1 ); |
337 | message << m_parentWindowId << QVariantMap(); |
338 | |
339 | QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); |
340 | auto watcher = new QDBusPendingCallWatcher(pendingCall, this); |
341 | connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this, |
342 | slot: [this](QDBusPendingCallWatcher *watcher) { |
343 | watcher->deleteLater(); |
344 | QDBusPendingReply<QDBusObjectPath> reply = *watcher; |
345 | if (reply.isError()) { |
346 | qWarning(msg: "DBus call to pick color failed: %s" , |
347 | qPrintable(reply.error().message())); |
348 | Q_EMIT colorPicked(color: {}); |
349 | } else { |
350 | QDBusConnection::sessionBus().connect( |
351 | service: "org.freedesktop.portal.Desktop"_L1 , path: reply.value().path(), |
352 | interface: "org.freedesktop.portal.Request"_L1 , name: "Response"_L1 , receiver: this, |
353 | // clang-format off |
354 | SLOT(gotColorResponse(uint,QVariantMap)) |
355 | // clang-format on |
356 | ); |
357 | } |
358 | }); |
359 | } |
360 | |
361 | private Q_SLOTS: |
362 | void gotColorResponse(uint result, const QVariantMap &map) |
363 | { |
364 | if (result != 0) |
365 | return; |
366 | if (map.contains(key: u"color"_s )) { |
367 | XDGDesktopColor color{}; |
368 | map.value(key: u"color"_s ).value<QDBusArgument>() >> color; |
369 | Q_EMIT colorPicked(color: color.toQColor()); |
370 | } else { |
371 | Q_EMIT colorPicked(color: {}); |
372 | } |
373 | deleteLater(); |
374 | } |
375 | |
376 | private: |
377 | const QString m_parentWindowId; |
378 | }; |
379 | } // namespace |
380 | |
381 | #endif // QT_CONFIG(dbus) |
382 | |
383 | QGenericUnixServices::QGenericUnixServices() |
384 | { |
385 | if (detectDesktopEnvironment() == QByteArrayLiteral("UNKNOWN" )) |
386 | return; |
387 | |
388 | #if QT_CONFIG(dbus) |
389 | if (qEnvironmentVariableIntValue(varName: "QT_NO_XDG_DESKTOP_PORTAL" ) > 0) { |
390 | return; |
391 | } |
392 | QDBusMessage message = QDBusMessage::createMethodCall( |
393 | destination: "org.freedesktop.portal.Desktop"_L1 , path: "/org/freedesktop/portal/desktop"_L1 , |
394 | interface: "org.freedesktop.DBus.Properties"_L1 , method: "Get"_L1 ); |
395 | message << "org.freedesktop.portal.Screenshot"_L1 |
396 | << "version"_L1 ; |
397 | |
398 | QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); |
399 | auto watcher = new QDBusPendingCallWatcher(pendingCall); |
400 | m_watcherConnection = |
401 | QObject::connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: watcher, |
402 | slot: [this](QDBusPendingCallWatcher *watcher) { |
403 | watcher->deleteLater(); |
404 | QDBusPendingReply<QVariant> reply = *watcher; |
405 | if (!reply.isError() && reply.value().toUInt() >= 2) |
406 | m_hasScreenshotPortalWithColorPicking = true; |
407 | }); |
408 | |
409 | #endif |
410 | } |
411 | |
412 | QGenericUnixServices::~QGenericUnixServices() |
413 | { |
414 | #if QT_CONFIG(dbus) |
415 | QObject::disconnect(m_watcherConnection); |
416 | #endif |
417 | } |
418 | |
419 | QPlatformServiceColorPicker *QGenericUnixServices::colorPicker(QWindow *parent) |
420 | { |
421 | #if QT_CONFIG(dbus) |
422 | // Make double sure that we are in a wayland environment. In particular check |
423 | // WAYLAND_DISPLAY so also XWayland apps benefit from portal-based color picking. |
424 | // Outside wayland we'll rather rely on other means than the XDG desktop portal. |
425 | if (!qEnvironmentVariableIsEmpty(varName: "WAYLAND_DISPLAY" ) |
426 | || QGuiApplication::platformName().startsWith(s: "wayland"_L1 )) { |
427 | return new XdgDesktopPortalColorPicker(portalWindowIdentifier(window: parent), parent); |
428 | } |
429 | return nullptr; |
430 | #else |
431 | Q_UNUSED(parent); |
432 | return nullptr; |
433 | #endif |
434 | } |
435 | |
436 | QByteArray QGenericUnixServices::desktopEnvironment() const |
437 | { |
438 | static const QByteArray result = detectDesktopEnvironment(); |
439 | return result; |
440 | } |
441 | |
442 | template<typename F> |
443 | void runWithXdgActivationToken(F &&functionToCall) |
444 | { |
445 | #if QT_CONFIG(wayland) |
446 | QWindow *window = qGuiApp->focusWindow(); |
447 | |
448 | if (!window) { |
449 | functionToCall({}); |
450 | return; |
451 | } |
452 | |
453 | auto waylandApp = dynamic_cast<QNativeInterface::QWaylandApplication *>( |
454 | qGuiApp->platformNativeInterface()); |
455 | auto waylandWindow = |
456 | dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(window->handle()); |
457 | |
458 | if (!waylandWindow || !waylandApp) { |
459 | functionToCall({}); |
460 | return; |
461 | } |
462 | |
463 | QObject::connect(waylandWindow, |
464 | &QNativeInterface::Private::QWaylandWindow::xdgActivationTokenCreated, |
465 | waylandWindow, functionToCall, Qt::SingleShotConnection); |
466 | waylandWindow->requestXdgActivationToken(serial: waylandApp->lastInputSerial()); |
467 | #else |
468 | functionToCall({}); |
469 | #endif |
470 | } |
471 | |
472 | bool QGenericUnixServices::openUrl(const QUrl &url) |
473 | { |
474 | auto openUrlInternal = [this](const QUrl &url, const QString &xdgActivationToken) { |
475 | if (url.scheme() == "mailto"_L1 ) { |
476 | # if QT_CONFIG(dbus) |
477 | if (checkNeedPortalSupport()) { |
478 | const QString parentWindow = QGuiApplication::focusWindow() |
479 | ? portalWindowIdentifier(window: QGuiApplication::focusWindow()) |
480 | : QString(); |
481 | QDBusError error = xdgDesktopPortalSendEmail(url, parentWindow, xdgActivationToken); |
482 | if (!error.isValid()) |
483 | return true; |
484 | |
485 | // service not running, fall back |
486 | } |
487 | # endif |
488 | return openDocument(url); |
489 | } |
490 | |
491 | # if QT_CONFIG(dbus) |
492 | if (checkNeedPortalSupport()) { |
493 | const QString parentWindow = QGuiApplication::focusWindow() |
494 | ? portalWindowIdentifier(window: QGuiApplication::focusWindow()) |
495 | : QString(); |
496 | QDBusError error = xdgDesktopPortalOpenUrl(url, parentWindow, xdgActivationToken); |
497 | if (!error.isValid()) |
498 | return true; |
499 | } |
500 | # endif |
501 | |
502 | if (m_webBrowser.isEmpty() |
503 | && !detectWebBrowser(desktop: desktopEnvironment(), checkBrowserVariable: true, browser: &m_webBrowser)) { |
504 | qWarning(msg: "Unable to detect a web browser to launch '%s'" , qPrintable(url.toString())); |
505 | return false; |
506 | } |
507 | return launch(launcher: m_webBrowser, url, xdgActivationToken); |
508 | }; |
509 | |
510 | if (QGuiApplication::platformName().startsWith(s: "wayland"_L1 )) { |
511 | runWithXdgActivationToken( |
512 | functionToCall: [openUrlInternal, url](const QString &token) { openUrlInternal(url, token); }); |
513 | |
514 | return true; |
515 | |
516 | } else { |
517 | return openUrlInternal(url, QString()); |
518 | } |
519 | } |
520 | |
521 | bool QGenericUnixServices::openDocument(const QUrl &url) |
522 | { |
523 | auto openDocumentInternal = [this](const QUrl &url, const QString &xdgActivationToken) { |
524 | |
525 | # if QT_CONFIG(dbus) |
526 | if (checkNeedPortalSupport()) { |
527 | const QString parentWindow = QGuiApplication::focusWindow() |
528 | ? portalWindowIdentifier(window: QGuiApplication::focusWindow()) |
529 | : QString(); |
530 | QDBusError error = xdgDesktopPortalOpenFile(url, parentWindow, xdgActivationToken); |
531 | if (!error.isValid()) |
532 | return true; |
533 | } |
534 | # endif |
535 | |
536 | if (m_documentLauncher.isEmpty() |
537 | && !detectWebBrowser(desktop: desktopEnvironment(), checkBrowserVariable: false, browser: &m_documentLauncher)) { |
538 | qWarning(msg: "Unable to detect a launcher for '%s'" , qPrintable(url.toString())); |
539 | return false; |
540 | } |
541 | return launch(launcher: m_documentLauncher, url, xdgActivationToken); |
542 | }; |
543 | |
544 | if (QGuiApplication::platformName().startsWith(s: "wayland"_L1 )) { |
545 | runWithXdgActivationToken(functionToCall: [openDocumentInternal, url](const QString &token) { |
546 | openDocumentInternal(url, token); |
547 | }); |
548 | |
549 | return true; |
550 | } else { |
551 | return openDocumentInternal(url, QString()); |
552 | } |
553 | } |
554 | |
555 | #else |
556 | QGenericUnixServices::QGenericUnixServices() = default; |
557 | QGenericUnixServices::~QGenericUnixServices() = default; |
558 | |
559 | QByteArray QGenericUnixServices::desktopEnvironment() const |
560 | { |
561 | return QByteArrayLiteral("UNKNOWN" ); |
562 | } |
563 | |
564 | bool QGenericUnixServices::openUrl(const QUrl &url) |
565 | { |
566 | Q_UNUSED(url); |
567 | qWarning("openUrl() not supported on this platform" ); |
568 | return false; |
569 | } |
570 | |
571 | bool QGenericUnixServices::openDocument(const QUrl &url) |
572 | { |
573 | Q_UNUSED(url); |
574 | qWarning("openDocument() not supported on this platform" ); |
575 | return false; |
576 | } |
577 | |
578 | QPlatformServiceColorPicker *QGenericUnixServices::colorPicker(QWindow *parent) |
579 | { |
580 | Q_UNUSED(parent); |
581 | return nullptr; |
582 | } |
583 | |
584 | #endif // QT_NO_MULTIPROCESS |
585 | |
586 | QString QGenericUnixServices::portalWindowIdentifier(QWindow *window) |
587 | { |
588 | Q_UNUSED(window); |
589 | return QString(); |
590 | } |
591 | |
592 | bool QGenericUnixServices::hasCapability(Capability capability) const |
593 | { |
594 | switch (capability) { |
595 | case Capability::ColorPicking: |
596 | return m_hasScreenshotPortalWithColorPicking; |
597 | } |
598 | return false; |
599 | } |
600 | |
601 | void QGenericUnixServices::setApplicationBadge(qint64 number) |
602 | { |
603 | #if QT_CONFIG(dbus) |
604 | if (qGuiApp->desktopFileName().isEmpty()) { |
605 | qWarning(msg: "QGuiApplication::desktopFileName() is empty" ); |
606 | return; |
607 | } |
608 | |
609 | |
610 | const QString launcherUrl = QStringLiteral("application://" ) + qGuiApp->desktopFileName() + QStringLiteral(".desktop" ); |
611 | const qint64 count = qBound(min: 0, val: number, max: 9999); |
612 | QVariantMap dbusUnityProperties; |
613 | |
614 | if (count > 0) { |
615 | dbusUnityProperties[QStringLiteral("count" )] = count; |
616 | dbusUnityProperties[QStringLiteral("count-visible" )] = true; |
617 | } else { |
618 | dbusUnityProperties[QStringLiteral("count-visible" )] = false; |
619 | } |
620 | |
621 | auto signal = QDBusMessage::createSignal(QStringLiteral("/com/canonical/unity/launcherentry/" ) |
622 | + qGuiApp->applicationName(), QStringLiteral("com.canonical.Unity.LauncherEntry" ), QStringLiteral("Update" )); |
623 | |
624 | signal.setArguments({launcherUrl, dbusUnityProperties}); |
625 | |
626 | QDBusConnection::sessionBus().send(message: signal); |
627 | #else |
628 | Q_UNUSED(number) |
629 | #endif |
630 | } |
631 | |
632 | QT_END_NAMESPACE |
633 | |
634 | #include "qgenericunixservices.moc" |
635 | |