File indexing completed on 2023-12-03 04:55:09
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 wether 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 avarage 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 m_clickPoint = event->globalPos(); 0204 m_grabRect = QRect(m_clickPoint, QSize(1, 1)); 0205 m_grabRectFrame->setGeometry(m_grabRect); 0206 m_grabRectFrame->show(); 0207 } 0208 } 0209 0210 void ColorPickerWidget::mouseReleaseEvent(QMouseEvent *event) 0211 { 0212 if (m_filterActive) { 0213 closeEventFilter(); 0214 0215 m_grabRect.setWidth(event->globalX() - m_grabRect.x()); 0216 m_grabRect.setHeight(event->globalY() - m_grabRect.y()); 0217 m_grabRect = m_grabRect.normalized(); 0218 m_clickPoint = QPoint(); 0219 0220 if (m_grabRect.width() * m_grabRect.height() == 0) { 0221 Q_EMIT colorPicked(m_mouseColor); 0222 Q_EMIT disableCurrentFilter(false); 0223 } else { 0224 // delay because m_grabRectFrame does not hide immediately 0225 connect(m_grabRectFrame, SIGNAL(getColor()), this, SLOT(slotGetAverageColor())); 0226 m_grabRectFrame->hide(); 0227 } 0228 } 0229 QWidget::mouseReleaseEvent(event); 0230 } 0231 0232 void ColorPickerWidget::mouseMoveEvent(QMouseEvent *event) 0233 { 0234 // Draw live rectangle of current color under mouse 0235 m_mouseColor = grabColor(QCursor::pos(), true); 0236 update(); 0237 if (m_filterActive && !m_clickPoint.isNull()) { 0238 m_grabRect.setWidth(event->globalX() - m_grabRect.x()); 0239 m_grabRect.setHeight(event->globalY() - m_grabRect.y()); 0240 m_grabRectFrame->setGeometry(m_grabRect.normalized()); 0241 } 0242 } 0243 0244 void ColorPickerWidget::slotSetupEventFilter() 0245 { 0246 Q_EMIT disableCurrentFilter(true); 0247 m_filterActive = true; 0248 setFocus(); 0249 installEventFilter(this); 0250 grabMouse(QCursor(QIcon::fromTheme(QStringLiteral("color-picker")).pixmap(32, 32), 4, 28)); 0251 grabKeyboard(); 0252 } 0253 0254 void ColorPickerWidget::closeEventFilter() 0255 { 0256 m_filterActive = false; 0257 releaseMouse(); 0258 releaseKeyboard(); 0259 removeEventFilter(this); 0260 } 0261 0262 bool ColorPickerWidget::eventFilter(QObject *object, QEvent *event) 0263 { 0264 // Close color picker on any key press 0265 if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) { 0266 closeEventFilter(); 0267 Q_EMIT disableCurrentFilter(false); 0268 event->setAccepted(true); 0269 return true; 0270 } 0271 return QObject::eventFilter(object, event); 0272 } 0273 0274 QColor ColorPickerWidget::grabColor(const QPoint &p, bool destroyImage) 0275 { 0276 #ifdef Q_WS_X11 0277 /* 0278 we use the X11 API directly in this case as we are not getting back a valid 0279 return from QPixmap::grabWindow in the case where the application is using 0280 an argb visual 0281 */ 0282 if (!QApplication::primaryScreen()->geometry().contains(p)) { 0283 return QColor(); 0284 } 0285 unsigned long xpixel; 0286 if (m_image == nullptr) { 0287 Window root = RootWindow(QX11Info::display(), QX11Info::appScreen()); 0288 m_image = XGetImage(QX11Info::display(), root, p.x(), p.y(), 1, 1, -1, ZPixmap); 0289 xpixel = XGetPixel(m_image, 0, 0); 0290 } else { 0291 xpixel = XGetPixel(m_image, p.x(), p.y()); 0292 } 0293 if (destroyImage) { 0294 XDestroyImage(m_image); 0295 m_image = 0; 0296 } 0297 XColor xcol; 0298 xcol.pixel = xpixel; 0299 xcol.flags = DoRed | DoGreen | DoBlue; 0300 XQueryColor(QX11Info::display(), DefaultColormap(QX11Info::display(), QX11Info::appScreen()), &xcol); 0301 return QColor::fromRgbF(xcol.red / 65535.0, xcol.green / 65535.0, xcol.blue / 65535.0); 0302 #else 0303 Q_UNUSED(destroyImage) 0304 if (m_image.isNull()) { 0305 for (QScreen *screen : QGuiApplication::screens()) { 0306 QRect screenRect = screen->geometry(); 0307 if (screenRect.contains(p)) { 0308 QPixmap pm = screen->grabWindow(0, p.x() - screenRect.x(), p.y() - screenRect.y(), 1, 1); 0309 QImage i = pm.toImage(); 0310 return i.pixel(0, 0); 0311 } 0312 } 0313 return qRgb(0, 0, 0); 0314 } 0315 return m_image.pixel(p.x(), p.y()); 0316 0317 #endif 0318 } 0319 0320 void ColorPickerWidget::grabColorDBus() 0321 { 0322 QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop"), QLatin1String("/org/freedesktop/portal/desktop"), 0323 QLatin1String("org.freedesktop.portal.Screenshot"), QLatin1String("PickColor")); 0324 message << QLatin1String("x11:") << QVariantMap{}; 0325 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); 0326 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall); 0327 Q_EMIT disableCurrentFilter(true); 0328 connect(watcher, &QDBusPendingCallWatcher::finished, [this](QDBusPendingCallWatcher *watcher) { 0329 QDBusPendingReply<QDBusObjectPath> reply = *watcher; 0330 Q_EMIT disableCurrentFilter(false); 0331 if (reply.isError()) { 0332 qWarning() << "Couldn't get reply"; 0333 qWarning() << "Error: " << reply.error().message(); 0334 } else { 0335 QDBusConnection::sessionBus().connect(QString(), reply.value().path(), QLatin1String("org.freedesktop.portal.Request"), QLatin1String("Response"), 0336 this, SLOT(gotColorResponse(uint, QVariantMap))); 0337 } 0338 }); 0339 } 0340 0341 void ColorPickerWidget::gotColorResponse(uint response, const QVariantMap &results) 0342 { 0343 if (!response) { 0344 if (results.contains(QLatin1String("color"))) { 0345 const QColor color = qdbus_cast<QColor>(results.value(QLatin1String("color"))); 0346 qDebug() << "picked" << color; 0347 m_mouseColor = color; 0348 Q_EMIT colorPicked(m_mouseColor); 0349 } 0350 } else { 0351 qWarning() << "Failed to take screenshot" << response << results; 0352 } 0353 }