Browse Source

Color grabber zoom preview (#1869)

* Add ColorGrabWidget

The new widget aims to decouple color grabbing from the SidePanelWidget.

* Refactor SidePanelWidget to use ColorGrabWidget

- All color grabbing functionality is now moved to ColorGrabWidget
- SidePanelWidget now uses a more organized sigslot approach
- Removed QColorPickingEventFilter

* Fix bug and complete implementation

Timer not yet implemented.

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Add 0.5s timer

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Fix failing builds

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Add hex color editor

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Add right mouse button instant preview

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Make zoom widget toggle-able

* Implement OverlayMessage class

* Make Right click do the same as Space

* Unzoom widget when mouse leaves it

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Fix some small issues

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Toggle panel when grabbing color

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Show with timer even if magnifier active

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Reduce timer delay

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Fix OverlayMessage bug

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>
Haris Gušić 3 years ago
parent
commit
aca0db963b

+ 2 - 0
src/widgets/capture/CMakeLists.txt

@@ -7,6 +7,7 @@ target_sources(
         capturewidget.h
         colorpicker.h
         hovereventfilter.h
+        overlaymessage.h
         selectionwidget.h
         notifierbox.h
         modificationcommand.h)
@@ -19,6 +20,7 @@ target_sources(
         capturewidget.cpp
         colorpicker.cpp
         hovereventfilter.cpp
+        overlaymessage.cpp
         notifierbox.cpp
         selectionwidget.cpp
         modificationcommand.cpp)

+ 15 - 60
src/widgets/capture/capturewidget.cpp

@@ -21,6 +21,7 @@
 #include "src/widgets/capture/hovereventfilter.h"
 #include "src/widgets/capture/modificationcommand.h"
 #include "src/widgets/capture/notifierbox.h"
+#include "src/widgets/capture/overlaymessage.h"
 #include "src/widgets/orientablepushbutton.h"
 #include "src/widgets/panel/sidepanelwidget.h"
 #include "src/widgets/panel/utilitypanel.h"
@@ -84,7 +85,6 @@ CaptureWidget::CaptureWidget(uint id,
             this,
             &CaptureWidget::childLeave);
     setAttribute(Qt::WA_DeleteOnClose);
-    m_showInitialMsg = m_config.showHelpValue();
     m_opacity = m_config.contrastOpacityValue();
     m_uiColor = m_config.uiMainColorValue();
     m_contrastUiColor = m_config.uiContrastColorValue();
@@ -199,6 +199,18 @@ CaptureWidget::CaptureWidget(uint id,
     });
 
     initPanel();
+
+    OverlayMessage::init(this,
+                         QGuiAppCurrentScreen().currentScreen()->geometry());
+
+    if (m_config.showHelpValue()) {
+        OverlayMessage::push(
+          tr("Select an area with the mouse, or press Esc to exit."
+             "\nPress Enter to capture the screen."
+             "\nPress Right Click to show the color picker."
+             "\nUse the Mouse Wheel to change the thickness of your tool."
+             "\nPress Space to open the side panel."));
+    }
 }
 
 CaptureWidget::~CaptureWidget()
@@ -364,12 +376,6 @@ void CaptureWidget::paintEvent(QPaintEvent* paintEvent)
 
     // draw inactive region
     drawInactiveRegion(&painter);
-
-    // show initial message on screen capture call if required (before selecting
-    // area)
-    if (m_showInitialMsg) {
-        drawInitialMessage(&painter);
-    }
 }
 
 void CaptureWidget::showColorPicker(const QPoint& pos)
@@ -485,7 +491,7 @@ void CaptureWidget::mousePressEvent(QMouseEvent* e)
         showColorPicker(m_mousePressedPos);
         return;
     } else if (e->button() == Qt::LeftButton) {
-        m_showInitialMsg = false;
+        OverlayMessage::pop();
         m_mouseIsClicked = true;
 
         // Click using a tool excluding tool MOVE
@@ -972,7 +978,7 @@ void CaptureWidget::initPanel()
             this,
             &CaptureWidget::updateActiveLayer);
 
-    m_sidePanel = new SidePanelWidget(&m_context.screenshot);
+    m_sidePanel = new SidePanelWidget(&m_context.screenshot, this);
     connect(m_sidePanel,
             &SidePanelWidget::colorChanged,
             this,
@@ -1325,7 +1331,6 @@ void CaptureWidget::selectAll()
     m_selection->setGeometry(newGeometry);
     m_context.selection = extendedRect(newGeometry);
     m_selection->setVisible(true);
-    m_showInitialMsg = false;
     m_buttonHandler->updatePosition(m_selection->geometry());
     updateSizeIndicator();
     m_buttonHandler->show();
@@ -1664,56 +1669,6 @@ QRect CaptureWidget::extendedRect(const QRect& r) const
                  r.height() * devicePixelRatio);
 }
 
