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 * Flatpack intended to provide access to desktop functionality for
65 * sandboxed Flatpack 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);
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 * @post If supported on the current platform, the screen color picking is
189 * started. Results can be obtained via @ref newColor.
190 *
191 * @param previousColorRed On some platforms, the signal @ref newColor is
192 * emitted with this color if the user cancels the color picking with
193 * the ESC key. Range: <tt>[0, 255]</tt>
194 * @param previousColorGreen See above.
195 * @param previousColorBlue See above. */
196// Using quint8 to make clear what is the maximum range and maximum precision
197// that can be expected. Indeed, QColorDialog uses QColor which allows for
198// more precision. However, it seems to not use it: When ESC is pressed,
199// previous value is restored only with this precision. So we use quint8
200// to make clear which precision will actually be provided of the underlying
201// implementation.
202void ScreenColorPicker::startPicking(quint8 previousColorRed, quint8 previousColorGreen, quint8 previousColorBlue)
203{
204 if (!parent()) {
205 // This class derives (currently) from QWidget, and QWidget guarantees
206 // that parent() will always return a QWidget (and not just a QObject).
207 // Without a parent widget, the QColorDialog support does not work.
208 // While the Portal support works also without parent widgets, it
209 // seems better to enforce a widget parent here, so that we get
210 // consistent behaviour for all possible backends.
211 return;
212 }
213
214 // The “Portal” implementation has priority over the “QColorDialog”
215 // implementation, because
216 // 1. “Portal” works reliably also on multi-monitor setups.
217 // QColorDialog doesn’t: https://bugreports.qt.io/browse/QTBUG-94748
218 // In Qt 6.5, QColorDialog starts to use “Portal” too, see
219 // https://bugreports.qt.io/browse/QTBUG-81538 but only for Wayland,
220 // and not for X11. We, however, also want it for X11.
221 // 2. The “QColorDialog” implementation is a hack because it relies on
222 // Qt’s internals, which could change in future versions and break
223 // our implementation, so we should avoid it if we can.
224 if (hasPortalSupport()) {
225 pickWithPortal();
226 return;
227 }
228
229 initializeQColorDialogSupport();
230 if (m_qColorDialogScreenButton) {
231 const auto previousColor = QColor(previousColorRed, //
232 previousColorGreen, //
233 previousColorBlue);
234 m_qColorDialog->setCurrentColor(previousColor);
235 m_qColorDialogScreenButton->click();
236 }
237}
238
239/** @brief Start color picking using the “Portal”. */
240void ScreenColorPicker::pickWithPortal()
241{
242 // For “Portal”, the parent window identifier is used if the
243 // requested function shows a dialog: This dialog will then be
244 // centered within and modal to the parent window. This includes
245 // permission dialog with which the user is asked if he grants permission
246 // to the application to use the requested function. Apparently,
247 // for screen color picker there is no permission dialog in KDE, so the
248 // identifier is rather useless. The format of the handle is defined in
249 // https://flatpak.github.io/xdg-desktop-portal/#parent_window
250 // and has different content for X11 and Wayland. X11 is easy to
251 // implement, while Wayland handles are more complex requiring a call
252 // with the xdg_foreign protocol. For other windowing systems, an
253 // empty string should be used. While tests show that is works fine
254 // with an empty string in X11, we provide at least the easy
255 // identifier for X11.
256 QString parentWindowIdentifier;
257 if (QGuiApplication::platformName() == QStringLiteral("xcb")) {
258 const QWidget *const parentWidget = qobject_cast<QWidget *>(parent());
259 if (parentWidget != nullptr) {
260 parentWindowIdentifier = QStringLiteral("x11:") //
261 + QString::number(parentWidget->winId(), 16);
262 }
263 }
264
265 // “Portal” documentation: https://flatpak.github.io/xdg-desktop-portal
267 QStringLiteral("org.freedesktop.portal.Desktop"), // service
268 QStringLiteral("/org/freedesktop/portal/desktop"), // path
269 QStringLiteral("org.freedesktop.portal.Screenshot"), // interface
270 QStringLiteral("PickColor")); // method
271 message << parentWindowIdentifier // argument: parent_window
272 << QVariantMap(); // argument: options
273 QDBusPendingCall pendingCall = //
275 auto watcher = new QDBusPendingCallWatcher(pendingCall, this);
276 connect(watcher, //
278 this, //
279 [this](QDBusPendingCallWatcher *myWatcher) {
280 myWatcher->deleteLater();
281 QDBusPendingReply<QDBusObjectPath> reply = *myWatcher;
282 if (!reply.isError()) {
283 QDBusConnection::sessionBus().connect(
284 // service
285 QStringLiteral("org.freedesktop.portal.Desktop"),
286 // path
287 reply.value().path(),
288 // interface
289 QStringLiteral("org.freedesktop.portal.Request"),
290 // name
291 QStringLiteral("Response"),
292 // receiver
293 this,
294 // slot
295 SLOT(getPortalResponse(uint, QVariantMap)));
296 // Ignoring the result of connect() because subsequent
297 // calls might occur with the same path(), which will
298 // make connect() return “false” because the connection
299 // is yet established, which is okay and not a failure;
300 // the slot will be called only once nevertheless.
301 }
302 });
303}
304
305/** @brief Process the response we get from the “Portal” service. */
306void ScreenColorPicker::getPortalResponse(uint exitCode, const QVariantMap &responseArguments)
307{
308 if (exitCode != 0) {
309 return;
310 }
311 const QDBusArgument responseColor = responseArguments //
312 .value(QStringLiteral("color")) //
313 .value<QDBusArgument>();
314 QList<double> rgb;
315 responseColor.beginStructure();
316 while (!responseColor.atEnd()) {
317 double temp;
318 responseColor >> temp;
319 rgb.append(temp);
320 }
321 responseColor.endStructure();
322 if (rgb.count() == 3) {
323 Q_EMIT newColor(rgb.at(0), rgb.at(1), rgb.at(2));
324 }
325}
326
327} // 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
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
void deleteLater()
QString tr(const char *sourceText, const char *disambiguation, int n)
void setDefault(bool)
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 Tue Mar 26 2024 11:20:36 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.