File indexing completed on 2024-04-21 04:52:31

0001 /*
0002     SPDX-FileCopyrightText: 2010 Till Theato <root@ttill.de>
0003 
0004 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "colorpickerwidget.h"
0008 #include "core.h"
0009 #include "mainwindow.h"
0010 
0011 #include <KLocalizedString>
0012 #include <QApplication>
0013 #include <QDBusConnection>
0014 #include <QDBusMessage>
0015 #include <QDBusMetaType>
0016 #include <QDBusObjectPath>
0017 #include <QDBusPendingCall>
0018 #include <QDBusPendingCallWatcher>
0019 #include <QDBusPendingReply>
0020 #include <QDebug>
0021 #include <QHBoxLayout>
0022 #include <QMouseEvent>
0023 #include <QScreen>
0024 #include <QStyleOptionFocusRect>
0025 #include <QStylePainter>
0026 #include <QTimer>
0027 #include <QToolButton>
0028 
0029 #ifdef Q_WS_X11
0030 #include <X11/Xutil.h>
0031 #include <fixx11h.h>
0032 #endif
0033 
0034 QDBusArgument &operator<<(QDBusArgument &arg, const QColor &color)
0035 {
0036     arg.beginStructure();
0037     arg << color.redF() << color.greenF() << color.blueF();
0038     arg.endStructure();
0039     return arg;
0040 }
0041 
0042 const QDBusArgument &operator>>(const QDBusArgument &arg, QColor &color)
0043 {
0044     double red, green, blue;
0045     arg.beginStructure();
0046     arg >> red >> green >> blue;
0047     color.setRedF(red);
0048     color.setGreenF(green);
0049     color.setBlueF(blue);
0050     arg.endStructure();
0051 
0052     return arg;
0053 }
0054 
0055 MyFrame::MyFrame(QWidget *parent)
0056     : QFrame(parent)
0057 {
0058     setFrameStyle(QFrame::Box | QFrame::Plain);
0059     setWindowOpacity(0.5);
0060     setWindowFlags(Qt::FramelessWindowHint);
0061 }
0062 
0063 // virtual
0064 void MyFrame::hideEvent(QHideEvent *event)
0065 {
0066     QFrame::hideEvent(event);
0067     // We need a timer here since hiding the frame will trigger a monitor refresh timer that will
0068     // repaint the monitor after 70 ms.
0069     QTimer::singleShot(250, this, &MyFrame::getColor);
0070 }
0071 
0072 ColorPickerWidget::ColorPickerWidget(QWidget *parent)
0073     : QWidget(parent)
0074     , m_mouseColor(Qt::transparent)
0075 
0076 {
0077 #ifdef Q_WS_X11
0078     m_image = nullptr;
0079 #endif
0080 
0081     auto *layout = new QHBoxLayout(this);
0082     layout->setContentsMargins(0, 0, 0, 0);
0083 
0084     // Check whether grabWindow() works. On some systems like with Wayland it does.
0085     // We fallback to the Freedesktop portal with DBus which has less features than
0086     // our custom implementation (eg. preview and average color are missing)
0087     if (pCore) {
0088         QPoint p(pCore->window()->geometry().center());
0089         for (QScreen *screen : QGuiApplication::screens()) {
0090             QRect screenRect = screen->geometry();
0091             if (screenRect.contains(p)) {
0092                 QPixmap pm = screen->grabWindow(pCore->window()->winId(), p.x(), p.y(), 1, 1);
0093                 qDebug() << "got pixmap that is not null";
0094                 m_useDBus = pm.isNull();
0095                 break;
0096             }
0097         }
0098     }
0099 
0100     auto *button = new QToolButton(this);
0101     button->setIcon(QIcon::fromTheme(QStringLiteral("color-picker")));
0102     button->setToolTip(i18n("Pick a color on the screen."));
0103     button->setAutoRaise(true);
0104     if (!m_useDBus) {
0105         button->setWhatsThis(xi18nc("@info:whatsthis", "Pick a color on the screen. By pressing the mouse button and then moving your mouse you can select a "
0106                                                        "section of the screen from which to get an average color."));
0107         connect(button, &QAbstractButton::clicked, this, &ColorPickerWidget::slotSetupEventFilter);
0108         setFocusPolicy(Qt::StrongFocus);
0109         setMouseTracking(true);
0110     } else {
0111         qDBusRegisterMetaType<QColor>();
0112         connect(button, &QAbstractButton::clicked, this, &ColorPickerWidget::grabColorDBus);
0113     }
0114 
0115     layout->addWidget(button);
0116     m_grabRectFrame = new MyFrame();
0117     m_grabRectFrame->hide();
0118 }
0119 
0120 ColorPickerWidget::~ColorPickerWidget()
0121 {
0122     delete m_grabRectFrame;
0123     if (m_filterActive) {
0124         removeEventFilter(this);
0125     }
0126 }
0127 
0128 void ColorPickerWidget::paintEvent(QPaintEvent *event)
0129 {
0130     Q_UNUSED(event);
0131     QStylePainter painter(this);
0132 
0133     QStyleOptionComplex option;
0134     option.initFrom(this);
0135     if (m_filterActive) {
0136         QRect r = option.rect;
0137         int margin = r.height() / 8;
0138         r.adjust(margin, 4 * margin, -margin, -margin);
0139         painter.fillRect(r, m_mouseColor);
0140     }
0141     painter.drawComplexControl(QStyle::CC_ToolButton, option);
0142 }
0143 
0144 void ColorPickerWidget::slotGetAverageColor()
0145 {
0146     disconnect(m_grabRectFrame, SIGNAL(getColor()), this, SLOT(slotGetAverageColor()));
0147     m_grabRect = m_grabRect.normalized();
0148 
0149     int numPixel = m_grabRect.width() * m_grabRect.height();
0150 
0151     int sumR = 0;
0152     int sumG = 0;
0153     int sumB = 0;
0154 
0155 /*
0156  Only getting the image once for the whole rect
0157  results in a vast speed improvement.
0158 */
0159 #ifdef Q_WS_X11
0160     Window root = RootWindow(QX11Info::display(), QX11Info::appScreen());
0161     m_image = XGetImage(QX11Info::display(), root, m_grabRect.x(), m_grabRect.y(), m_grabRect.width(), m_grabRect.height(), -1, ZPixmap);
0162 #else
0163     for (QScreen *screen : QGuiApplication::screens()) {
0164         QRect screenRect = screen->geometry();
0165         if (screenRect.contains(m_grabRect.topLeft())) {
0166             m_image =
0167                 screen->grabWindow(0, m_grabRect.x() - screenRect.x(), m_grabRect.y() - screenRect.y(), m_grabRect.width(), m_grabRect.height()).toImage();
0168             break;
0169         }
0170     }
0171 #endif
0172 
0173     for (int x = 0; x < m_grabRect.width(); ++x) {
0174         for (int y = 0; y < m_grabRect.height(); ++y) {
0175             QColor color = grabColor(QPoint(x, y), false);
0176             sumR += color.red();
0177             sumG += color.green();
0178             sumB += color.blue();
0179         }
0180     }
0181 
0182 #ifdef Q_WS_X11
0183     XDestroyImage(m_image);
0184     m_image = nullptr;
0185 #else
0186     m_image = QImage();
0187 #endif
0188 
0189     Q_EMIT colorPicked(QColor(sumR / numPixel, sumG / numPixel, sumB / numPixel));
0190     Q_EMIT disableCurrentFilter(false);
0191 }
0192 
0193 void ColorPickerWidget::mousePressEvent(QMouseEvent *event)
0194 {
0195     if (event->button() != Qt::LeftButton) {
0196         closeEventFilter();
0197         Q_EMIT disableCurrentFilter(false);
0198         event->accept();
0199         return;
0200     }
0201 
0202     if (m_filterActive) {
0203 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0204         m_clickPoint = event->globalPos();
0205 #else
0206         m_clickPoint = event->globalPosition().toPoint();
0207 #endif
0208         m_grabRect = QRect(m_clickPoint, QSize(1, 1));
0209         m_grabRectFrame->setGeometry(m_grabRect);
0210         m_grabRectFrame->show();
0211     }
0212 }
0213 
0214 void ColorPickerWidget::mouseReleaseEvent(QMouseEvent *event)
0215 {
0216     if (m_filterActive) {
0217         closeEventFilter();
0218 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0219         m_grabRect.setWidth(event->globalX() - m_grabRect.x());
0220         m_grabRect.setHeight(event->globalY() - m_grabRect.y());
0221 #else
0222         m_grabRect.setWidth(event->globalPosition().x() - m_grabRect.x());
0223         m_grabRect.setHeight(event->globalPosition().y() - m_grabRect.y());
0224 #endif
0225         m_grabRect = m_grabRect.normalized();
0226         m_clickPoint = QPoint();
0227 
0228         if (m_grabRect.width() * m_grabRect.height() == 0) {
0229             Q_EMIT colorPicked(m_mouseColor);
0230             Q_EMIT disableCurrentFilter(false);
0231         } else {
0232             // delay because m_grabRectFrame does not hide immediately
0233             connect(m_grabRectFrame, SIGNAL(getColor()), this, SLOT(slotGetAverageColor()));
0234             m_grabRectFrame->hide();
0235         }
0236     }
0237     QWidget::mouseReleaseEvent(event);
0238 }
0239 
0240 void ColorPickerWidget::mouseMoveEvent(QMouseEvent *event)
0241 {
0242     // Draw live rectangle of current color under mouse
0243     m_mouseColor = grabColor(QCursor::pos(), true);
0244     update();
0245     if (m_filterActive && !m_clickPoint.isNull()) {
0246 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0247         m_grabRect.setWidth(event->globalX() - m_grabRect.x());
0248         m_grabRect.setHeight(event->globalY() - m_grabRect.y());
0249 #else
0250         m_grabRect.setWidth(event->globalPosition().x() - m_grabRect.x());
0251         m_grabRect.setHeight(event->globalPosition().y() - m_grabRect.y());
0252 #endif
0253         m_grabRectFrame->setGeometry(m_grabRect.normalized());
0254     }
0255 }
0256 
0257 void ColorPickerWidget::slotSetupEventFilter()
0258 {
0259     Q_EMIT disableCurrentFilter(true);
0260     m_filterActive = true;
0261     setFocus();
0262     installEventFilter(this);
0263     grabMouse(QCursor(QIcon::fromTheme(QStringLiteral("color-picker")).pixmap(32, 32), 4, 28));
0264     grabKeyboard();
0265 }
0266 
0267 void ColorPickerWidget::closeEventFilter()
0268 {
0269     m_filterActive = false;
0270     releaseMouse();
0271     releaseKeyboard();
0272     removeEventFilter(this);
0273 }
0274 
0275 bool ColorPickerWidget::eventFilter(QObject *object, QEvent *event)
0276 {
0277     // Close color picker on any key press
0278     if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
0279         closeEventFilter();
0280         Q_EMIT disableCurrentFilter(false);
0281         event->setAccepted(true);
0282         return true;
0283     }
0284     return QObject::eventFilter(object, event);
0285 }
0286 
0287 QColor ColorPickerWidget::grabColor(const QPoint &p, bool destroyImage)
0288 {
0289 #ifdef Q_WS_X11
0290     /*
0291      we use the X11 API directly in this case as we are not getting back a valid
0292      return from QPixmap::grabWindow in the case where the application is using
0293      an argb visual
0294     */
0295     if (!QApplication::primaryScreen()->geometry().contains(p)) {
0296         return QColor();
0297     }
0298     unsigned long xpixel;
0299     if (m_image == nullptr) {
0300         Window root = RootWindow(QX11Info::display(), QX11Info::appScreen());
0301         m_image = XGetImage(QX11Info::display(), root, p.x(), p.y(), 1, 1, -1, ZPixmap);
0302         xpixel = XGetPixel(m_image, 0, 0);
0303     } else {
0304         xpixel = XGetPixel(m_image, p.x(), p.y());
0305     }
0306     if (destroyImage) {
0307         XDestroyImage(m_image);
0308         m_image = 0;
0309     }
0310     XColor xcol;
0311     xcol.pixel = xpixel;
0312     xcol.flags = DoRed | DoGreen | DoBlue;
0313     XQueryColor(QX11Info::display(), DefaultColormap(QX11Info::display(), QX11Info::appScreen()), &xcol);
0314     return QColor::fromRgbF(xcol.red / 65535.0, xcol.green / 65535.0, xcol.blue / 65535.0);
0315 #else
0316     Q_UNUSED(destroyImage)
0317     if (m_image.isNull()) {
0318         for (QScreen *screen : QGuiApplication::screens()) {
0319             QRect screenRect = screen->geometry();
0320             if (screenRect.contains(p)) {
0321                 QPixmap pm = screen->grabWindow(0, p.x() - screenRect.x(), p.y() - screenRect.y(), 1, 1);
0322                 QImage i = pm.toImage();
0323                 return i.pixel(0, 0);
0324             }
0325         }
0326         return qRgb(0, 0, 0);
0327     }
0328     return m_image.pixel(p.x(), p.y());
0329 
0330 #endif
0331 }
0332 
0333 void ColorPickerWidget::grabColorDBus()
0334 {
0335     QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop"), QLatin1String("/org/freedesktop/portal/desktop"),
0336                                                           QLatin1String("org.freedesktop.portal.Screenshot"), QLatin1String("PickColor"));
0337     message << QLatin1String("x11:") << QVariantMap{};
0338     QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
0339     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall);
0340     Q_EMIT disableCurrentFilter(true);
0341     connect(watcher, &QDBusPendingCallWatcher::finished, [this](QDBusPendingCallWatcher *watcher) {
0342         QDBusPendingReply<QDBusObjectPath> reply = *watcher;
0343         Q_EMIT disableCurrentFilter(false);
0344         if (reply.isError()) {
0345             qWarning() << "Couldn't get reply";
0346             qWarning() << "Error: " << reply.error().message();
0347         } else {
0348             QDBusConnection::sessionBus().connect(QString(), reply.value().path(), QLatin1String("org.freedesktop.portal.Request"), QLatin1String("Response"),
0349                                                   this, SLOT(gotColorResponse(uint, QVariantMap)));
0350         }
0351     });
0352 }
0353 
0354 void ColorPickerWidget::gotColorResponse(uint response, const QVariantMap &results)
0355 {
0356     if (!response) {
0357         if (results.contains(QLatin1String("color"))) {
0358             const QColor color = qdbus_cast<QColor>(results.value(QLatin1String("color")));
0359             qDebug() << "picked" << color;
0360             m_mouseColor = color;
0361             Q_EMIT colorPicked(m_mouseColor);
0362         }
0363     } else {
0364         qWarning() << "Failed to take screenshot" << response << results;
0365     }
0366 }