-void CaptureWidget::drawInitialMessage(QPainter* painter)
-{
-    if (nullptr == painter) {
-        return;
-    }
-#if (defined(Q_OS_MACOS) || defined(Q_OS_LINUX))
-    QRect helpRect;
-    QScreen* currentScreen = QGuiAppCurrentScreen().currentScreen();
-    if (currentScreen) {
-        helpRect = currentScreen->geometry();
-    } else {
-        helpRect = QGuiApplication::primaryScreen()->geometry();
-    }
-#else
-    QRect helpRect = QGuiApplication::primaryScreen()->geometry();
-#endif
-
-    helpRect.moveTo(mapFromGlobal(helpRect.topLeft()));
-
-    QString helpTxt =
-      tr("Select an area with the mouse, or press Esc to exit."
-         "\nPress Enter to capture the screen."
-         "\nPress Right Click to show the color picker."
-         "\nUse the Mouse Wheel to change the thickness of your tool."
-         "\nPress Space to open the side panel.");
-
-    // We draw the white contrasting background for the text, using the
-    // same text and options to get the boundingRect that the text will
-    // have.
-    QRectF bRect = painter->boundingRect(helpRect, Qt::AlignCenter, helpTxt);
-
-    // These four calls provide padding for the rect
-    const int margin = QApplication::fontMetrics().height() / 2;
-    bRect.setWidth(bRect.width() + margin);
-    bRect.setHeight(bRect.height() + margin);
-    bRect.setX(bRect.x() - margin);
-    bRect.setY(bRect.y() - margin);
-
-    QColor rectColor(m_uiColor);
-    rectColor.setAlpha(180);
-    QColor textColor(
-      (ColorUtils::colorIsDark(rectColor) ? Qt::white : Qt::black));
-
-    painter->setBrush(QBrush(rectColor, Qt::SolidPattern));
-    painter->setPen(QPen(textColor));
-
-    painter->drawRect(bRect);
-    painter->drawText(helpRect, Qt::AlignCenter, helpTxt);
-}
-
 void CaptureWidget::drawInactiveRegion(QPainter* painter)
 {
     QColor overlayColor(0, 0, 0, m_opacity);

+ 0 - 2
src/widgets/capture/capturewidget.h

@@ -131,7 +131,6 @@ private:
 
     QRect extendedSelection() const;
     QRect extendedRect(const QRect& r) const;
-    void drawInitialMessage(QPainter* painter);
     void drawInactiveRegion(QPainter* painter);
     void drawToolsData(bool updateLayersPanel = true,
                        bool drawSelection = false);
@@ -159,7 +158,6 @@ private:
     bool m_newSelection;
     bool m_grabbing;
     bool m_movingSelection;
-    bool m_showInitialMsg;
     bool m_captureDone;
     bool m_previewEnabled;
     bool m_adjustmentButtonPressed;

+ 110 - 0
src/widgets/capture/overlaymessage.cpp

@@ -0,0 +1,110 @@
+#include "overlaymessage.h"
+#include "colorutils.h"
+#include "confighandler.h"
+#include "qguiappcurrentscreen.h"
+
+#include <QApplication>
+#include <QDebug>
+#include <QPainter>
+#include <QPen>
+#include <QScreen>
+#include <QWidget>
+
+OverlayMessage::OverlayMessage(QWidget* parent, const QRect& targetArea)
+  : QWidget(parent)
+  , m_targetArea(targetArea)
+{
+    // NOTE: do not call the static functions from the constructor
+    m_instance = this;
+    m_messageStack.push(QString()); // Default message is empty
+    setAttribute(Qt::WA_TransparentForMouseEvents);
+    setAttribute(Qt::WA_AlwaysStackOnTop);
+    QWidget::hide();
+}
+
+void OverlayMessage::init(QWidget* parent, const QRect& targetArea)
+{
+    new OverlayMessage(parent, targetArea);
+}
+
+void OverlayMessage::push(const QString& msg)
+{
+    m_instance->m_messageStack.push(msg);
+    setVisibility(true);
+}
+
+void OverlayMessage::pop()
+{
+    if (m_instance->m_messageStack.size() > 1) {
+        m_instance->m_messageStack.pop();
+    }
+
+    if (m_instance->m_messageStack.size() == 1) {
+        // Only empty message left (don't show it)
+        m_instance->QWidget::hide();
+    } else {
+        // Still visible, resize for new message
+        m_instance->updateGeometry();
+    }
+}
+
+void OverlayMessage::setVisibility(bool visible)
+{
+    m_instance->updateGeometry();
+    m_instance->setVisible(visible);
+}
+
+OverlayMessage* OverlayMessage::instance()
+{
+    return m_instance;
+}
+
+void OverlayMessage::paintEvent(QPaintEvent*)
+{
+    QPainter painter(this);
+
+    QRectF bRect = boundingRect();
+    bRect.moveTo(0, 0);
+
+    QColor rectColor(ConfigHandler().uiMainColorValue());
+    rectColor.setAlpha(180);
+    QColor textColor(
+      (ColorUtils::colorIsDark(rectColor) ? Qt::white : Qt::black));
+
+    painter.setBrush(QBrush(rectColor, Qt::SolidPattern));
+    painter.setPen(QPen(textColor));
+
+    float margin = painter.pen().widthF();
+    painter.drawRect(bRect - QMarginsF(margin, margin, margin, margin));
+    painter.drawText(bRect, Qt::AlignCenter, m_messageStack.top());
+}
+
+void OverlayMessage::showEvent(QShowEvent*)
+{
+    update();
+}
+
+QRectF OverlayMessage::boundingRect() const
+{
+    // We draw the white contrasting background for the text, using the
+    // same text and options to get the boundingRect that the text will
+    // have.
+    QRectF bRect = QApplication::fontMetrics().boundingRect(
+      m_targetArea, Qt::AlignCenter, m_messageStack.top());
+
+    // These four calls provide padding for the rect
+    const int margin = QApplication::fontMetrics().height() / 2;
+    bRect.setWidth(bRect.width() + margin);
+    bRect.setHeight(bRect.height() + margin);
+    bRect.setX(bRect.x() - margin);
+    bRect.setY(bRect.y() - margin);
+    return bRect;
+}
+
+void OverlayMessage::updateGeometry()
+{
+    m_instance->setGeometry(m_instance->boundingRect().toRect());
+    QWidget::updateGeometry();
+}
+
+OverlayMessage* OverlayMessage::m_instance = nullptr;

+ 43 - 0
src/widgets/capture/overlaymessage.h

@@ -0,0 +1,43 @@
+#pragma once
+
+#include <QStack>
+#include <QWidget>
+
+/**
+ * @brief Overlay a message in capture mode.
+ *
+ * The message must be initialized by calling `init` before it can be used. That
+ * can be done once per capture session. The class is a singleton.
+ *
+ * To change the active message call `push`. This will automatically show the
+ * widget. Previous messages won't be forgotten and will be reactivated after
+ * you call `pop`. You can show/hide the message using `setVisibility` (this
+ * won't push/pop anything).
+ *
+ * @note You have to make sure that widgets pop the messages they pushed when
+ * they are closed, to avoid potential bugs and resource leaks.
+ */
+class OverlayMessage : public QWidget
+{
+public:
+    OverlayMessage() = delete;
+
+    static void init(QWidget* parent, const QRect& targetArea);
+    static void push(const QString& msg);
+    static void pop();
+    static void setVisibility(bool visible);
+    static OverlayMessage* instance();
+
+private:
+    QStack<QString> m_messageStack;
+    QRect m_targetArea;
+    static OverlayMessage* m_instance;
+
+    OverlayMessage(QWidget* parent, const QRect& center);
+
+    void paintEvent(QPaintEvent*) override;
+    void showEvent(QShowEvent*) override;
+
+    QRectF boundingRect() const;
+    void updateGeometry();
+};

+ 2 - 2
src/widgets/panel/CMakeLists.txt

@@ -1,4 +1,4 @@
 # Required to generate MOC
-target_sources(flameshot PRIVATE sidepanelwidget.h utilitypanel.h)
+target_sources(flameshot PRIVATE sidepanelwidget.h utilitypanel.h colorgrabwidget.h)
 
-target_sources(flameshot PRIVATE sidepanelwidget.cpp utilitypanel.cpp)
+target_sources(flameshot PRIVATE sidepanelwidget.cpp utilitypanel.cpp colorgrabwidget.cpp)

+ 219 - 0
src/widgets/panel/colorgrabwidget.cpp

@@ -0,0 +1,219 @@
+#include "colorgrabwidget.h"
+#include "sidepanelwidget.h"
+
+#include "colorutils.h"
+#include "confighandler.h"
+#include "overlaymessage.h"
+#include "src/core/qguiappcurrentscreen.h"
+#include <QApplication>
+#include <QDebug>
+#include <QKeyEvent>
+#include <QPainter>
+#include <QScreen>
+#include <QShortcut>
+#include <QTimer>
+#include <stdexcept>
+
+// Width (= height) and zoom level of the widget before the user clicks
+#define WIDTH1 77
+#define ZOOM1 11
+// Width (= height) and zoom level of the widget after the user clicks
+#define WIDTH2 165
+#define ZOOM2 15
+
+// NOTE: WIDTH1(2) should be divisible by ZOOM1(2) for best precision.
+//       WIDTH1 should be odd so the cursor can be centered on a pixel.
+
+ColorGrabWidget::ColorGrabWidget(QPixmap* p, QWidget* parent)
+  : QWidget(parent)
+  , m_pixmap(p)
+  , m_mousePressReceived(false)
+  , m_extraZoomActive(false)
+  , m_magnifierActive(false)
+{
+    if (p == nullptr) {
+        throw std::logic_error("Pixmap must not be null");
+    }
+    setAttribute(Qt::WA_DeleteOnClose);
+    // We don't need this widget to receive mouse events because we use
+    // eventFilter on other objects that do
+    setAttribute(Qt::WA_TransparentForMouseEvents);
+    setWindowFlags(Qt::BypassWindowManagerHint | Qt::FramelessWindowHint);
+    setMouseTracking(true);
+}
+
+void ColorGrabWidget::startGrabbing()
+{
+    // NOTE: grabMouse() would prevent move events being received
+    // With this method we just need to make sure that mouse press and release
+    // events get consumed before they reach their target widget.
+    // This is undone in the destructor.
+    qApp->setOverrideCursor(Qt::CrossCursor);
+    qApp->installEventFilter(this);
+    OverlayMessage::push(
+      "Press Enter or Left Mouse Button to accept color\n"
+      "Press and hold Left Mouse Button to precisely select color\n"
+      "Press Space or Right Mouse Button to toggle magnifier\n"
+      "Press ESC to cancel");
+}
+
+QColor ColorGrabWidget::color()
+{
+    return m_color;
+}
+
+bool ColorGrabWidget::eventFilter(QObject*, QEvent* event)
+{
+    // Consume shortcut events and handle key presses from whole app
+    if (event->type() == QEvent::KeyPress ||
+        event->type() == QEvent::Shortcut) {
+        QKeySequence key = event->type() == QEvent::KeyPress
+                             ? static_cast<QKeyEvent*>(event)->key()
+                             : static_cast<QShortcutEvent*>(event)->key();
+        if (key == Qt::Key_Escape) {
+            emit grabAborted();
+            finalize();
+        } else if (key == Qt::Key_Return || key == Qt::Key_Enter) {
+            emit colorGrabbed(m_color);
+            finalize();
+        } else if (key == Qt::Key_Space && !m_extraZoomActive) {
+            setMagnifierActive(!m_magnifierActive);
+        }
+        return true;
+    } else if (event->type() == QEvent::MouseMove) {
+        // NOTE: This relies on the fact that CaptureWidget tracks mouse moves
+
+        if (m_extraZoomActive && !geometry().contains(cursorPos())) {
+            setExtraZoomActive(false);
+            return true;
+        }
+        if (!m_extraZoomActive && !m_magnifierActive) {
+            // This fixes an issue when the mouse leaves the zoom area before
+            // the widget even appears.
+            hide();
+        }
+        if (!m_extraZoomActive) {
+            // Update only before the user clicks the mouse, after the mouse
+            // press the widget remains static.
+            updateWidget();
+        }
+
+        // Hide overlay message when cursor is over it
+        OverlayMessage* overlayMsg = OverlayMessage::instance();
+        overlayMsg->setVisibility(
+          !overlayMsg->geometry().contains(cursorPos()));
+
+        m_color = getColorAtPoint(cursorPos());
+        emit colorUpdated(m_color);
+        return true;
+    } else if (event->type() == QEvent::MouseButtonPress) {
+        m_mousePressReceived = true;
+        auto* e = static_cast<QMouseEvent*>(event);
+        if (e->buttons() == Qt::RightButton) {
+            setMagnifierActive(!m_magnifierActive);
+        } else if (e->buttons() == Qt::LeftButton) {
+            setExtraZoomActive(true);
+        }
+        return true;
+    } else if (event->type() == QEvent::MouseButtonRelease) {
+        if (!m_mousePressReceived) {
+            // Do not consume event if it corresponds to the mouse press that
+            // triggered the color grabbing in the first place. This prevents
+            // focus issues in the capture widget when the color grabber is
+            // closed.
+            return false;
+        }
+        auto* e = static_cast<QMouseEvent*>(event);
+        if (e->button() == Qt::LeftButton && m_extraZoomActive) {
+            emit colorGrabbed(getColorAtPoint(cursorPos()));
+            finalize();
+        }
+        return true;
+    } else if (event->type() == QEvent::MouseButtonDblClick) {
+        return true;
+    }
+    return false;
+}
+
+void ColorGrabWidget::paintEvent(QPaintEvent*)
+{
+    QPainter painter(this);
+    painter.drawImage(QRectF(0, 0, width(), height()), m_previewImage);
+}
+
+void ColorGrabWidget::showEvent(QShowEvent*)
+{
+    updateWidget();
+}
+
+QPoint ColorGrabWidget::cursorPos() const
+{
+    return QCursor::pos(QGuiAppCurrentScreen().currentScreen());
+}
+
+/// @note The point is in screen coordinates.
+QColor ColorGrabWidget::getColorAtPoint(const QPoint& p) const
+{
+    if (m_extraZoomActive && geometry().contains(p)) {
+        QPoint point = mapFromGlobal(p);
+        // we divide coordinate-wise to avoid rounding to nearest
+        return m_previewImage.pixel(
+          QPoint(point.x() / ZOOM2, point.y() / ZOOM2));
+    }
+    QPoint point = p;
+#if defined(Q_OS_MACOS)
+    QScreen* currentScreen = QGuiAppCurrentScreen().currentScreen();
+    if (currentScreen) {
+        point = QPoint((p.x() - currentScreen->geometry().x()) *
+                         currentScreen->devicePixelRatio(),
+                       (p.y() - currentScreen->geometry().y()) *
+                         currentScreen->devicePixelRatio());
+    }
+#endif
+    QPixmap pixel = m_pixmap->copy(QRect(point, point));
+    return pixel.toImage().pixel(0, 0);
+}
+
+void ColorGrabWidget::setExtraZoomActive(bool active)
+{
+    m_extraZoomActive = active;
+    if (!active && !m_magnifierActive) {
+        hide();
+    } else {
+        if (!isVisible()) {
+            QTimer::singleShot(250, this, [this]() { show(); });
+        } else {
+            QTimer::singleShot(250, this, [this]() { updateWidget(); });
+        }
+    }
+}
+
+void ColorGrabWidget::setMagnifierActive(bool active)
+{
+    m_magnifierActive = active;
+    setVisible(active);
+}
+
+void ColorGrabWidget::updateWidget()
+{
+    int width = m_extraZoomActive ? WIDTH2 : WIDTH1;
+    float zoom = m_extraZoomActive ? ZOOM2 : ZOOM1;
+    // Set window size and move its center to the mouse cursor
+    QRect rect(0, 0, width, width);
+    rect.moveCenter(cursorPos());
+    setGeometry(rect);
+    // Store a pixmap containing the zoomed-in section around the cursor
+    QRect sourceRect(0, 0, width / zoom, width / zoom);
+    sourceRect.moveCenter(rect.center());
+    m_previewImage = m_pixmap->copy(sourceRect).toImage();
+    // Repaint
+    update();
+}
+
+void ColorGrabWidget::finalize()
+{
+    qApp->removeEventFilter(this);
+    qApp->restoreOverrideCursor();
+    OverlayMessage::pop();
+    close();
+}

+ 45 - 0
src/widgets/panel/colorgrabwidget.h

@@ -0,0 +1,45 @@
+#ifndef COLORGRABWIDGET_H
+#define COLORGRABWIDGET_H
+
+#include <QWidget>
+
+class SidePanelWidget;
+class OverlayMessage;
+
+class ColorGrabWidget : public QWidget
+{
+    Q_OBJECT
+public:
+    ColorGrabWidget(QPixmap* p, QWidget* parent = nullptr);
+
+    void startGrabbing();
+
+    QColor color();
+
+signals:
+    void colorUpdated(const QColor& color);
+    void colorGrabbed(const QColor& color);
+    void grabAborted();
+
+private:
+    bool eventFilter(QObject* obj, QEvent* event) override;
+    void paintEvent(QPaintEvent* e);
+    void showEvent(QShowEvent* event) override;
+
+    QPoint cursorPos() const;
+    QColor getColorAtPoint(const QPoint& point) const;
+    void setExtraZoomActive(bool active);
+    void setMagnifierActive(bool active);
+    void updateWidget();
+    void finalize();
+
+    QPixmap* m_pixmap;
+    QImage m_previewImage;
+    QColor m_color;
+
+    bool m_mousePressReceived;
+    bool m_extraZoomActive;
+    bool m_magnifierActive;
+};
+
+#endif // COLORGRABWIDGET_H

+ 90 - 119
src/widgets/panel/sidepanelwidget.cpp

@@ -2,57 +2,33 @@
 // SPDX-FileCopyrightText: 2017-2019 Alejandro Sirgo Rica & Contributors
 
 #include "sidepanelwidget.h"
+#include "colorgrabwidget.h"
 #include "src/core/qguiappcurrentscreen.h"
 #include "src/utils/colorutils.h"
 #include "src/utils/pathinfo.h"
+#include "utilitypanel.h"
+#include <QApplication>
+#include <QDebug> // TODO remove
 #include <QFormLayout>
 #include <QKeyEvent>
 #include <QLabel>
+#include <QLineEdit>
 #include <QPushButton>
+#include <QShortcut>
 #include <QSlider>
 #include <QVBoxLayout>
 #if defined(Q_OS_MACOS)
 #include <QScreen>
 #endif
 
-class QColorPickingEventFilter : public QObject
-{
-public:
-    explicit QColorPickingEventFilter(SidePanelWidget* pw,
-                                      QObject* parent = nullptr)
-      : QObject(parent)
-      , m_pw(pw)
-    {}
-
-    bool eventFilter(QObject*, QEvent* event) override
-    {
-        event->accept();
-        switch (event->type()) {
-            case QEvent::MouseMove:
-                return m_pw->handleMouseMove(static_cast<QMouseEvent*>(event));
-            case QEvent::MouseButtonPress:
-                return m_pw->handleMouseButtonPressed(
-                  static_cast<QMouseEvent*>(event));
-            case QEvent::KeyPress:
-                return m_pw->handleKeyPress(static_cast<QKeyEvent*>(event));
-            default:
-                break;
-        }
-        return false;
-    }
-
-private:
-    SidePanelWidget* m_pw;
-};
-
-////////////////////////
-
 SidePanelWidget::SidePanelWidget(QPixmap* p, QWidget* parent)
   : QWidget(parent)
   , m_pixmap(p)
-  , m_eventFilter(nullptr)
 {
     m_layout = new QVBoxLayout(this);
+    if (parent) {
+        parent->installEventFilter(this);
+    }
 
     QFormLayout* colorForm = new QFormLayout();
     m_thicknessSlider = new QSlider(Qt::Horizontal);
@@ -64,6 +40,23 @@ SidePanelWidget::SidePanelWidget(QPixmap* p, QWidget* parent)
     colorForm->addRow(tr("Active color:"), m_colorLabel);
     m_layout->addLayout(colorForm);
 
+    m_colorWheel = new color_widgets::ColorWheel(this);
+    m_colorWheel->setColor(m_color);
+    m_colorHex = new QLineEdit(this);
+    m_colorHex->setAlignment(Qt::AlignCenter);
+
+    QColor background = this->palette().window().color();
+    bool isDark = ColorUtils::colorIsDark(background);
+    QString modifier =
+      isDark ? PathInfo::whiteIconPath() : PathInfo::blackIconPath();
+    QIcon grabIcon(modifier + "colorize.svg");
+    m_colorGrabButton = new QPushButton(grabIcon, tr("Grab Color"));
+
+    m_layout->addWidget(m_colorGrabButton);
+    m_layout->addWidget(m_colorWheel);
+    m_layout->addWidget(m_colorHex);
+
+    // thickness sigslots
     connect(m_thicknessSlider,
             &QSlider::sliderMoved,
             this,
@@ -72,22 +65,20 @@ SidePanelWidget::SidePanelWidget(QPixmap* p, QWidget* parent)
             &SidePanelWidget::thicknessChanged,
             this,
             &SidePanelWidget::updateThickness);
-
-    QColor background = this->palette().window().color();
-    bool isDark = ColorUtils::colorIsDark(background);
-    QString modifier =
-      isDark ? PathInfo::whiteIconPath() : PathInfo::blackIconPath();
-    QIcon grabIcon(modifier + "colorize.svg");
-    m_colorGrabButton = new QPushButton(grabIcon, QLatin1String(""));
-    updateGrabButton(false);
+    // color hex editor sigslots
+    connect(m_colorHex, &QLineEdit::editingFinished, this, [=]() {
+        if (!QColor::isValidColor(m_colorHex->text())) {
+            m_colorHex->setText(m_color.name(QColor::HexRgb));
+        } else {
+            updateColor(m_colorHex->text());
+        }
+    });
+    // color grab button sigslots
     connect(m_colorGrabButton,
             &QPushButton::pressed,
             this,
-            &SidePanelWidget::colorGrabberActivated);
-    m_layout->addWidget(m_colorGrabButton);
-
-    m_colorWheel = new color_widgets::ColorWheel(this);
-    m_colorWheel->setColor(m_color);
+            &SidePanelWidget::startColorGrab);
+    // color wheel sigslots
     connect(m_colorWheel,
             &color_widgets::ColorWheel::mouseReleaseOnColor,
             this,
@@ -96,15 +87,20 @@ SidePanelWidget::SidePanelWidget(QPixmap* p, QWidget* parent)
             &color_widgets::ColorWheel::colorChanged,
             this,
             &SidePanelWidget::updateColorNoWheel);
-    m_layout->addWidget(m_colorWheel);
 }
 
 void SidePanelWidget::updateColor(const QColor& c)
 {
     m_color = c;
+    updateColorNoWheel(c);
+    m_colorWheel->setColor(c);
+}
+
+void SidePanelWidget::updateColorNoWheel(const QColor& c)
+{
     m_colorLabel->setStyleSheet(
       QStringLiteral("QLabel { background-color : %1; }").arg(c.name()));
-    m_colorWheel->setColor(m_color);
+    m_colorHex->setText(c.name(QColor::HexRgb));
 }
 
 void SidePanelWidget::updateThickness(const int& t)
