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