Perceptual Color

screencolorpicker.cpp
1// SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
2// SPDX-License-Identifier: BSD-2-Clause OR MIT
3
4#include "screencolorpicker.h"
5#include <qcolor.h>
6#include <qcolordialog.h>
7#include <qdbusargument.h>
8#include <qdbusconnection.h>
9#include <qdbusextratypes.h>
10#include <qdbusmessage.h>
11#include <qdbuspendingcall.h>
12#include <qdbuspendingreply.h>
13#include <qglobal.h>
14#include <qguiapplication.h>
15#include <qlist.h>
16#include <qobjectdefs.h>
17#include <qpushbutton.h>
18#include <qstring.h>
19#include <qstringbuilder.h>
20#include <qstringliteral.h>
21#include <qvariant.h>
22#include <qwidget.h>
23#include <type_traits>
24#include <utility>
25
26#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
27#include <qmap.h>
28#else
29#include <qmetatype.h>
30#endif
31
32namespace PerceptualColor
33{
34
35/** @brief Constructor
36 *
37 * @param parent pointer to the parent widget, if any */
38ScreenColorPicker::ScreenColorPicker(QWidget *parent)
39 : QWidget(parent)
40{
41 hide();
42}
43
44/** @brief Destructor */
45ScreenColorPicker::~ScreenColorPicker()
46{
47}
48
49/** @brief If screen color picking is available at the current platform.
50 *
51 * @returns If screen color picking is available at the current platform. */
52bool ScreenColorPicker::isAvailable()
53{
54 if (hasPortalSupport()) {
55 return true;
56 }
57 initializeQColorDialogSupport();
58 return m_hasQColorDialogSupport.value();
59}
60
61/** @brief If “Portal” support is available.
62 *
63 * “Portal” is a Freedesktop (formerly XDG) service maintained by
64 * Flatpak intended to provide access to desktop functionality for
65 * sandboxed Flatpak applications.
66 *
67 * @returns If “Portal” support is available. */
68bool ScreenColorPicker::hasPortalSupport()
69{
70 static const bool m_hasPortalSupport = queryPortalSupport();
71 return m_hasPortalSupport;
72}
73
74/** @brief Make a DBus query for “Portal” screen color picker support.
75 *
76 * This function makes a synchronous DBus query to see if there is
77 * support for screen color picker in the current system.
78 * It might be slow.
79 *
80 * @note Do not use this function directly. Instead, for performance
81 * reasons, use @ref hasPortalSupport which provides a cached value.
82 *
83 * @returns If there is support for “Portal” color picking. */
84bool ScreenColorPicker::queryPortalSupport()
85{
87 QStringLiteral("org.freedesktop.portal.Desktop"), // service
88 QStringLiteral("/org/freedesktop/portal/desktop"), // path
89 QStringLiteral("org.freedesktop.DBus.Properties"), // interface
90 QStringLiteral("Get")); // method
91 message << QStringLiteral("org.freedesktop.portal.Screenshot") // argument
92 << QStringLiteral("version"); // argument
93 const QDBusMessage reply = QDBusConnection::sessionBus().call(message);
94 if (reply.type() != QDBusMessage::MessageType::ReplyMessage) {
95 return false;
96 }
97 constexpr quint8 minimumSupportedPortalVersion = 2;
98 const qulonglong actualPortalVersion = reply //
99 .arguments() //
100 .value(0) //
101 .value<QDBusVariant>() //
102 .variant() //
103 .toULongLong();
104 if (actualPortalVersion < minimumSupportedPortalVersion) {
105 // No screen color picker support available
106 return false;
107 }
108 return true;
109}
110
111/** @brief Translates a given text in the context of QColorDialog.
112 *
113 * @param sourceText The text to be translated.
114 * @returns The translation. */
115QString ScreenColorPicker::translateViaQColorDialog(const char *sourceText)
116{
117 return QColorDialog::tr(sourceText);
118}
119
120/** @brief Test for QColorDialog support, and if available, initialize it.
121 *
122 * @post @ref m_hasQColorDialogSupport holds if QColorDialog support is
123 * available. If so, also @ref m_qColorDialogScreenButton holds a value.
124 *
125 * Calling this function the first time might be expensive, but subsequent
126 * calls will be cheap.
127 *
128 * @note This basically hijacks QColorDialog’s screen picker, but
129 * this relies on internals of Qt and could therefore theoretically
130 * fail in later Qt versions. On the other hand, making a
131 * cross-platform implementation ourself would also be a lot
132 * of work. However, if we could solve this, we could claim again at
133 * @ref index "main page" that we do not use internal APIs. There is
134 * also a <a href="https://bugreports.qt.io/browse/QTBUG-109440">request
135 * to add a public API to Qt</a> for this. */
136void ScreenColorPicker::initializeQColorDialogSupport()
137{
138 if (m_hasQColorDialogSupport.has_value()) {
139 if (m_hasQColorDialogSupport.value() == false) {
140 // We know yet from a previous attempt that there is no
141 // support for QColorDialog.
142 return;
143 }
144 }
145
146 if (m_qColorDialogScreenButton) {
147 // Yet initialized.
148 return;
149 }
150
151 m_qColorDialog = new QColorDialog();
152 m_qColorDialog->setOptions( //
154 const auto buttonList = m_qColorDialog->findChildren<QPushButton *>();
155 for (const auto &button : std::as_const(buttonList)) {
156 button->setDefault(false); // Prevent interfering with our dialog.
157 // Going through translateViaQColorDialog() to avoid that the
158 // string will be included in our own translation file; instead
159 // intentionally fallback to Qt-provided translation.
160 if (button->text() == translateViaQColorDialog("&Pick Screen Color")) {
161 m_qColorDialogScreenButton = button;
162 }
163 }
164 m_hasQColorDialogSupport = m_qColorDialogScreenButton;
165 if (m_hasQColorDialogSupport) {
166 m_qColorDialog->setParent(this);
167 m_qColorDialog->hide();
168 connect(m_qColorDialog, //
170 this, //
171 [this](const QColor &color) {
172 const auto red = static_cast<double>(color.redF());
173 const auto green = static_cast<double>(color.greenF());
174 const auto blue = static_cast<double>(color.blueF());
175 Q_EMIT newColor(red, green, blue, false);
176 });
177 } else {
178 delete m_qColorDialog;
179 m_qColorDialog = nullptr;
180 }
181}
182
183/** @brief Start the screen color picking.
184 *
185 * @pre This widget has a parent widget which should be a widget within
186 * the currently active window.
187 *
188 * @warning On some platforms, behind the scenes, QColorDialog is hijacked
189 * to perform the actual color picking. If so, this will have side effects:
190 * It might mix up the default button setting of the parent dialog, if any.
191 * Workaround: If using default buttons in a parent dialog, reimplement
192 * <tt>QWidget::setVisible()</tt> in this parent dialog: Call the
193 * parent’s class implementation, and <em>after</em> that, call
194 * <tt>QPushButton::setDefault(true)</tt> on the default button.
195 *
196 * @post If supported on the current platform, the screen color picking is
197 * started. Results can be obtained via @ref newColor.
198 *
199 * @param previousColorRed On some platforms, the signal @ref newColor is
200 * emitted with this color if the user cancels the color picking with
201 * the ESC key. Range: <tt>[0, 255]</tt>
202 * @param previousColorGreen See above.
203 * @param previousColorBlue See above. */
204// Using quint8 to make clear what is the maximum range and maximum precision
205// that can be expected. Indeed, QColorDialog uses QColor which allows for
206// more precision. However, it seems to not use it: When ESC is pressed,
207// previous value is restored only with this precision. So we use quint8
208// to make clear which precision will actually be provided of the underlying
209// implementation.
210void ScreenColorPicker::startPicking(quint8 previousColorRed, quint8 previousColorGreen, quint8 previousColorBlue)
211{
212 if (!parent()) {
213 // This class derives (currently) from QWidget, and QWidget guarantees
214 // that parent() will always return a QWidget (and not just a QObject).
215 // Without a parent widget, the QColorDialog support does not work.
216 // While the Portal support works also without parent widgets, it
217 // seems better to enforce a widget parent here, so that we get
218 // consistent behaviour for all possible backends.
219 return;
220 }
221
222 // The “Portal” implementation has priority over the “QColorDialog”
223 // implementation, because
224 // 1. “Portal” works reliably also on multi-monitor setups.
225 // QColorDialog doesn’t: https://bugreports.qt.io/browse/QTBUG-94748
226 // In Qt 6.5, QColorDialog starts to use “Portal” too, see
227 // https://bugreports.qt.io/browse/QTBUG-81538 but only for Wayland,
228 // and not for X11. We, however, also want it for X11.
229 // 2. The “QColorDialog” implementation is a hack because it relies on
230 // Qt’s internals, which could change in future versions and break
231 // our implementation, so we should avoid it if we can.
232 if (hasPortalSupport()) {
233 pickWithPortal();
234 return;
235 }
236
237 initializeQColorDialogSupport();
238 if (m_qColorDialogScreenButton) {
239 const auto previousColor = QColor(previousColorRed, //
240 previousColorGreen, //
241 previousColorBlue);
242 m_qColorDialog->setCurrentColor(previousColor);
243 m_qColorDialogScreenButton->click();
244 }
245}
246
247/** @brief Start color picking using the “Portal”. */
248void ScreenColorPicker::pickWithPortal()
249{
250 // For “Portal”, the parent window identifier is used if the
251 // requested function shows a dialog: This dialog will then be
252 // centered within and modal to the parent window. This includes
253 // permission dialog with which the user is asked if he grants permission
254 // to the application to use the requested function. Apparently,
255 // for screen color picker there is no permission dialog in KDE, so the
256 // identifier is rather useless. The format of the handle is defined in
257 // https://flatpak.github.io/xdg-desktop-portal/#parent_window
258 // and has different content for X11 and Wayland. X11 is easy to
259 // implement, while Wayland handles are more complex requiring a call
260 // with the xdg_foreign protocol. For other windowing systems, an
261 // empty string should be used. While tests show that is works fine
262 // with an empty string in X11, we provide at least the easy
263 // identifier for X11.
264 QString parentWindowIdentifier;
265 if (QGuiApplication::platformName() == QStringLiteral("xcb")) {
266 const QWidget *const parentWidget = qobject_cast<QWidget *>(parent());
267 if (parentWidget != nullptr) {
268 parentWindowIdentifier = QStringLiteral("x11:") //
269 + QString::number(parentWidget->winId(), 16);
270 }
271 }
272
273 // “Portal” documentation:
274 // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Screenshot.html#org-freedesktop-portal-screenshot-pickcolor
276 QStringLiteral("org.freedesktop.portal.Desktop"), // service
277 QStringLiteral("/org/freedesktop/portal/desktop"), // path
278 QStringLiteral("org.freedesktop.portal.Screenshot"), // interface
279 QStringLiteral("PickColor")); // method
280 message << parentWindowIdentifier // argument: parent_window
281 << QVariantMap(); // argument: options
282 QDBusPendingCall pendingCall = //
284 auto watcher = new QDBusPendingCallWatcher(pendingCall, this);
285 connect(watcher, //
287 this, //
288 [this](QDBusPendingCallWatcher *myWatcher) {
289 myWatcher->deleteLater();
290 QDBusPendingReply<QDBusObjectPath> reply = *myWatcher;
291 if (!reply.isError()) {
293 // service
294 QStringLiteral("org.freedesktop.portal.Desktop"),
295 // path
296 reply.value().path(),
297 // interface
298 QStringLiteral("org.freedesktop.portal.Request"),
299 // name
300 QStringLiteral("Response"),
301 // receiver
302 this,
303 // slot
304 SLOT(getPortalResponse(uint, QVariantMap)));
305 // Ignoring the result of connect() because subsequent
306 // calls might occur with the same path(), which will
307 // make connect() return “false” because the connection
308 // is yet established, which is okay and not a failure;
309 // the slot will be called only once nevertheless.
310 }
311 });
312}
313
314/** @brief Process the response we get from the “Portal” service. */
315void ScreenColorPicker::getPortalResponse(uint exitCode, const QVariantMap &responseArguments)
316{
317 if (exitCode != 0) {
318 return;
319 }
320 const QDBusArgument responseColor = responseArguments //
321 .value(QStringLiteral("color")) //
322 .value<QDBusArgument>();
323 QList<double> rgb;
324 responseColor.beginStructure();
325 while (!responseColor.atEnd()) {
326 double temp;
327 responseColor >> temp;
328 rgb.append(temp);
329 }
330 responseColor.endStructure();
331 if (rgb.count() == 3) {
332 // The documentation of Portal claims to return always sRGB values,
333 // so if the screen has a different color space, portal is supposed
334 // to apply color management and return the sRGB correspondence.
335 Q_EMIT newColor(rgb.at(0), rgb.at(1), rgb.at(2), true);
336 }
337}
338
339} // namespace PerceptualColor
The namespace of this library.
float blueF() const const
float greenF() const const
float redF() const const
void currentColorChanged(const QColor &color)
bool atEnd() const const
void beginStructure()
void endStructure()
QDBusPendingCall asyncCall(const QDBusMessage &message, int timeout) const const
QDBusMessage call(const QDBusMessage &message, QDBus::CallMode mode, int timeout) const const
bool connect(const QString &service, const QString &path, const QString &interface, const QString &name, QObject *receiver, const char *slot)
QDBusConnection sessionBus()
QList< QVariant > arguments() const const
QDBusMessage createMethodCall(const QString &service, const QString &path, const QString &interface, const QString &method)
MessageType type() const const
void finished(QDBusPendingCallWatcher *self)
bool isError() const const
typename Select< 0 >::Type value() const const
void deleteLater()
QString tr(const char *sourceText, const char *disambiguation, int n)
QString number(double n, char format, int precision)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
WId winId() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:18:38 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.