@@ -113,95 +109,70 @@ void SidePanelWidget::updateThickness(const int& t)
     m_thicknessSlider->setValue(m_thickness);
 }
 
-void SidePanelWidget::updateColorNoWheel(const QColor& c)
+void SidePanelWidget::startColorGrab()
 {
-    m_color = c;
-    m_colorLabel->setStyleSheet(
-      QStringLiteral("QLabel { background-color : %1; }").arg(c.name()));
-}
+    m_revertColor = m_color;
+    m_colorGrabber = new ColorGrabWidget(m_pixmap);
+    connect(m_colorGrabber,
+            &ColorGrabWidget::colorUpdated,
+            this,
+            &SidePanelWidget::onColorUpdated);
+    connect(m_colorGrabber,
+            &ColorGrabWidget::colorGrabbed,
+            this,
+            &SidePanelWidget::onColorGrabFinished);
+    connect(m_colorGrabber,
+            &ColorGrabWidget::grabAborted,
+            this,
+            &SidePanelWidget::onColorGrabAborted);
 
-void SidePanelWidget::colorGrabberActivated()
-{
-    grabKeyboard();
-    grabMouse(Qt::CrossCursor);
-    setMouseTracking(true);
-    m_colorBackup = m_color;
-    if (!m_eventFilter) {
-        m_eventFilter = new QColorPickingEventFilter(this, this);
-    }
-    installEventFilter(m_eventFilter);
-    updateGrabButton(true);
+    emit togglePanel();
+    m_colorGrabber->startGrabbing();
 }
 
