File indexing completed on 2024-05-12 04:44:35

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 #include "screencolorpicker.h"
0005 #include <qcolor.h>
0006 #include <qcolordialog.h>
0007 #include <qdbusargument.h>
0008 #include <qdbusconnection.h>
0009 #include <qdbusextratypes.h>
0010 #include <qdbusmessage.h>
0011 #include <qdbuspendingcall.h>
0012 #include <qdbuspendingreply.h>
0013 #include <qglobal.h>
0014 #include <qguiapplication.h>
0015 #include <qlist.h>
0016 #include <qobjectdefs.h>
0017 #include <qpushbutton.h>
0018 #include <qstring.h>
0019 #include <qstringbuilder.h>
0020 #include <qstringliteral.h>
0021 #include <qvariant.h>
0022 #include <qwidget.h>
0023 #include <type_traits>
0024 #include <utility>
0025 
0026 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0027 #include <qmap.h>
0028 #else
0029 #include <qmetatype.h>
0030 #endif
0031 
0032 namespace PerceptualColor
0033 {
0034 
0035 /** @brief Constructor
0036  *
0037  *  @param parent pointer to the parent widget, if any */
0038 ScreenColorPicker::ScreenColorPicker(QWidget *parent)
0039     : QWidget(parent)
0040 {
0041     hide();
0042 }
0043 
0044 /** @brief Destructor */
0045 ScreenColorPicker::~ScreenColorPicker()
0046 {
0047 }
0048 
0049 /** @brief If screen color picking is available at the current platform.
0050  *
0051  * @returns If screen color picking is available at the current platform. */
0052 bool ScreenColorPicker::isAvailable()
0053 {
0054     if (hasPortalSupport()) {
0055         return true;
0056     }
0057     initializeQColorDialogSupport();
0058     return m_hasQColorDialogSupport.value();
0059 }
0060 
0061 /** @brief If “Portal” support is available.
0062  *
0063  * “Portal” is a Freedesktop (formerly XDG) service maintained by
0064  * Flatpack intended to provide access to desktop functionality for
0065  * sandboxed Flatpack applications.
0066  *
0067  * @returns If “Portal” support is available. */
0068 bool ScreenColorPicker::hasPortalSupport()
0069 {
0070     static const bool m_hasPortalSupport = queryPortalSupport();
0071     return m_hasPortalSupport;
0072 }
0073 
0074 /** @brief Make a DBus query for “Portal” screen color picker support.
0075  *
0076  * This function makes a synchronous DBus query to see if there is
0077  * support for screen color picker in the current system.
0078  * It might be slow.
0079  *
0080  * @note Do not use this function directly. Instead, for performance
0081  * reasons, use @ref hasPortalSupport which provides a cached value.
0082  *
0083  * @returns If there is support for “Portal” color picking. */
0084 bool ScreenColorPicker::queryPortalSupport()
0085 {
0086     QDBusMessage message = QDBusMessage::createMethodCall( //
0087         QStringLiteral("org.freedesktop.portal.Desktop"), // service
0088         QStringLiteral("/org/freedesktop/portal/desktop"), // path
0089         QStringLiteral("org.freedesktop.DBus.Properties"), // interface
0090         QStringLiteral("Get")); // method
0091     message << QStringLiteral("org.freedesktop.portal.Screenshot") // argument
0092             << QStringLiteral("version"); // argument
0093     const QDBusMessage reply = QDBusConnection::sessionBus().call(message);
0094     if (reply.type() != QDBusMessage::MessageType::ReplyMessage) {
0095         return false;
0096     }
0097     constexpr quint8 minimumSupportedPortalVersion = 2;
0098     const qulonglong actualPortalVersion = reply //
0099                                                .arguments() //
0100                                                .value(0) //
0101                                                .value<QDBusVariant>() //
0102                                                .variant() //
0103                                                .toULongLong();
0104     if (actualPortalVersion < minimumSupportedPortalVersion) {
0105         // No screen color picker support available
0106         return false;
0107     }
0108     return true;
0109 }
0110 
0111 /** @brief Translates a given text in the context of QColorDialog.
0112  *
0113  * @param sourceText The text to be translated.
0114  * @returns The translation. */
0115 QString ScreenColorPicker::translateViaQColorDialog(const char *sourceText)
0116 {
0117     return QColorDialog::tr(sourceText);
0118 }
0119 
0120 /** @brief Test for QColorDialog support, and if available, initialize it.
0121  *
0122  * @post @ref m_hasQColorDialogSupport holds if QColorDialog support is
0123  * available. If so, also @ref m_qColorDialogScreenButton holds a value.
0124  *
0125  * Calling this function the first time might be expensive, but subsequent
0126  * calls will be cheap.
0127  *
0128  * @note This basically hijacks QColorDialog’s screen picker, but
0129  * this relies on internals of Qt and could therefore theoretically
0130  * fail in later Qt versions. On the other hand, making a
0131  * cross-platform implementation ourself would also be a lot
0132  * of work. However, if we could solve this, we could claim again at
0133  * @ref index "main page" that we do not use internal APIs. There is
0134  * also a <a href="https://bugreports.qt.io/browse/QTBUG-109440">request
0135  * to add a public API to Qt</a> for this. */
0136 void ScreenColorPicker::initializeQColorDialogSupport()
0137 {
0138     if (m_hasQColorDialogSupport.has_value()) {
0139         if (m_hasQColorDialogSupport.value() == false) {
0140             // We know yet from a previous attempt that there is no
0141             // support for QColorDialog.
0142             return;
0143         }
0144     }
0145 
0146     if (m_qColorDialogScreenButton) {
0147         // Yet initialized.
0148         return;
0149     }
0150 
0151     m_qColorDialog = new QColorDialog();
0152     m_qColorDialog->setOptions( //
0153         QColorDialog::DontUseNativeDialog | QColorDialog::NoButtons);
0154     const auto buttonList = m_qColorDialog->findChildren<QPushButton *>();
0155     for (const auto &button : std::as_const(buttonList)) {
0156         button->setDefault(false); // Prevent interfering with our dialog.
0157         // Going through translateViaQColorDialog() to avoid that the
0158         // string will be included in our own translation file; instead
0159         // intentionally fallback to Qt-provided translation.
0160         if (button->text() == translateViaQColorDialog("&Pick Screen Color")) {
0161             m_qColorDialogScreenButton = button;
0162         }
0163     }
0164     m_hasQColorDialogSupport = m_qColorDialogScreenButton;
0165     if (m_hasQColorDialogSupport) {
0166         m_qColorDialog->setParent(this);
0167         m_qColorDialog->hide();
0168         connect(m_qColorDialog, //
0169                 &QColorDialog::currentColorChanged, //
0170                 this, //
0171                 [this](const QColor &color) {
0172                     const auto red = static_cast<double>(color.redF());
0173                     const auto green = static_cast<double>(color.greenF());
0174                     const auto blue = static_cast<double>(color.blueF());
0175                     Q_EMIT newColor(red, green, blue);
0176                 });
0177     } else {
0178         delete m_qColorDialog;
0179         m_qColorDialog = nullptr;
0180     }
0181 }
0182 
0183 /** @brief Start the screen color picking.
0184  *
0185  * @pre This widget has a parent widget which should be a widget within
0186  * the currently active window.
0187  *
0188  * @post If supported on the current platform, the screen color picking is
0189  * started. Results can be obtained via @ref newColor.
0190  *
0191  * @param previousColorRed On some platforms, the signal @ref newColor is
0192  * emitted with this color if the user cancels the color picking with
0193  * the ESC key. Range: <tt>[0, 255]</tt>
0194  * @param previousColorGreen See above.
0195  * @param previousColorBlue See above. */
0196 // Using quint8 to make clear what is the maximum range and maximum precision
0197 // that can be expected. Indeed, QColorDialog uses QColor which allows for
0198 // more precision. However, it seems to not use it: When ESC is pressed,
0199 // previous value is restored only with this precision. So we use quint8
0200 // to make clear which precision will actually be provided of the underlying
0201 // implementation.
0202 void ScreenColorPicker::startPicking(quint8 previousColorRed, quint8 previousColorGreen, quint8 previousColorBlue)
0203 {
0204     if (!parent()) {
0205         // This class derives (currently) from QWidget, and QWidget guarantees
0206         // that parent() will always return a QWidget (and not just a QObject).
0207         // Without a parent widget, the QColorDialog support does not work.
0208         // While the Portal support works also without parent widgets, it
0209         // seems better to enforce a widget parent here, so that we get
0210         // consistent behaviour for all possible backends.
0211         return;
0212     }
0213 
0214     // The “Portal” implementation has priority over the “QColorDialog”
0215     // implementation, because
0216     // 1. “Portal” works reliably also on multi-monitor setups.
0217     //    QColorDialog doesn’t: https://bugreports.qt.io/browse/QTBUG-94748
0218     //    In Qt 6.5, QColorDialog starts to use “Portal” too, see
0219     //    https://bugreports.qt.io/browse/QTBUG-81538 but only for Wayland,
0220     //    and not for X11. We, however, also want it for X11.
0221     // 2. The “QColorDialog” implementation is a hack because it relies on
0222     //    Qt’s internals, which could change in future versions and break
0223     //    our implementation, so we should avoid it if we can.
0224     if (hasPortalSupport()) {
0225         pickWithPortal();
0226         return;
0227     }
0228 
0229     initializeQColorDialogSupport();
0230     if (m_qColorDialogScreenButton) {
0231         const auto previousColor = QColor(previousColorRed, //
0232                                           previousColorGreen, //
0233                                           previousColorBlue);
0234         m_qColorDialog->setCurrentColor(previousColor);
0235         m_qColorDialogScreenButton->click();
0236     }
0237 }
0238 
0239 /** @brief Start color picking using the “Portal”. */
0240 void ScreenColorPicker::pickWithPortal()
0241 {
0242     // For “Portal”, the parent window identifier is used if the
0243     // requested function shows a dialog: This dialog will then be
0244     // centered within and modal to the parent window. This includes
0245     // permission dialog with which the user is asked if he grants permission
0246     // to the application to use the requested function. Apparently,
0247     // for screen color picker there is no permission dialog in KDE, so the
0248     // identifier is rather useless. The format of the handle is defined in
0249     // https://flatpak.github.io/xdg-desktop-portal/#parent_window
0250     // and has different content for X11 and Wayland. X11 is easy to
0251     // implement, while Wayland handles are more complex requiring a call
0252     // with the xdg_foreign protocol. For other windowing systems, an
0253     // empty string should be used. While tests show that is works fine
0254     // with an empty string in X11, we provide at least the easy
0255     // identifier for X11.
0256     QString parentWindowIdentifier;
0257     if (QGuiApplication::platformName() == QStringLiteral("xcb")) {
0258         const QWidget *const parentWidget = qobject_cast<QWidget *>(parent());
0259         if (parentWidget != nullptr) {
0260             parentWindowIdentifier = QStringLiteral("x11:") //
0261                 + QString::number(parentWidget->winId(), 16);
0262         }
0263     }
0264 
0265     // “Portal” documentation: https://flatpak.github.io/xdg-desktop-portal
0266     QDBusMessage message = QDBusMessage::createMethodCall( //
0267         QStringLiteral("org.freedesktop.portal.Desktop"), // service
0268         QStringLiteral("/org/freedesktop/portal/desktop"), // path
0269         QStringLiteral("org.freedesktop.portal.Screenshot"), // interface
0270         QStringLiteral("PickColor")); // method
0271     message << parentWindowIdentifier // argument: parent_window
0272             << QVariantMap(); // argument: options
0273     QDBusPendingCall pendingCall = //
0274         QDBusConnection::sessionBus().asyncCall(message);
0275     auto watcher = new QDBusPendingCallWatcher(pendingCall, this);
0276     connect(watcher, //
0277             &QDBusPendingCallWatcher::finished, //
0278             this, //
0279             [this](QDBusPendingCallWatcher *myWatcher) {
0280                 myWatcher->deleteLater();
0281                 QDBusPendingReply<QDBusObjectPath> reply = *myWatcher;
0282                 if (!reply.isError()) {
0283                     QDBusConnection::sessionBus().connect(
0284                         // service
0285                         QStringLiteral("org.freedesktop.portal.Desktop"),
0286                         // path
0287                         reply.value().path(),
0288                         // interface
0289                         QStringLiteral("org.freedesktop.portal.Request"),
0290                         // name
0291                         QStringLiteral("Response"),
0292                         // receiver
0293                         this,
0294                         // slot
0295                         SLOT(getPortalResponse(uint, QVariantMap)));
0296                     // Ignoring the result of connect() because subsequent
0297                     // calls might occur with the same path(), which will
0298                     // make connect() return “false” because the connection
0299                     // is yet established, which is okay and not a failure;
0300                     // the slot will be called only once nevertheless.
0301                 }
0302             });
0303 }
0304 
0305 /** @brief Process the response we get from the “Portal” service. */
0306 void ScreenColorPicker::getPortalResponse(uint exitCode, const QVariantMap &responseArguments)
0307 {
0308     if (exitCode != 0) {
0309         return;
0310     }
0311     const QDBusArgument responseColor = responseArguments //
0312                                             .value(QStringLiteral("color")) //
0313                                             .value<QDBusArgument>();
0314     QList<double> rgb;
0315     responseColor.beginStructure();
0316     while (!responseColor.atEnd()) {
0317         double temp;
0318         responseColor >> temp;
0319         rgb.append(temp);
0320     }
0321     responseColor.endStructure();
0322     if (rgb.count() == 3) {
0323         Q_EMIT newColor(rgb.at(0), rgb.at(1), rgb.at(2));
0324     }
0325 }
0326 
0327 } // namespace PerceptualColor