| 1 | // Copyright (C) 2016 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
| 3 | |
| 4 | #include "qpixeltool.h" |
| 5 | |
| 6 | #include <qapplication.h> |
| 7 | #include <qdir.h> |
| 8 | #include <qscreen.h> |
| 9 | #if QT_CONFIG(clipboard) |
| 10 | #include <qclipboard.h> |
| 11 | #endif |
| 12 | #include <qpainter.h> |
| 13 | #include <qevent.h> |
| 14 | #include <qfiledialog.h> |
| 15 | #include <qmessagebox.h> |
| 16 | #include <qsettings.h> |
| 17 | #include <qmenu.h> |
| 18 | #include <qactiongroup.h> |
| 19 | #include <qimagewriter.h> |
| 20 | #include <qstandardpaths.h> |
| 21 | #include <qtextstream.h> |
| 22 | #include <qwindow.h> |
| 23 | #include <qmetaobject.h> |
| 24 | #include <private/qhighdpiscaling_p.h> |
| 25 | |
| 26 | #include <qdebug.h> |
| 27 | |
| 28 | #include <cmath> |
| 29 | |
| 30 | QT_BEGIN_NAMESPACE |
| 31 | |
| 32 | using namespace Qt::StringLiterals; |
| 33 | |
| 34 | static constexpr auto settingsGroup = "QPixelTool"_L1 ; |
| 35 | static constexpr auto organization = "QtProject"_L1 ; |
| 36 | static constexpr auto autoUpdateKey = "autoUpdate"_L1 ; |
| 37 | static constexpr auto gridSizeKey = "gridSize"_L1 ; |
| 38 | static constexpr auto gridActiveKey = "gridActive"_L1 ; |
| 39 | static constexpr auto zoomKey = "zoom"_L1 ; |
| 40 | static constexpr auto initialSizeKey = "initialSize"_L1 ; |
| 41 | static constexpr auto positionKey = "position"_L1 ; |
| 42 | static constexpr auto lcdModeKey = "lcdMode"_L1 ; |
| 43 | |
| 44 | static QPoint initialPos(const QSettings &settings, QSize initialSize) |
| 45 | { |
| 46 | const QPoint defaultPos = QGuiApplication::primaryScreen()->availableGeometry().topLeft(); |
| 47 | const QPoint savedPos = |
| 48 | settings.value(key: positionKey, defaultValue: QVariant(defaultPos)).toPoint(); |
| 49 | auto *savedScreen = QGuiApplication::screenAt(point: savedPos); |
| 50 | return savedScreen != nullptr |
| 51 | && savedScreen->availableGeometry().intersects(r: QRect(savedPos, initialSize)) |
| 52 | ? savedPos : defaultPos; |
| 53 | } |
| 54 | |
| 55 | QPixelTool::QPixelTool(QWidget *parent) |
| 56 | : QWidget(parent) |
| 57 | { |
| 58 | setWindowTitle(QCoreApplication::applicationName()); |
| 59 | QSettings settings(organization, settingsGroup); |
| 60 | m_autoUpdate = settings.value(key: autoUpdateKey, defaultValue: 0).toBool(); |
| 61 | m_gridSize = settings.value(key: gridSizeKey, defaultValue: 1).toInt(); |
| 62 | m_gridActive = settings.value(key: gridActiveKey, defaultValue: 1).toInt(); |
| 63 | m_zoom = settings.value(key: zoomKey, defaultValue: 4).toInt(); |
| 64 | m_initialSize = settings.value(key: initialSizeKey, defaultValue: QSize(250, 200)).toSize(); |
| 65 | m_lcdMode = settings.value(key: lcdModeKey, defaultValue: 0).toInt(); |
| 66 | |
| 67 | move(initialPos(settings, initialSize: m_initialSize)); |
| 68 | |
| 69 | setMouseTracking(true); |
| 70 | setAttribute(Qt::WA_OpaquePaintEvent); |
| 71 | m_updateId = startTimer(interval: 30); |
| 72 | } |
| 73 | |
| 74 | QPixelTool::~QPixelTool() |
| 75 | { |
| 76 | QSettings settings(organization, settingsGroup); |
| 77 | settings.setValue(key: autoUpdateKey, value: int(m_autoUpdate)); |
| 78 | settings.setValue(key: gridSizeKey, value: m_gridSize); |
| 79 | settings.setValue(key: gridActiveKey, value: m_gridActive); |
| 80 | settings.setValue(key: zoomKey, value: m_zoom); |
| 81 | settings.setValue(key: initialSizeKey, value: size()); |
| 82 | settings.setValue(key: positionKey, value: pos()); |
| 83 | settings.setValue(key: lcdModeKey, value: m_lcdMode); |
| 84 | } |
| 85 | |
| 86 | void QPixelTool::setPreviewImage(const QImage &image) |
| 87 | { |
| 88 | m_preview_mode = true; |
| 89 | m_preview_image = image; |
| 90 | m_freeze = true; |
| 91 | } |
| 92 | |
| 93 | void QPixelTool::timerEvent(QTimerEvent *event) |
| 94 | { |
| 95 | if (event->timerId() == m_updateId && !m_freeze) { |
| 96 | grabScreen(); |
| 97 | } else if (event->timerId() == m_displayZoomId) { |
| 98 | killTimer(id: m_displayZoomId); |
| 99 | m_displayZoomId = 0; |
| 100 | setZoomVisible(false); |
| 101 | } else if (event->timerId() == m_displayGridSizeId) { |
| 102 | killTimer(id: m_displayGridSizeId); |
| 103 | m_displayGridSizeId = 0; |
| 104 | m_displayGridSize = false; |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | void render_string(QPainter *p, int w, int h, const QString &text, int flags) |
| 109 | { |
| 110 | p->setBrush(QColor(255, 255, 255, 191)); |
| 111 | p->setPen(Qt::black); |
| 112 | QRect bounds; |
| 113 | p->drawText(x: 0, y: 0, w, h, flags: Qt::TextDontPrint | flags, str: text, br: &bounds); |
| 114 | |
| 115 | if (bounds.x() == 0) bounds.adjust(dx1: 0, dy1: 0, dx2: 10, dy2: 0); |
| 116 | else bounds.adjust(dx1: -10, dy1: 0, dx2: 0, dy2: 0); |
| 117 | |
| 118 | if (bounds.y() == 0) bounds.adjust(dx1: 0, dy1: 0, dx2: 0, dy2: 10); |
| 119 | else bounds.adjust(dx1: 0, dy1: -10, dx2: 0, dy2: 0); |
| 120 | |
| 121 | p->drawRect(r: bounds); |
| 122 | p->drawText(r: bounds, flags, text); |
| 123 | } |
| 124 | |
| 125 | static QImage imageLCDFilter(const QImage &image, int lcdMode) |
| 126 | { |
| 127 | Q_ASSERT(lcdMode > 0 && lcdMode < 5); |
| 128 | const bool vertical = (lcdMode > 2); |
| 129 | QImage scaled(image.width() * (vertical ? 1 : 3), |
| 130 | image.height() * (vertical ? 3 : 1), |
| 131 | image.format()); |
| 132 | |
| 133 | const int w = image.width(); |
| 134 | const int h = image.height(); |
| 135 | if (!vertical) { |
| 136 | for (int y = 0; y < h; ++y) { |
| 137 | const QRgb *in = reinterpret_cast<const QRgb *>(image.scanLine(y)); |
| 138 | QRgb *out = reinterpret_cast<QRgb *>(scaled.scanLine(y)); |
| 139 | if (lcdMode == 1) { |
| 140 | for (int x = 0; x < w; ++x) { |
| 141 | *out++ = in[x] & 0xffff0000; |
| 142 | *out++ = in[x] & 0xff00ff00; |
| 143 | *out++ = in[x] & 0xff0000ff; |
| 144 | } |
| 145 | } else { |
| 146 | for (int x = 0; x < w; ++x) { |
| 147 | *out++ = in[x] & 0xff0000ff; |
| 148 | *out++ = in[x] & 0xff00ff00; |
| 149 | *out++ = in[x] & 0xffff0000; |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | } else { |
| 154 | for (int y = 0; y < h; ++y) { |
| 155 | const QRgb *in = reinterpret_cast<const QRgb *>(image.scanLine(y)); |
| 156 | QRgb *out1 = reinterpret_cast<QRgb *>(scaled.scanLine(y * 3 + 0)); |
| 157 | QRgb *out2 = reinterpret_cast<QRgb *>(scaled.scanLine(y * 3 + 1)); |
| 158 | QRgb *out3 = reinterpret_cast<QRgb *>(scaled.scanLine(y * 3 + 2)); |
| 159 | if (lcdMode == 2) { |
| 160 | for (int x = 0; x < w; ++x) { |
| 161 | out1[x] = in[x] & 0xffff0000; |
| 162 | out2[x] = in[x] & 0xff00ff00; |
| 163 | out3[x] = in[x] & 0xff0000ff; |
| 164 | } |
| 165 | } else { |
| 166 | for (int x = 0; x < w; ++x) { |
| 167 | out1[x] = in[x] & 0xff0000ff; |
| 168 | out2[x] = in[x] & 0xff00ff00; |
| 169 | out3[x] = in[x] & 0xffff0000; |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | } |
| 174 | return scaled; |
| 175 | } |
| 176 | |
| 177 | void QPixelTool::paintEvent(QPaintEvent *) |
| 178 | { |
| 179 | QPainter p(this); |
| 180 | |
| 181 | if (m_preview_mode) { |
| 182 | QPixmap pixmap(40, 40); |
| 183 | QPainter pt(&pixmap); |
| 184 | pt.fillRect(x: 0, y: 0, w: 20, h: 20, c: Qt::white); |
| 185 | pt.fillRect(x: 20, y: 20, w: 20, h: 20, c: Qt::white); |
| 186 | pt.fillRect(x: 20, y: 0, w: 20, h: 20, c: Qt::lightGray); |
| 187 | pt.fillRect(x: 0, y: 20, w: 20, h: 20, c: Qt::lightGray); |
| 188 | pt.end(); |
| 189 | p.fillRect(x: 0, y: 0, w: width(), h: height(), b: pixmap); |
| 190 | } |
| 191 | |
| 192 | int w = width(); |
| 193 | int h = height(); |
| 194 | |
| 195 | p.save(); |
| 196 | if (m_lcdMode == 0) { |
| 197 | p.scale(sx: m_zoom, sy: m_zoom); |
| 198 | p.drawPixmap(x: 0, y: 0, pm: m_buffer); |
| 199 | } else { |
| 200 | if (m_lcdMode <= 2) |
| 201 | p.scale(sx: m_zoom / 3.0, sy: m_zoom); |
| 202 | else |
| 203 | p.scale(sx: m_zoom, sy: m_zoom / 3.0); |
| 204 | p.drawImage(x: 0, y: 0, image: imageLCDFilter(image: m_buffer.toImage(), lcdMode: m_lcdMode)); |
| 205 | } |
| 206 | p.restore(); |
| 207 | |
| 208 | // Draw the grid on top. |
| 209 | if (m_gridActive) { |
| 210 | p.setPen(m_gridActive == 1 ? Qt::black : Qt::white); |
| 211 | int incr = m_gridSize * m_zoom; |
| 212 | if (m_lcdMode == 0 || m_lcdMode > 2) { |
| 213 | for (int x=0; x<w; x+=incr) |
| 214 | p.drawLine(x1: x, y1: 0, x2: x, y2: h); |
| 215 | } |
| 216 | if (m_lcdMode <= 2) { |
| 217 | for (int y=0; y<h; y+=incr) |
| 218 | p.drawLine(x1: 0, y1: y, x2: w, y2: y); |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | QFont f(QStringList{u"courier"_s }, -1, QFont::Bold); |
| 223 | p.setFont(f); |
| 224 | |
| 225 | if (m_displayZoom) { |
| 226 | render_string(p: &p, w, h, |
| 227 | text: "Zoom: x"_L1 + QString::number(m_zoom), |
| 228 | flags: Qt::AlignTop | Qt::AlignRight); |
| 229 | } |
| 230 | |
| 231 | if (m_displayGridSize) { |
| 232 | render_string(p: &p, w, h, |
| 233 | text: "Grid size: "_L1 + QString::number(m_gridSize), |
| 234 | flags: Qt::AlignBottom | Qt::AlignLeft); |
| 235 | } |
| 236 | |
| 237 | if (m_freeze) { |
| 238 | QString str = QString::asprintf(format: "%8X (%3d,%3d,%3d,%3d)" , |
| 239 | m_currentColor, |
| 240 | (0xff000000 & m_currentColor) >> 24, |
| 241 | (0x00ff0000 & m_currentColor) >> 16, |
| 242 | (0x0000ff00 & m_currentColor) >> 8, |
| 243 | (0x000000ff & m_currentColor)); |
| 244 | render_string(p: &p, w, h, |
| 245 | text: str, |
| 246 | flags: Qt::AlignBottom | Qt::AlignRight); |
| 247 | } |
| 248 | |
| 249 | if (m_mouseDown && m_dragStart != m_dragCurrent) { |
| 250 | int x1 = (m_dragStart.x() / m_zoom) * m_zoom; |
| 251 | int y1 = (m_dragStart.y() / m_zoom) * m_zoom; |
| 252 | int x2 = (m_dragCurrent.x() / m_zoom) * m_zoom; |
| 253 | int y2 = (m_dragCurrent.y() / m_zoom) * m_zoom; |
| 254 | QRect r = QRect(x1, y1, x2 - x1, y2 - y1).normalized(); |
| 255 | p.setBrush(Qt::NoBrush); |
| 256 | p.setPen(QPen(Qt::red, 3, Qt::SolidLine)); |
| 257 | p.drawRect(r); |
| 258 | p.setPen(QPen(Qt::black, 1, Qt::SolidLine)); |
| 259 | p.drawRect(r); |
| 260 | |
| 261 | QString str = QString::asprintf(format: "Rect: x=%d, y=%d, w=%d, h=%d" , |
| 262 | r.x() / m_zoom, |
| 263 | r.y() / m_zoom, |
| 264 | r.width() / m_zoom, |
| 265 | r.height() / m_zoom); |
| 266 | render_string(p: &p, w, h, text: str, flags: Qt::AlignBottom | Qt::AlignLeft); |
| 267 | } |
| 268 | |
| 269 | |
| 270 | } |
| 271 | |
| 272 | void QPixelTool::keyPressEvent(QKeyEvent *e) |
| 273 | { |
| 274 | switch (e->key()) { |
| 275 | case Qt::Key_Space: |
| 276 | toggleFreeze(); |
| 277 | break; |
| 278 | case Qt::Key_Plus: |
| 279 | increaseZoom(); |
| 280 | break; |
| 281 | case Qt::Key_Minus: |
| 282 | decreaseZoom(); |
| 283 | break; |
| 284 | case Qt::Key_PageUp: |
| 285 | setGridSize(m_gridSize + 1); |
| 286 | break; |
| 287 | case Qt::Key_PageDown: |
| 288 | setGridSize(m_gridSize - 1); |
| 289 | break; |
| 290 | case Qt::Key_G: |
| 291 | toggleGrid(); |
| 292 | break; |
| 293 | case Qt::Key_A: |
| 294 | m_autoUpdate = !m_autoUpdate; |
| 295 | break; |
| 296 | #if QT_CONFIG(clipboard) |
| 297 | case Qt::Key_C: |
| 298 | if (e->modifiers().testFlag(flag: Qt::ControlModifier)) |
| 299 | copyToClipboard(); |
| 300 | else |
| 301 | copyColorToClipboard(); |
| 302 | break; |
| 303 | #endif // QT_CONFIG(clipboard) |
| 304 | case Qt::Key_S: |
| 305 | if (e->modifiers().testFlag(flag: Qt::ControlModifier)) { |
| 306 | releaseKeyboard(); |
| 307 | saveToFile(); |
| 308 | } |
| 309 | break; |
| 310 | case Qt::Key_Control: |
| 311 | grabKeyboard(); |
| 312 | break; |
| 313 | case Qt::Key_F1: |
| 314 | aboutPixelTool(); |
| 315 | break; |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | void QPixelTool::keyReleaseEvent(QKeyEvent *e) |
| 320 | { |
| 321 | switch(e->key()) { |
| 322 | case Qt::Key_Control: |
| 323 | releaseKeyboard(); |
| 324 | break; |
| 325 | default: |
| 326 | break; |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | void QPixelTool::resizeEvent(QResizeEvent *) |
| 331 | { |
| 332 | grabScreen(); |
| 333 | } |
| 334 | |
| 335 | void QPixelTool::mouseMoveEvent(QMouseEvent *e) |
| 336 | { |
| 337 | if (m_mouseDown) |
| 338 | m_dragCurrent = e->pos(); |
| 339 | |
| 340 | const auto pos = e->position().toPoint(); |
| 341 | const int x = pos.x() / m_zoom; |
| 342 | const int y = pos.y() / m_zoom; |
| 343 | |
| 344 | QImage im = m_buffer.toImage().convertToFormat(f: QImage::Format_ARGB32); |
| 345 | if (x < im.width() && y < im.height() && x >= 0 && y >= 0) { |
| 346 | m_currentColor = im.pixel(x, y); |
| 347 | update(); |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | void QPixelTool::mousePressEvent(QMouseEvent *e) |
| 352 | { |
| 353 | if (!m_freeze) |
| 354 | return; |
| 355 | m_mouseDown = true; |
| 356 | m_dragStart = e->pos(); |
| 357 | } |
| 358 | |
| 359 | void QPixelTool::mouseReleaseEvent(QMouseEvent *) |
| 360 | { |
| 361 | m_mouseDown = false; |
| 362 | } |
| 363 | |
| 364 | static QAction *(QMenu &, const QString &title, |
| 365 | bool value, const QKeySequence &key) |
| 366 | { |
| 367 | QAction *result = menu.addAction(text: title); |
| 368 | result->setCheckable(true); |
| 369 | result->setChecked(value); |
| 370 | result->setShortcut(key); |
| 371 | return result; |
| 372 | } |
| 373 | |
| 374 | static QAction *(QMenu &, const QString &title, |
| 375 | bool value, const QKeySequence &key, |
| 376 | QActionGroup *group) |
| 377 | { |
| 378 | QAction *result = addCheckableAction(menu, title, value, key); |
| 379 | result->setActionGroup(group); |
| 380 | return result; |
| 381 | } |
| 382 | |
| 383 | void QPixelTool::(QContextMenuEvent *e) |
| 384 | { |
| 385 | const bool tmpFreeze = m_freeze; |
| 386 | m_freeze = true; |
| 387 | |
| 388 | QMenu ; |
| 389 | menu.addAction(text: "Qt Pixel Zooming Tool"_L1 )->setEnabled(false); |
| 390 | menu.addSeparator(); |
| 391 | |
| 392 | // Grid color options... |
| 393 | auto *gridGroup = new QActionGroup(&menu); |
| 394 | addCheckableAction(menu, title: "White grid"_L1 , value: m_gridActive == 2, |
| 395 | key: Qt::Key_W, group: gridGroup); |
| 396 | QAction *blackGrid = addCheckableAction(menu, title: "Black grid"_L1 , |
| 397 | value: m_gridActive == 1, key: Qt::Key_B, group: gridGroup); |
| 398 | QAction *noGrid = addCheckableAction(menu, title: "No grid"_L1 , value: m_gridActive == 0, |
| 399 | key: Qt::Key_N, group: gridGroup); |
| 400 | menu.addSeparator(); |
| 401 | |
| 402 | // Grid size options |
| 403 | menu.addAction(text: "Increase grid size"_L1 , shortcut: Qt::Key_PageUp, |
| 404 | args: this, args: &QPixelTool::increaseGridSize); |
| 405 | menu.addAction(text: "Decrease grid size"_L1 , shortcut: Qt::Key_PageDown, |
| 406 | args: this, args: &QPixelTool::decreaseGridSize); |
| 407 | menu.addSeparator(); |
| 408 | |
| 409 | auto *lcdGroup = new QActionGroup(&menu); |
| 410 | addCheckableAction(menu, title: "No subpixels"_L1 , value: m_lcdMode == 0, |
| 411 | key: QKeySequence(), group: lcdGroup); |
| 412 | QAction *rgbPixels = addCheckableAction(menu, title: "RGB subpixels"_L1 , |
| 413 | value: m_lcdMode == 1, key: QKeySequence(), group: lcdGroup); |
| 414 | QAction *bgrPixels = addCheckableAction(menu, title: "BGR subpixels"_L1 , |
| 415 | value: m_lcdMode == 2, key: QKeySequence(), group: lcdGroup); |
| 416 | QAction *vrgbPixels = addCheckableAction(menu, title: "VRGB subpixels"_L1 , |
| 417 | value: m_lcdMode == 3, key: QKeySequence(), group: lcdGroup); |
| 418 | QAction *vbgrPixels = addCheckableAction(menu, title: "VBGR subpixels"_L1 , |
| 419 | value: m_lcdMode == 4, key: QKeySequence(), group: lcdGroup); |
| 420 | menu.addSeparator(); |
| 421 | |
| 422 | // Zoom options |
| 423 | menu.addAction(text: "Zoom in"_L1 , shortcut: Qt::Key_Plus, |
| 424 | args: this, args: &QPixelTool::increaseZoom); |
| 425 | menu.addAction(text: "Zoom out"_L1 , shortcut: Qt::Key_Minus, |
| 426 | args: this, args: &QPixelTool::decreaseZoom); |
| 427 | menu.addSeparator(); |
| 428 | |
| 429 | // Freeze / Autoupdate |
| 430 | QAction *freeze = addCheckableAction(menu, title: "Frozen"_L1 , |
| 431 | value: tmpFreeze, key: Qt::Key_Space); |
| 432 | QAction *autoUpdate = addCheckableAction(menu, title: "Continuous update"_L1 , |
| 433 | value: m_autoUpdate, key: Qt::Key_A); |
| 434 | menu.addSeparator(); |
| 435 | |
| 436 | // Copy to clipboard / save |
| 437 | menu.addAction(text: "Save as image..."_L1 , shortcut: QKeySequence::SaveAs, |
| 438 | args: this, args: &QPixelTool::saveToFile); |
| 439 | #if QT_CONFIG(clipboard) |
| 440 | menu.addAction(text: "Copy to clipboard"_L1 , shortcut: QKeySequence::Copy, |
| 441 | args: this, args: &QPixelTool::copyToClipboard); |
| 442 | menu.addAction(text: "Copy color value to clipboard"_L1 , shortcut: Qt::Key_C, |
| 443 | args: this, args: &QPixelTool::copyColorToClipboard); |
| 444 | // Screen |
| 445 | menu.addSeparator(); |
| 446 | menu.addAction(text: "Copy screen shot to clipboard"_L1 , shortcut: QKeySequence(), |
| 447 | args: this, args: &QPixelTool::copyScreenShotToClipboard); |
| 448 | menu.addAction(text: "Copy screen info to clipboard"_L1 , shortcut: QKeySequence(), |
| 449 | args: this, args: &QPixelTool::copyScreenInfoToClipboard); |
| 450 | #endif // QT_CONFIG(clipboard) |
| 451 | |
| 452 | menu.addSeparator(); |
| 453 | menu.addAction(text: "About Qt"_L1 , qApp, args: &QApplication::aboutQt); |
| 454 | menu.addAction(text: "About Qt Pixeltool"_L1 , args: this, args: &QPixelTool::aboutPixelTool); |
| 455 | |
| 456 | menu.exec(pos: mapToGlobal(e->pos())); |
| 457 | |
| 458 | // Read out grid settings |
| 459 | if (noGrid->isChecked()) |
| 460 | m_gridActive = 0; |
| 461 | else if (blackGrid->isChecked()) |
| 462 | m_gridActive = 1; |
| 463 | else |
| 464 | m_gridActive = 2; |
| 465 | |
| 466 | // Read out lcd settings |
| 467 | if (rgbPixels->isChecked()) |
| 468 | m_lcdMode = 1; |
| 469 | else if (bgrPixels->isChecked()) |
| 470 | m_lcdMode = 2; |
| 471 | else if (vrgbPixels->isChecked()) |
| 472 | m_lcdMode = 3; |
| 473 | else if (vbgrPixels->isChecked()) |
| 474 | m_lcdMode = 4; |
| 475 | else |
| 476 | m_lcdMode = 0; |
| 477 | |
| 478 | m_autoUpdate = autoUpdate->isChecked(); |
| 479 | m_freeze = freeze->isChecked(); |
| 480 | |
| 481 | // LCD mode looks off unless zoom is dividable by 3 |
| 482 | if (m_lcdMode && (m_zoom % 3) != 0) |
| 483 | setZoom(qMax(a: 3, b: (m_zoom + 1) / 3)); |
| 484 | } |
| 485 | |
| 486 | QSize QPixelTool::sizeHint() const |
| 487 | { |
| 488 | return m_initialSize; |
| 489 | } |
| 490 | |
| 491 | static inline QString pixelToolTitle(QPoint pos, const QScreen *screen, const QColor ¤tColor) |
| 492 | { |
| 493 | if (screen != nullptr) |
| 494 | pos = QHighDpi::toNativePixels(value: pos, context: screen); |
| 495 | return QCoreApplication::applicationName() + " ["_L1 |
| 496 | + QString::number(pos.x()) |
| 497 | + ", "_L1 + QString::number(pos.y()) + "] "_L1 |
| 498 | + currentColor.name(); |
| 499 | } |
| 500 | |
| 501 | void QPixelTool::grabScreen() |
| 502 | { |
| 503 | if (m_preview_mode) { |
| 504 | int w = qMin(a: width() / m_zoom + 1, b: m_preview_image.width()); |
| 505 | int h = qMin(a: height() / m_zoom + 1, b: m_preview_image.height()); |
| 506 | m_buffer = QPixmap::fromImage(image: m_preview_image).copy(ax: 0, ay: 0, awidth: w, aheight: h); |
| 507 | update(); |
| 508 | return; |
| 509 | } |
| 510 | |
| 511 | QPoint mousePos = QCursor::pos(); |
| 512 | if (mousePos == m_lastMousePos && !m_autoUpdate) |
| 513 | return; |
| 514 | |
| 515 | QScreen *screen = QGuiApplication::screenAt(point: mousePos); |
| 516 | |
| 517 | if (m_lastMousePos != mousePos) |
| 518 | setWindowTitle(pixelToolTitle(pos: mousePos, screen, currentColor: m_currentColor)); |
| 519 | |
| 520 | const auto widgetDpr = devicePixelRatioF(); |
| 521 | const auto screenDpr = screen != nullptr ? screen->devicePixelRatio() : widgetDpr; |
| 522 | // When grabbing from another screen, we grab an area fitting our size using our DPR. |
| 523 | const auto factor = widgetDpr / screenDpr / qreal(m_zoom); |
| 524 | const QSize size{int(std::ceil(x: width() * factor)), int(std::ceil(x: height() * factor))}; |
| 525 | const QPoint pos = mousePos - QPoint{size.width(), size.height()} / 2; |
| 526 | |
| 527 | const QBrush darkBrush = palette().color(cr: QPalette::Dark); |
| 528 | if (screen != nullptr) { |
| 529 | const QPoint screenPos = pos - screen->geometry().topLeft(); |
| 530 | m_buffer = screen->grabWindow(window: 0, x: screenPos.x(), y: screenPos.y(), w: size.width(), h: size.height()); |
| 531 | } else { |
| 532 | m_buffer = QPixmap(size); |
| 533 | m_buffer.fill(fillColor: darkBrush.color()); |
| 534 | } |
| 535 | m_buffer.setDevicePixelRatio(widgetDpr); |
| 536 | |
| 537 | QRegion geom(QRect{pos, size}); |
| 538 | QRect screenRect; |
| 539 | const auto screens = QGuiApplication::screens(); |
| 540 | for (auto *screen : screens) |
| 541 | screenRect |= screen->geometry(); |
| 542 | geom -= screenRect; |
| 543 | const auto rectsInRegion = geom.rectCount(); |
| 544 | if (!geom.isEmpty()) { |
| 545 | QPainter p(&m_buffer); |
| 546 | p.translate(offset: -pos); |
| 547 | p.setPen(Qt::NoPen); |
| 548 | p.setBrush(darkBrush); |
| 549 | p.drawRects(rects: geom.begin(), rectCount: rectsInRegion); |
| 550 | } |
| 551 | |
| 552 | update(); |
| 553 | |
| 554 | m_currentColor = m_buffer.toImage().pixel(pt: m_buffer.rect().center()); |
| 555 | m_lastMousePos = mousePos; |
| 556 | } |
| 557 | |
| 558 | void QPixelTool::startZoomVisibleTimer() |
| 559 | { |
| 560 | if (m_displayZoomId > 0) |
| 561 | killTimer(id: m_displayZoomId); |
| 562 | m_displayZoomId = startTimer(interval: 5000); |
| 563 | setZoomVisible(true); |
| 564 | } |
| 565 | |
| 566 | void QPixelTool::startGridSizeVisibleTimer() |
| 567 | { |
| 568 | if (m_gridActive) { |
| 569 | if (m_displayGridSizeId > 0) |
| 570 | killTimer(id: m_displayGridSizeId); |
| 571 | m_displayGridSizeId = startTimer(interval: 5000); |
| 572 | m_displayGridSize = true; |
| 573 | update(); |
| 574 | } |
| 575 | } |
| 576 | |
| 577 | void QPixelTool::setZoomVisible(bool visible) |
| 578 | { |
| 579 | m_displayZoom = visible; |
| 580 | update(); |
| 581 | } |
| 582 | |
| 583 | void QPixelTool::toggleFreeze() |
| 584 | { |
| 585 | m_freeze = !m_freeze; |
| 586 | if (!m_freeze) |
| 587 | m_dragStart = m_dragCurrent = QPoint(); |
| 588 | } |
| 589 | |
| 590 | void QPixelTool::increaseZoom() |
| 591 | { |
| 592 | if (!m_lcdMode) |
| 593 | setZoom(m_zoom + 1); |
| 594 | else |
| 595 | setZoom(m_zoom + 3); |
| 596 | } |
| 597 | |
| 598 | void QPixelTool::decreaseZoom() |
| 599 | { |
| 600 | if (!m_lcdMode) |
| 601 | setZoom(m_zoom - 1); |
| 602 | else |
| 603 | setZoom(m_zoom - 3); |
| 604 | } |
| 605 | |
| 606 | void QPixelTool::setZoom(int zoom) |
| 607 | { |
| 608 | if (zoom > 0) { |
| 609 | QPoint pos = m_lastMousePos; |
| 610 | m_lastMousePos = QPoint(); |
| 611 | m_zoom = zoom; |
| 612 | grabScreen(); |
| 613 | m_lastMousePos = pos; |
| 614 | m_dragStart = m_dragCurrent = QPoint(); |
| 615 | startZoomVisibleTimer(); |
| 616 | } |
| 617 | } |
| 618 | |
| 619 | void QPixelTool::toggleGrid() |
| 620 | { |
| 621 | if (++m_gridActive > 2) |
| 622 | m_gridActive = 0; |
| 623 | update(); |
| 624 | } |
| 625 | |
| 626 | void QPixelTool::setGridSize(int gridSize) |
| 627 | { |
| 628 | if (m_gridActive && gridSize > 0) { |
| 629 | m_gridSize = gridSize; |
| 630 | startGridSizeVisibleTimer(); |
| 631 | update(); |
| 632 | } |
| 633 | } |
| 634 | |
| 635 | #if QT_CONFIG(clipboard) |
| 636 | void QPixelTool::copyToClipboard() |
| 637 | { |
| 638 | QGuiApplication::clipboard()->setPixmap(m_buffer); |
| 639 | } |
| 640 | |
| 641 | void QPixelTool::copyColorToClipboard() |
| 642 | { |
| 643 | QGuiApplication::clipboard()->setText(QColor(m_currentColor).name()); |
| 644 | } |
| 645 | |
| 646 | void QPixelTool::copyScreenShotToClipboard() |
| 647 | { |
| 648 | QPixmap screenShot = screen()->grabWindow(); |
| 649 | QGuiApplication::clipboard()->setImage(screenShot.toImage()); |
| 650 | } |
| 651 | |
| 652 | void QPixelTool::copyScreenInfoToClipboard() |
| 653 | { |
| 654 | const auto *screen = this->screen(); |
| 655 | QString text; |
| 656 | QTextStream str(&text); |
| 657 | const auto geom = screen->geometry(); |
| 658 | const auto availGeom = screen->availableGeometry(); |
| 659 | auto orientationMt = QMetaEnum::fromType<Qt::ScreenOrientation>(); |
| 660 | |
| 661 | str << "Model/name: \"" << screen->model() << "\"/\"" << screen->name() << '"' |
| 662 | << "\nGeometry: " << geom.width() << 'x' << geom.height() << Qt::forcesign |
| 663 | << geom.x() << geom.y() << Qt::noforcesign |
| 664 | << "\nAvailable geometry: " << availGeom.width() << 'x' << availGeom.height() << Qt::forcesign |
| 665 | << availGeom.x() << availGeom.y() << Qt::noforcesign |
| 666 | << "\nDevice pixel ratio: " << screen->devicePixelRatio() |
| 667 | << "\nLogical DPI: " << screen->logicalDotsPerInchX() << ',' |
| 668 | << screen->logicalDotsPerInchY() << "DPI" |
| 669 | << "\nPhysical DPI: " << screen->physicalDotsPerInchX() << ',' |
| 670 | << screen->physicalDotsPerInchY() << "DPI" |
| 671 | << "\nPhysical size: " << screen->physicalSize().width() << 'x' |
| 672 | << screen->physicalSize().height() << "mm" ; |
| 673 | if (const char *orientation = orientationMt.valueToKey(value: screen->orientation())) |
| 674 | str << "\nOrientation: " << orientation; |
| 675 | str << "\nRefresh rate: " << screen->refreshRate() << "Hz" ; |
| 676 | QGuiApplication::clipboard()->setText(text); |
| 677 | } |
| 678 | |
| 679 | #endif // QT_CONFIG(clipboard) |
| 680 | |
| 681 | void QPixelTool::saveToFile() |
| 682 | { |
| 683 | bool oldFreeze = m_freeze; |
| 684 | m_freeze = true; |
| 685 | |
| 686 | QFileDialog fileDialog(this); |
| 687 | fileDialog.setWindowTitle("Save as image"_L1 ); |
| 688 | fileDialog.setAcceptMode(QFileDialog::AcceptSave); |
| 689 | fileDialog.setDirectory(QStandardPaths::writableLocation(type: QStandardPaths::PicturesLocation)); |
| 690 | QStringList mimeTypes; |
| 691 | const QByteArrayList supportedMimeTypes = QImageWriter::supportedMimeTypes(); |
| 692 | for (const QByteArray &mimeTypeB : supportedMimeTypes) |
| 693 | mimeTypes.append(t: QString::fromLatin1(ba: mimeTypeB)); |
| 694 | fileDialog.setMimeTypeFilters(mimeTypes); |
| 695 | const QString pngType = "image/png"_L1 ; |
| 696 | if (mimeTypes.contains(str: pngType)) { |
| 697 | fileDialog.selectMimeTypeFilter(filter: pngType); |
| 698 | fileDialog.setDefaultSuffix("png"_L1 ); |
| 699 | } |
| 700 | |
| 701 | while (fileDialog.exec() == QDialog::Accepted |
| 702 | && !m_buffer.save(fileName: fileDialog.selectedFiles().constFirst())) { |
| 703 | QMessageBox::warning(parent: this, title: "Unable to write image"_L1 , |
| 704 | text: "Unable to write "_L1 |
| 705 | + QDir::toNativeSeparators(pathName: fileDialog.selectedFiles().constFirst())); |
| 706 | } |
| 707 | m_freeze = oldFreeze; |
| 708 | } |
| 709 | |
| 710 | QTextStream &operator<<(QTextStream &str, const QScreen *screen) |
| 711 | { |
| 712 | const QRect geometry = screen->geometry(); |
| 713 | str << '"' << screen->name() << "\" " << geometry.width() |
| 714 | << 'x' << geometry.height() << Qt::forcesign << geometry.x() << geometry.y() |
| 715 | << Qt::noforcesign << ", " << qRound(d: screen->logicalDotsPerInch()) << "DPI" |
| 716 | << ", Depth: " << screen->depth() << ", " << screen->refreshRate() << "Hz" ; |
| 717 | const qreal dpr = screen->devicePixelRatio(); |
| 718 | if (!qFuzzyCompare(p1: dpr, p2: qreal(1))) |
| 719 | str << ", DPR: " << dpr; |
| 720 | return str; |
| 721 | } |
| 722 | |
| 723 | QString QPixelTool::aboutText() const |
| 724 | { |
| 725 | const QList<QScreen *> screens = QGuiApplication::screens(); |
| 726 | const QScreen *windowScreen = windowHandle()->screen(); |
| 727 | |
| 728 | QString result; |
| 729 | QTextStream str(&result); |
| 730 | str << "<html><head></head><body><h2>Qt Pixeltool</h2><p>Qt " << QT_VERSION_STR |
| 731 | << "</p><p>Copyright (C) 2017 The Qt Company Ltd.</p><h3>Screens</h3><ul>" ; |
| 732 | for (const QScreen *screen : screens) |
| 733 | str << "<li>" << (screen == windowScreen ? "* " : " " ) << screen << "</li>" ; |
| 734 | str << "</ul></body></html>" ; |
| 735 | return result; |
| 736 | } |
| 737 | |
| 738 | void QPixelTool::aboutPixelTool() |
| 739 | { |
| 740 | QMessageBox aboutBox(QMessageBox::Information, tr(s: "About Qt Pixeltool" ), aboutText(), |
| 741 | QMessageBox::Close, this); |
| 742 | aboutBox.setWindowFlags(aboutBox.windowFlags() & ~Qt::WindowContextHelpButtonHint); |
| 743 | aboutBox.setTextInteractionFlags(Qt::TextBrowserInteraction); |
| 744 | aboutBox.exec(); |
| 745 | } |
| 746 | |
| 747 | QT_END_NAMESPACE |
| 748 | |