-void SidePanelWidget::releaseColorGrab()
+void SidePanelWidget::onColorGrabFinished()
 {
-    setMouseTracking(false);
-    removeEventFilter(m_eventFilter);
-    releaseMouse();
-    releaseKeyboard();
-    setFocus();
-    updateGrabButton(false);
+    finalizeGrab();
+    m_color = m_colorGrabber->color();
+    emit colorChanged(m_color);
 }
 
-QColor SidePanelWidget::grabPixmapColor(const QPoint& p)
+void SidePanelWidget::onColorGrabAborted()
 {
-    QColor c;
-    if (m_pixmap) {
-#if defined(Q_OS_MACOS)
-        QScreen* currentScreen = QGuiAppCurrentScreen().currentScreen();
-        QPoint point = p;
-        if (currentScreen) {
-            point = QPoint((p.x() - currentScreen->geometry().x()) *
-                             currentScreen->devicePixelRatio(),
-                           (p.y() - currentScreen->geometry().y()) *
-                             currentScreen->devicePixelRatio());
-        }
-        QPixmap pixel = m_pixmap->copy(QRect(point, point));
-#else
-        QPixmap pixel = m_pixmap->copy(QRect(p, p));
-#endif
-        c = pixel.toImage().pixel(0, 0);
-    }
-    return c;
+    finalizeGrab();
+    // Restore color that was selected before we started grabbing
+    updateColor(m_revertColor);
 }
 
