// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "qpixeltool.h" #include #include #include #if QT_CONFIG(clipboard) #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; static constexpr auto settingsGroup = "QPixelTool"_L1; static constexpr auto organization = "QtProject"_L1; static constexpr auto autoUpdateKey = "autoUpdate"_L1; static constexpr auto gridSizeKey = "gridSize"_L1; static constexpr auto gridActiveKey = "gridActive"_L1; static constexpr auto zoomKey = "zoom"_L1; static constexpr auto initialSizeKey = "initialSize"_L1; static constexpr auto positionKey = "position"_L1; static constexpr auto lcdModeKey = "lcdMode"_L1; static QPoint initialPos(const QSettings &settings, QSize initialSize) { const QPoint defaultPos = QGuiApplication::primaryScreen()->availableGeometry().topLeft(); const QPoint savedPos = settings.value(positionKey, QVariant(defaultPos)).toPoint(); auto *savedScreen = QGuiApplication::screenAt(savedPos); return savedScreen != nullptr && savedScreen->availableGeometry().intersects(QRect(savedPos, initialSize)) ? savedPos : defaultPos; } QPixelTool::QPixelTool(QWidget *parent) : QWidget(parent) { setWindowTitle(QCoreApplication::applicationName()); QSettings settings(organization, settingsGroup); m_autoUpdate = settings.value(autoUpdateKey, 0).toBool(); m_gridSize = settings.value(gridSizeKey, 1).toInt(); m_gridActive = settings.value(gridActiveKey, 1).toInt(); m_zoom = settings.value(zoomKey, 4).toInt(); m_initialSize = settings.value(initialSizeKey, QSize(250, 200)).toSize(); m_lcdMode = settings.value(lcdModeKey, 0).toInt(); move(initialPos(settings, m_initialSize)); setMouseTracking(true); setAttribute(Qt::WA_OpaquePaintEvent); m_updateId = startTimer(30); } QPixelTool::~QPixelTool() { QSettings settings(organization, settingsGroup); settings.setValue(autoUpdateKey, int(m_autoUpdate)); settings.setValue(gridSizeKey, m_gridSize); settings.setValue(gridActiveKey, m_gridActive); settings.setValue(zoomKey, m_zoom); settings.setValue(initialSizeKey, size()); settings.setValue(positionKey, pos()); settings.setValue(lcdModeKey, m_lcdMode); } void QPixelTool::setPreviewImage(const QImage &image) { m_preview_mode = true; m_preview_image = image; m_freeze = true; } void QPixelTool::timerEvent(QTimerEvent *event) { if (event->timerId() == m_updateId && !m_freeze) { grabScreen(); } else if (event->timerId() == m_displayZoomId) { killTimer(m_displayZoomId); m_displayZoomId = 0; setZoomVisible(false); } else if (event->timerId() == m_displayGridSizeId) { killTimer(m_displayGridSizeId); m_displayGridSizeId = 0; m_displayGridSize = false; } } void render_string(QPainter *p, int w, int h, const QString &text, int flags) { p->setBrush(QColor(255, 255, 255, 191)); p->setPen(Qt::black); QRect bounds; p->drawText(0, 0, w, h, Qt::TextDontPrint | flags, text, &bounds); if (bounds.x() == 0) bounds.adjust(0, 0, 10, 0); else bounds.adjust(-10, 0, 0, 0); if (bounds.y() == 0) bounds.adjust(0, 0, 0, 10); else bounds.adjust(0, -10, 0, 0); p->drawRect(bounds); p->drawText(bounds, flags, text); } static QImage imageLCDFilter(const QImage &image, int lcdMode) { Q_ASSERT(lcdMode > 0 && lcdMode < 5); const bool vertical = (lcdMode > 2); QImage scaled(image.width() * (vertical ? 1 : 3), image.height() * (vertical ? 3 : 1), image.format()); const int w = image.width(); const int h = image.height(); if (!vertical) { for (int y = 0; y < h; ++y) { const QRgb *in = reinterpret_cast(image.scanLine(y)); QRgb *out = reinterpret_cast(scaled.scanLine(y)); if (lcdMode == 1) { for (int x = 0; x < w; ++x) { *out++ = in[x] & 0xffff0000; *out++ = in[x] & 0xff00ff00; *out++ = in[x] & 0xff0000ff; } } else { for (int x = 0; x < w; ++x) { *out++ = in[x] & 0xff0000ff; *out++ = in[x] & 0xff00ff00; *out++ = in[x] & 0xffff0000; } } } } else { for (int y = 0; y < h; ++y) { const QRgb *in = reinterpret_cast(image.scanLine(y)); QRgb *out1 = reinterpret_cast(scaled.scanLine(y * 3 + 0)); QRgb *out2 = reinterpret_cast(scaled.scanLine(y * 3 + 1)); QRgb *out3 = reinterpret_cast(scaled.scanLine(y * 3 + 2)); if (lcdMode == 2) { for (int x = 0; x < w; ++x) { out1[x] = in[x] & 0xffff0000; out2[x] = in[x] & 0xff00ff00; out3[x] = in[x] & 0xff0000ff; } } else { for (int x = 0; x < w; ++x) { out1[x] = in[x] & 0xff0000ff; out2[x] = in[x] & 0xff00ff00; out3[x] = in[x] & 0xffff0000; } } } } return scaled; } void QPixelTool::paintEvent(QPaintEvent *) { QPainter p(this); if (m_preview_mode) { QPixmap pixmap(40, 40); QPainter pt(&pixmap); pt.fillRect(0, 0, 20, 20, Qt::white); pt.fillRect(20, 20, 20, 20, Qt::white); pt.fillRect(20, 0, 20, 20, Qt::lightGray); pt.fillRect(0, 20, 20, 20, Qt::lightGray); pt.end(); p.fillRect(0, 0, width(), height(), pixmap); } int w = width(); int h = height(); p.save(); if (m_lcdMode == 0) { p.scale(m_zoom, m_zoom); p.drawPixmap(0, 0, m_buffer); } else { if (m_lcdMode <= 2) p.scale(m_zoom / 3.0, m_zoom); else p.scale(m_zoom, m_zoom / 3.0); p.drawImage(0, 0, imageLCDFilter(m_buffer.toImage(), m_lcdMode)); } p.restore(); // Draw the grid on top. if (m_gridActive) { p.setPen(m_gridActive == 1 ? Qt::black : Qt::white); int incr = m_gridSize * m_zoom; if (m_lcdMode == 0 || m_lcdMode > 2) { for (int x=0; x> 24, (0x00ff0000 & m_currentColor) >> 16, (0x0000ff00 & m_currentColor) >> 8, (0x000000ff & m_currentColor)); render_string(&p, w, h, str, Qt::AlignBottom | Qt::AlignRight); } if (m_mouseDown && m_dragStart != m_dragCurrent) { int x1 = (m_dragStart.x() / m_zoom) * m_zoom; int y1 = (m_dragStart.y() / m_zoom) * m_zoom; int x2 = (m_dragCurrent.x() / m_zoom) * m_zoom; int y2 = (m_dragCurrent.y() / m_zoom) * m_zoom; QRect r = QRect(x1, y1, x2 - x1, y2 - y1).normalized(); p.setBrush(Qt::NoBrush); p.setPen(QPen(Qt::red, 3, Qt::SolidLine)); p.drawRect(r); p.setPen(QPen(Qt::black, 1, Qt::SolidLine)); p.drawRect(r); QString str = QString::asprintf("Rect: x=%d, y=%d, w=%d, h=%d", r.x() / m_zoom, r.y() / m_zoom, r.width() / m_zoom, r.height() / m_zoom); render_string(&p, w, h, str, Qt::AlignBottom | Qt::AlignLeft); } } void QPixelTool::keyPressEvent(QKeyEvent *e) { switch (e->key()) { case Qt::Key_Space: toggleFreeze(); break; case Qt::Key_Plus: increaseZoom(); break; case Qt::Key_Minus: decreaseZoom(); break; case Qt::Key_PageUp: setGridSize(m_gridSize + 1); break; case Qt::Key_PageDown: setGridSize(m_gridSize - 1); break; case Qt::Key_G: toggleGrid(); break; case Qt::Key_A: m_autoUpdate = !m_autoUpdate; break; #if QT_CONFIG(clipboard) case Qt::Key_C: if (e->modifiers().testFlag(Qt::ControlModifier)) copyToClipboard(); else copyColorToClipboard(); break; #endif // QT_CONFIG(clipboard) case Qt::Key_S: if (e->modifiers().testFlag(Qt::ControlModifier)) { releaseKeyboard(); saveToFile(); } break; case Qt::Key_Control: grabKeyboard(); break; case Qt::Key_F1: aboutPixelTool(); break; } } void QPixelTool::keyReleaseEvent(QKeyEvent *e) { switch(e->key()) { case Qt::Key_Control: releaseKeyboard(); break; default: break; } } void QPixelTool::resizeEvent(QResizeEvent *) { grabScreen(); } void QPixelTool::mouseMoveEvent(QMouseEvent *e) { if (m_mouseDown) m_dragCurrent = e->pos(); const auto pos = e->position().toPoint(); const int x = pos.x() / m_zoom; const int y = pos.y() / m_zoom; QImage im = m_buffer.toImage().convertToFormat(QImage::Format_ARGB32); if (x < im.width() && y < im.height() && x >= 0 && y >= 0) { m_currentColor = im.pixel(x, y); update(); } } void QPixelTool::mousePressEvent(QMouseEvent *e) { if (!m_freeze) return; m_mouseDown = true; m_dragStart = e->pos(); } void QPixelTool::mouseReleaseEvent(QMouseEvent *) { m_mouseDown = false; } static QAction *addCheckableAction(QMenu &menu, const QString &title, bool value, const QKeySequence &key) { QAction *result = menu.addAction(title); result->setCheckable(true); result->setChecked(value); result->setShortcut(key); return result; } static QAction *addCheckableAction(QMenu &menu, const QString &title, bool value, const QKeySequence &key, QActionGroup *group) { QAction *result = addCheckableAction(menu, title, value, key); result->setActionGroup(group); return result; } void QPixelTool::contextMenuEvent(QContextMenuEvent *e) { const bool tmpFreeze = m_freeze; m_freeze = true; QMenu menu; menu.addAction("Qt Pixel Zooming Tool"_L1)->setEnabled(false); menu.addSeparator(); // Grid color options... auto *gridGroup = new QActionGroup(&menu); addCheckableAction(menu, "White grid"_L1, m_gridActive == 2, Qt::Key_W, gridGroup); QAction *blackGrid = addCheckableAction(menu, "Black grid"_L1, m_gridActive == 1, Qt::Key_B, gridGroup); QAction *noGrid = addCheckableAction(menu, "No grid"_L1, m_gridActive == 0, Qt::Key_N, gridGroup); menu.addSeparator(); // Grid size options menu.addAction("Increase grid size"_L1, Qt::Key_PageUp, this, &QPixelTool::increaseGridSize); menu.addAction("Decrease grid size"_L1, Qt::Key_PageDown, this, &QPixelTool::decreaseGridSize); menu.addSeparator(); auto *lcdGroup = new QActionGroup(&menu); addCheckableAction(menu, "No subpixels"_L1, m_lcdMode == 0, QKeySequence(), lcdGroup); QAction *rgbPixels = addCheckableAction(menu, "RGB subpixels"_L1, m_lcdMode == 1, QKeySequence(), lcdGroup); QAction *bgrPixels = addCheckableAction(menu, "BGR subpixels"_L1, m_lcdMode == 2, QKeySequence(), lcdGroup); QAction *vrgbPixels = addCheckableAction(menu, "VRGB subpixels"_L1, m_lcdMode == 3, QKeySequence(), lcdGroup); QAction *vbgrPixels = addCheckableAction(menu, "VBGR subpixels"_L1, m_lcdMode == 4, QKeySequence(), lcdGroup); menu.addSeparator(); // Zoom options menu.addAction("Zoom in"_L1, Qt::Key_Plus, this, &QPixelTool::increaseZoom); menu.addAction("Zoom out"_L1, Qt::Key_Minus, this, &QPixelTool::decreaseZoom); menu.addSeparator(); // Freeze / Autoupdate QAction *freeze = addCheckableAction(menu, "Frozen"_L1, tmpFreeze, Qt::Key_Space); QAction *autoUpdate = addCheckableAction(menu, "Continuous update"_L1, m_autoUpdate, Qt::Key_A); menu.addSeparator(); // Copy to clipboard / save menu.addAction("Save as image..."_L1, QKeySequence::SaveAs, this, &QPixelTool::saveToFile); #if QT_CONFIG(clipboard) menu.addAction("Copy to clipboard"_L1, QKeySequence::Copy, this, &QPixelTool::copyToClipboard); menu.addAction("Copy color value to clipboard"_L1, Qt::Key_C, this, &QPixelTool::copyColorToClipboard); #endif // QT_CONFIG(clipboard) menu.addSeparator(); menu.addAction("About Qt"_L1, qApp, &QApplication::aboutQt); menu.addAction("About Qt Pixeltool"_L1, this, &QPixelTool::aboutPixelTool); menu.exec(mapToGlobal(e->pos())); // Read out grid settings if (noGrid->isChecked()) m_gridActive = 0; else if (blackGrid->isChecked()) m_gridActive = 1; else m_gridActive = 2; // Read out lcd settings if (rgbPixels->isChecked()) m_lcdMode = 1; else if (bgrPixels->isChecked()) m_lcdMode = 2; else if (vrgbPixels->isChecked()) m_lcdMode = 3; else if (vbgrPixels->isChecked()) m_lcdMode = 4; else m_lcdMode = 0; m_autoUpdate = autoUpdate->isChecked(); m_freeze = freeze->isChecked(); // LCD mode looks off unless zoom is dividable by 3 if (m_lcdMode && (m_zoom % 3) != 0) setZoom(qMax(3, (m_zoom + 1) / 3)); } QSize QPixelTool::sizeHint() const { return m_initialSize; } static inline QString pixelToolTitle(QPoint pos, const QScreen *screen, const QColor ¤tColor) { if (screen != nullptr) pos = QHighDpi::toNativePixels(pos, screen); return QCoreApplication::applicationName() + " ["_L1 + QString::number(pos.x()) + ", "_L1 + QString::number(pos.y()) + "] "_L1 + currentColor.name(); } void QPixelTool::grabScreen() { if (m_preview_mode) { int w = qMin(width() / m_zoom + 1, m_preview_image.width()); int h = qMin(height() / m_zoom + 1, m_preview_image.height()); m_buffer = QPixmap::fromImage(m_preview_image).copy(0, 0, w, h); update(); return; } QPoint mousePos = QCursor::pos(); if (mousePos == m_lastMousePos && !m_autoUpdate) return; QScreen *screen = QGuiApplication::screenAt(mousePos); if (m_lastMousePos != mousePos) setWindowTitle(pixelToolTitle(mousePos, screen, m_currentColor)); const auto widgetDpr = devicePixelRatioF(); const auto screenDpr = screen != nullptr ? screen->devicePixelRatio() : widgetDpr; // When grabbing from another screen, we grab an area fitting our size using our DPR. const auto factor = widgetDpr / screenDpr / qreal(m_zoom); const QSize size{int(std::ceil(width() * factor)), int(std::ceil(height() * factor))}; const QPoint pos = mousePos - QPoint{size.width(), size.height()} / 2; const QBrush darkBrush = palette().color(QPalette::Dark); if (screen != nullptr) { const QPoint screenPos = pos - screen->geometry().topLeft(); m_buffer = screen->grabWindow(0, screenPos.x(), screenPos.y(), size.width(), size.height()); } else { m_buffer = QPixmap(size); m_buffer.fill(darkBrush.color()); } m_buffer.setDevicePixelRatio(widgetDpr); QRegion geom(QRect{pos, size}); QRect screenRect; const auto screens = QGuiApplication::screens(); for (auto *screen : screens) screenRect |= screen->geometry(); geom -= screenRect; const auto rectsInRegion = geom.rectCount(); if (!geom.isEmpty()) { QPainter p(&m_buffer); p.translate(-pos); p.setPen(Qt::NoPen); p.setBrush(darkBrush); p.drawRects(geom.begin(), rectsInRegion); } update(); m_currentColor = m_buffer.toImage().pixel(m_buffer.rect().center()); m_lastMousePos = mousePos; } void QPixelTool::startZoomVisibleTimer() { if (m_displayZoomId > 0) killTimer(m_displayZoomId); m_displayZoomId = startTimer(5000); setZoomVisible(true); } void QPixelTool::startGridSizeVisibleTimer() { if (m_gridActive) { if (m_displayGridSizeId > 0) killTimer(m_displayGridSizeId); m_displayGridSizeId = startTimer(5000); m_displayGridSize = true; update(); } } void QPixelTool::setZoomVisible(bool visible) { m_displayZoom = visible; update(); } void QPixelTool::toggleFreeze() { m_freeze = !m_freeze; if (!m_freeze) m_dragStart = m_dragCurrent = QPoint(); } void QPixelTool::increaseZoom() { if (!m_lcdMode) setZoom(m_zoom + 1); else setZoom(m_zoom + 3); } void QPixelTool::decreaseZoom() { if (!m_lcdMode) setZoom(m_zoom - 1); else setZoom(m_zoom - 3); } void QPixelTool::setZoom(int zoom) { if (zoom > 0) { QPoint pos = m_lastMousePos; m_lastMousePos = QPoint(); m_zoom = zoom; grabScreen(); m_lastMousePos = pos; m_dragStart = m_dragCurrent = QPoint(); startZoomVisibleTimer(); } } void QPixelTool::toggleGrid() { if (++m_gridActive > 2) m_gridActive = 0; update(); } void QPixelTool::setGridSize(int gridSize) { if (m_gridActive && gridSize > 0) { m_gridSize = gridSize; startGridSizeVisibleTimer(); update(); } } #if QT_CONFIG(clipboard) void QPixelTool::copyToClipboard() { QGuiApplication::clipboard()->setPixmap(m_buffer); } void QPixelTool::copyColorToClipboard() { QGuiApplication::clipboard()->setText(QColor(m_currentColor).name()); } #endif // QT_CONFIG(clipboard) void QPixelTool::saveToFile() { bool oldFreeze = m_freeze; m_freeze = true; QFileDialog fileDialog(this); fileDialog.setWindowTitle("Save as image"_L1); fileDialog.setAcceptMode(QFileDialog::AcceptSave); fileDialog.setDirectory(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); QStringList mimeTypes; const QByteArrayList supportedMimeTypes = QImageWriter::supportedMimeTypes(); for (const QByteArray &mimeTypeB : supportedMimeTypes) mimeTypes.append(QString::fromLatin1(mimeTypeB)); fileDialog.setMimeTypeFilters(mimeTypes); const QString pngType = "image/png"_L1; if (mimeTypes.contains(pngType)) { fileDialog.selectMimeTypeFilter(pngType); fileDialog.setDefaultSuffix("png"_L1); } while (fileDialog.exec() == QDialog::Accepted && !m_buffer.save(fileDialog.selectedFiles().constFirst())) { QMessageBox::warning(this, "Unable to write image"_L1, "Unable to write "_L1 + QDir::toNativeSeparators(fileDialog.selectedFiles().constFirst())); } m_freeze = oldFreeze; } QTextStream &operator<<(QTextStream &str, const QScreen *screen) { const QRect geometry = screen->geometry(); str << '"' << screen->name() << "\" " << geometry.width() << 'x' << geometry.height() << Qt::forcesign << geometry.x() << geometry.y() << Qt::noforcesign << ", " << qRound(screen->logicalDotsPerInch()) << "DPI" << ", Depth: " << screen->depth() << ", " << screen->refreshRate() << "Hz"; const qreal dpr = screen->devicePixelRatio(); if (!qFuzzyCompare(dpr, qreal(1))) str << ", DPR: " << dpr; return str; } QString QPixelTool::aboutText() const { const QList screens = QGuiApplication::screens(); const QScreen *windowScreen = windowHandle()->screen(); QString result; QTextStream str(&result); str << "

Qt Pixeltool

Qt " << QT_VERSION_STR << "

Copyright (C) 2017 The Qt Company Ltd.

Screens

    "; for (const QScreen *screen : screens) str << "
  • " << (screen == windowScreen ? "* " : " ") << screen << "
  • "; str << "
"; return result; } void QPixelTool::aboutPixelTool() { QMessageBox aboutBox(QMessageBox::Information, tr("About Qt Pixeltool"), aboutText(), QMessageBox::Close, this); aboutBox.setWindowFlags(aboutBox.windowFlags() & ~Qt::WindowContextHelpButtonHint); aboutBox.setTextInteractionFlags(Qt::TextBrowserInteraction); aboutBox.exec(); } QT_END_NAMESPACE