-bool SidePanelWidget::handleKeyPress(QKeyEvent* e)
+void SidePanelWidget::onColorUpdated(const QColor& color)
 {
-    if (e->key() == Qt::Key_Escape) {
-        releaseColorGrab();
-        updateColor(m_colorBackup);
-    } else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
-        updateColor(grabPixmapColor(QCursor::pos()));
-        releaseColorGrab();
-        emit colorChanged(m_color);
-    }
-    return true;
+    updateColorNoWheel(color);
 }
 
-bool SidePanelWidget::handleMouseButtonPressed(QMouseEvent* e)
+void SidePanelWidget::finalizeGrab()
 {
-    if (m_colorGrabButton->geometry().contains(e->pos()) ||
-        e->button() == Qt::RightButton) {
-        updateColorNoWheel(m_colorBackup);
-    } else if (e->button() == Qt::LeftButton) {
-        updateColor(grabPixmapColor(QCursor::pos()));
-    }
-    releaseColorGrab();
-    emit colorChanged(m_color);
-    return true;
+    emit togglePanel();
 }
 
-bool SidePanelWidget::handleMouseMove(QMouseEvent* e)
+bool SidePanelWidget::eventFilter(QObject* obj, QEvent* event)
 {
-    updateColorNoWheel(grabPixmapColor(e->globalPos()));
-    return true;
+    if (event->type() == QEvent::ShortcutOverride) {
+        // Override Escape shortcut from CaptureWidget
+        auto* e = static_cast<QKeyEvent*>(event);
+        if (e->key() == Qt::Key_Escape && m_colorHex->hasFocus()) {
+            m_colorHex->clearFocus();
+            e->accept();
+            return true;
+        }
+    } else if (event->type() == QEvent::MouseButtonPress) {
+        // Clicks outside of the Color Hex editor
+        m_colorHex->clearFocus();
+    }
+    return QWidget::eventFilter(obj, event);
 }
 
-void SidePanelWidget::updateGrabButton(const bool activated)
+void SidePanelWidget::hideEvent(QHideEvent* event)
 {
-    if (activated) {
-        m_colorGrabButton->setText(tr("Press ESC to cancel"));
-    } else {
-        m_colorGrabButton->setText(tr("Grab Color"));
-    }
+    QWidget::hideEvent(event);
+    m_colorHex->clearFocus();
 }

+ 13 - 13
src/widgets/panel/sidepanelwidget.h

@@ -9,6 +9,8 @@
 class QVBoxLayout;
 class QPushButton;
 class QLabel;
+class QLineEdit;
+class ColorGrabWidget;
 class QColorPickingEventFilter;
 class QSlider;
 
@@ -28,32 +30,30 @@ signals:
 
 public slots:
     void updateColor(const QColor& c);
-    void updateThickness(const int& t);
-
-private slots:
     void updateColorNoWheel(const QColor& c);
+    void updateThickness(const int& t);
 
 private slots:
-    void colorGrabberActivated();
-    void releaseColorGrab();
+    void startColorGrab();
+    void onColorGrabFinished();
+    void onColorGrabAborted();
+    void onColorUpdated(const QColor& color);
 
 private:
-    QColor grabPixmapColor(const QPoint& p);
-
-    bool handleKeyPress(QKeyEvent* e);
-    bool handleMouseButtonPressed(QMouseEvent* e);
-    bool handleMouseMove(QMouseEvent* e);
+    void finalizeGrab();
 
-    void updateGrabButton(const bool activated);
+    bool eventFilter(QObject* obj, QEvent* event) override;
+    void hideEvent(QHideEvent* event) override;
 
     QVBoxLayout* m_layout;
     QPushButton* m_colorGrabButton;
+    ColorGrabWidget* m_colorGrabber;
     color_widgets::ColorWheel* m_colorWheel;
     QLabel* m_colorLabel;
+    QLineEdit* m_colorHex;
     QPixmap* m_pixmap;
-    QColor m_colorBackup;
     QColor m_color;
+    QColor m_revertColor;
     QSlider* m_thicknessSlider;
     int m_thickness;
-    QColorPickingEventFilter* m_eventFilter;
 };