Improve color selection and display (#5057)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix
2024-01-06 21:52:29 +01:00
committed by GitHub
parent 693d4f401d
commit 78a7ebb9f9
26 changed files with 1276 additions and 945 deletions
-23
View File
@@ -1,23 +0,0 @@
#include "widgets/helper/ColorButton.hpp"
namespace chatterino {
ColorButton::ColorButton(const QColor &color, QWidget *parent)
: QPushButton(parent)
, color_(color)
{
this->setColor(color_);
}
const QColor &ColorButton::color() const
{
return this->color_;
}
void ColorButton::setColor(QColor color)
{
this->color_ = color;
this->setStyleSheet("background-color: " + color.name(QColor::HexArgb));
}
} // namespace chatterino
-20
View File
@@ -1,20 +0,0 @@
#pragma once
#include <QPushButton>
namespace chatterino {
class ColorButton : public QPushButton
{
public:
ColorButton(const QColor &color, QWidget *parent = nullptr);
const QColor &color() const;
void setColor(QColor color);
private:
QColor color_;
};
} // namespace chatterino
-292
View File
@@ -1,292 +0,0 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWidgets module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "widgets/helper/QColorPicker.hpp"
#include <qdrawutil.h>
#include <QMouseEvent>
#include <QPainter>
/*
* These classes are literally copied from the Qt source.
* Unfortunately, they are private to the QColorDialog class so we cannot use
* them directly.
* If they become public at any point in the future, it should be possible to
* replace every include of this header with the respective includes for the
* QColorPicker, QColorLuminancePicker, and QColSpinBox classes.
*/
namespace chatterino {
int QColorLuminancePicker::y2val(int y)
{
int d = height() - 2 * coff - 1;
return 255 - (y - coff) * 255 / d;
}
int QColorLuminancePicker::val2y(int v)
{
int d = height() - 2 * coff - 1;
return coff + (255 - v) * d / 255;
}
QColorLuminancePicker::QColorLuminancePicker(QWidget *parent)
: QWidget(parent)
{
hue = 100;
val = 100;
sat = 100;
pix = 0;
setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
}
QColorLuminancePicker::~QColorLuminancePicker()
{
delete pix;
}
void QColorLuminancePicker::mouseMoveEvent(QMouseEvent *m)
{
setVal(y2val(m->y()));
}
void QColorLuminancePicker::mousePressEvent(QMouseEvent *m)
{
setVal(y2val(m->y()));
}
void QColorLuminancePicker::setVal(int v)
{
if (val == v)
return;
val = qMax(0, qMin(v, 255));
delete pix;
pix = 0;
repaint();
emit newHsv(hue, sat, val);
}
//receives from a hue,sat chooser and relays.
void QColorLuminancePicker::setCol(int h, int s)
{
setCol(h, s, val);
emit newHsv(h, s, val);
}
QSize QColorLuminancePicker::sizeHint() const
{
return QSize(LUMINANCE_PICKER_WIDTH, LUMINANCE_PICKER_HEIGHT);
}
void QColorLuminancePicker::paintEvent(QPaintEvent *)
{
int w = width() - 5;
QRect r(0, foff, w, height() - 2 * foff);
int wi = r.width() - 2;
int hi = r.height() - 2;
if (!pix || pix->height() != hi || pix->width() != wi)
{
delete pix;
QImage img(wi, hi, QImage::Format_RGB32);
int y;
uint *pixel = (uint *)img.scanLine(0);
for (y = 0; y < hi; y++)
{
uint *end = pixel + wi;
std::fill(pixel, end,
QColor::fromHsv(hue, sat, y2val(y + coff)).rgb());
pixel = end;
}
pix = new QPixmap(QPixmap::fromImage(img));
}
QPainter p(this);
p.drawPixmap(1, coff, *pix);
const QPalette &g = palette();
qDrawShadePanel(&p, r, g, true);
p.setPen(g.windowText().color());
p.setBrush(g.windowText());
QPolygon a;
int y = val2y(val);
a.setPoints(3, w, y, w + 5, y + 5, w + 5, y - 5);
p.eraseRect(w, 0, 5, height());
p.drawPolygon(a);
}
void QColorLuminancePicker::setCol(int h, int s, int v)
{
val = v;
hue = h;
sat = s;
delete pix;
pix = 0;
repaint();
}
QPoint QColorPicker::colPt()
{
QRect r = contentsRect();
return QPoint((360 - hue) * (r.width() - 1) / 360,
(255 - sat) * (r.height() - 1) / 255);
}
int QColorPicker::huePt(const QPoint &pt)
{
QRect r = contentsRect();
return 360 - pt.x() * 360 / (r.width() - 1);
}
int QColorPicker::satPt(const QPoint &pt)
{
QRect r = contentsRect();
return 255 - pt.y() * 255 / (r.height() - 1);
}
void QColorPicker::setCol(const QPoint &pt)
{
setCol(huePt(pt), satPt(pt));
}
QColorPicker::QColorPicker(QWidget *parent)
: QFrame(parent)
, crossVisible(true)
{
hue = 0;
sat = 0;
setCol(150, 255);
setAttribute(Qt::WA_NoSystemBackground);
setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
}
QColorPicker::~QColorPicker()
{
}
void QColorPicker::setCrossVisible(bool visible)
{
if (crossVisible != visible)
{
crossVisible = visible;
update();
}
}
QSize QColorPicker::sizeHint() const
{
return QSize(COLOR_PICKER_WIDTH, COLOR_PICKER_HEIGHT);
}
void QColorPicker::setCol(int h, int s)
{
int nhue = qMin(qMax(0, h), 359);
int nsat = qMin(qMax(0, s), 255);
if (nhue == hue && nsat == sat)
return;
QRect r(colPt(), QSize(20, 20));
hue = nhue;
sat = nsat;
r = r.united(QRect(colPt(), QSize(20, 20)));
r.translate(contentsRect().x() - 9, contentsRect().y() - 9);
repaint(r);
}
void QColorPicker::mouseMoveEvent(QMouseEvent *m)
{
QPoint p = m->pos() - contentsRect().topLeft();
setCol(p);
emit newCol(hue, sat);
}
void QColorPicker::mousePressEvent(QMouseEvent *m)
{
QPoint p = m->pos() - contentsRect().topLeft();
setCol(p);
emit newCol(hue, sat);
}
void QColorPicker::paintEvent(QPaintEvent *)
{
QPainter p(this);
drawFrame(&p);
QRect r = contentsRect();
p.drawPixmap(r.topLeft(), pix);
if (crossVisible)
{
QPoint pt = colPt() + r.topLeft();
p.setPen(Qt::black);
p.fillRect(pt.x() - 9, pt.y(), 20, 2, Qt::black);
p.fillRect(pt.x(), pt.y() - 9, 2, 20, Qt::black);
}
}
void QColorPicker::resizeEvent(QResizeEvent *ev)
{
QFrame::resizeEvent(ev);
int w = width() - frameWidth() * 2;
int h = height() - frameWidth() * 2;
QImage img(w, h, QImage::Format_RGB32);
int x, y;
uint *pixel = (uint *)img.scanLine(0);
for (y = 0; y < h; y++)
{
const uint *end = pixel + w;
x = 0;
while (pixel < end)
{
QPoint p(x, y);
QColor c;
c.setHsv(huePt(p), satPt(p), 200);
*pixel = c.rgb();
++pixel;
++x;
}
}
pix = QPixmap::fromImage(img);
}
QColSpinBox::QColSpinBox(QWidget *parent)
: QSpinBox(parent)
{
this->setRange(0, 255);
}
void QColSpinBox::setValue(int i)
{
const QSignalBlocker blocker(this);
QSpinBox::setValue(i);
}
} // namespace chatterino
-131
View File
@@ -1,131 +0,0 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWidgets module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#pragma once
#include <QFrame>
#include <QSpinBox>
namespace chatterino {
/*
* These classes are literally copied from the Qt source.
* Unfortunately, they are private to the QColorDialog class so we cannot use
* them directly.
* If they become public at any point in the future, it should be possible to
* replace every include of this header with the respective includes for the
* QColorPicker, QColorLuminancePicker, and QColSpinBox classes.
*/
class QColorPicker : public QFrame
{
Q_OBJECT
public:
QColorPicker(QWidget *parent);
~QColorPicker() override;
void setCrossVisible(bool visible);
public slots:
void setCol(int h, int s);
signals:
void newCol(int h, int s);
protected:
QSize sizeHint() const override;
void paintEvent(QPaintEvent *) override;
void mouseMoveEvent(QMouseEvent *) override;
void mousePressEvent(QMouseEvent *) override;
void resizeEvent(QResizeEvent *) override;
private:
int hue;
int sat;
QPoint colPt();
int huePt(const QPoint &pt);
int satPt(const QPoint &pt);
void setCol(const QPoint &pt);
QPixmap pix;
bool crossVisible;
};
static const int COLOR_PICKER_WIDTH = 220;
static const int COLOR_PICKER_HEIGHT = 200;
class QColorLuminancePicker : public QWidget
{
Q_OBJECT
public:
QColorLuminancePicker(QWidget *parent = 0);
~QColorLuminancePicker() override;
public slots:
void setCol(int h, int s, int v);
void setCol(int h, int s);
signals:
void newHsv(int h, int s, int v);
protected:
QSize sizeHint() const override;
void paintEvent(QPaintEvent *) override;
void mouseMoveEvent(QMouseEvent *) override;
void mousePressEvent(QMouseEvent *) override;
private:
enum { foff = 3, coff = 4 }; //frame and contents offset
int val;
int hue;
int sat;
int y2val(int y);
int val2y(int val);
void setVal(int v);
QPixmap *pix;
};
static const int LUMINANCE_PICKER_WIDTH = 25;
static const int LUMINANCE_PICKER_HEIGHT = COLOR_PICKER_HEIGHT;
class QColSpinBox : public QSpinBox
{
public:
QColSpinBox(QWidget *parent);
void setValue(int i);
};
} // namespace chatterino
+163
View File
@@ -0,0 +1,163 @@
#include "widgets/helper/color/AlphaSlider.hpp"
#include "widgets/helper/color/Checkerboard.hpp"
#include <QMouseEvent>
#include <QPainterPath>
namespace {
constexpr int SLIDER_WIDTH = 256;
constexpr int SLIDER_HEIGHT = 12;
} // namespace
namespace chatterino {
AlphaSlider::AlphaSlider(QColor color, QWidget *parent)
: QWidget(parent)
, alpha_(color.alpha())
, color_(color)
{
this->setSizePolicy({QSizePolicy::Expanding, QSizePolicy::Fixed});
}
void AlphaSlider::setColor(QColor color)
{
if (this->color_ == color)
{
return;
}
this->alpha_ = color.alpha();
this->color_ = color;
this->cachedPixmap_ = {};
this->update();
}
int AlphaSlider::alpha() const
{
return this->alpha_;
}
QSize AlphaSlider::sizeHint() const
{
return {SLIDER_WIDTH, SLIDER_HEIGHT};
}
void AlphaSlider::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
this->updatePixmap();
}
int AlphaSlider::xPosToAlpha(int xPos) const
{
return (xPos * 255) / (this->width() - this->height());
}
void AlphaSlider::mousePressEvent(QMouseEvent *event)
{
if (event->buttons().testFlag(Qt::MouseButton::LeftButton))
{
this->trackingMouseEvents_ = true;
this->updateFromEvent(event);
this->setFocus(Qt::FocusReason::MouseFocusReason);
}
}
void AlphaSlider::mouseMoveEvent(QMouseEvent *event)
{
if (this->trackingMouseEvents_)
{
this->updateFromEvent(event);
event->accept();
}
}
void AlphaSlider::mouseReleaseEvent(QMouseEvent *event)
{
if (this->trackingMouseEvents_ &&
event->buttons().testFlag(Qt::MouseButton::LeftButton))
{
this->updateFromEvent(event);
this->trackingMouseEvents_ = false;
event->accept();
}
}
void AlphaSlider::updateFromEvent(QMouseEvent *event)
{
int cornerRadius = this->height() / 2;
auto clampedX = std::clamp(event->pos().x(), cornerRadius,
this->width() - cornerRadius);
this->setAlpha(this->xPosToAlpha(clampedX - cornerRadius));
}
void AlphaSlider::updatePixmap()
{
this->cachedPixmap_ = QPixmap(this->size());
this->cachedPixmap_.fill(Qt::transparent);
QPainter painter(&this->cachedPixmap_);
painter.setRenderHint(QPainter::Antialiasing);
qreal cornerRadius = (qreal)this->height() / 2.0;
QPainterPath mask;
mask.addRoundedRect(QRect({0, 0}, this->size()), cornerRadius,
cornerRadius);
painter.setClipPath(mask);
drawCheckerboard(painter, this->size(), this->height() / 2);
QLinearGradient gradient(cornerRadius, 0.0,
(qreal)this->width() - cornerRadius, 0.0);
QColor start = this->color_;
QColor end = this->color_;
start.setAlpha(0);
end.setAlpha(255);
gradient.setColorAt(0.0, start);
gradient.setColorAt(1.0, end);
painter.setPen({Qt::transparent, 0});
painter.setBrush(gradient);
painter.drawRect(QRect({0, 0}, this->size()));
}
void AlphaSlider::paintEvent(QPaintEvent * /*event*/)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
if (this->cachedPixmap_.isNull())
{
this->updatePixmap();
}
painter.drawPixmap(this->rect().topLeft(), this->cachedPixmap_);
int cornerRadius = this->height() / 2;
QPoint circ = {
cornerRadius +
(this->alpha() * (this->width() - 2 * cornerRadius)) / 255,
cornerRadius};
auto circleColor = 0;
painter.setPen({QColor(circleColor, circleColor, circleColor), 2});
auto opaqueBase = this->color_;
opaqueBase.setAlpha(255);
painter.setBrush(opaqueBase);
painter.drawEllipse(circ, cornerRadius - 1, cornerRadius - 1);
}
void AlphaSlider::setAlpha(int alpha)
{
if (this->alpha_ == alpha)
{
return;
}
this->alpha_ = alpha;
this->color_.setAlpha(alpha);
emit this->colorChanged(this->color_);
this->update();
}
} // namespace chatterino
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <QWidget>
namespace chatterino {
class AlphaSlider : public QWidget
{
Q_OBJECT
public:
AlphaSlider(QColor color, QWidget *parent = nullptr);
QSize sizeHint() const override;
int alpha() const;
signals:
void colorChanged(QColor color) const;
public slots:
void setColor(QColor color);
protected:
void resizeEvent(QResizeEvent *event) override;
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
private:
int alpha_ = 255;
QColor color_;
QPixmap cachedPixmap_;
bool trackingMouseEvents_ = false;
void updatePixmap();
int xPosToAlpha(int xPos) const;
void updateFromEvent(QMouseEvent *event);
void setAlpha(int alpha);
};
} // namespace chatterino
+29
View File
@@ -0,0 +1,29 @@
#include "widgets/helper/color/Checkerboard.hpp"
namespace chatterino {
void drawCheckerboard(QPainter &painter, QRect rect, int tileSize)
{
painter.fillRect(rect, QColor(255, 255, 255));
if (tileSize <= 0)
{
tileSize = 1;
}
int overflowY = rect.height() % tileSize == 0 ? 0 : 1;
int overflowX = rect.width() % tileSize == 0 ? 0 : 1;
for (int row = 0; row < rect.height() / tileSize + overflowY; row++)
{
int offsetX = row % 2 == 0 ? 0 : 1;
for (int col = offsetX; col < rect.width() / tileSize + overflowX;
col += 2)
{
painter.fillRect(rect.x() + col * tileSize,
rect.y() + row * tileSize, tileSize, tileSize,
QColor(204, 204, 204));
}
}
}
} // namespace chatterino
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <QPainter>
namespace chatterino {
void drawCheckerboard(QPainter &painter, QRect rect, int tileSize = 4);
inline void drawCheckerboard(QPainter &painter, QSize size, int tileSize = 4)
{
drawCheckerboard(painter, {{0, 0}, size}, tileSize);
}
} // namespace chatterino
+81
View File
@@ -0,0 +1,81 @@
#include "widgets/helper/color/ColorButton.hpp"
#include "widgets/helper/color/Checkerboard.hpp"
#include <QPainterPath>
namespace chatterino {
ColorButton::ColorButton(QColor color, QWidget *parent)
: QAbstractButton(parent)
, currentColor_(color)
{
this->setSizePolicy({QSizePolicy::Expanding, QSizePolicy::Expanding});
this->setMinimumSize({30, 30});
}
QSize ColorButton::sizeHint() const
{
return {50, 30};
}
void ColorButton::setColor(const QColor &color)
{
if (this->currentColor_ == color)
{
return;
}
this->currentColor_ = color;
this->update();
}
QColor ColorButton::color() const
{
return this->currentColor_;
}
void ColorButton::resizeEvent(QResizeEvent * /*event*/)
{
this->checkerboardCacheValid_ = false;
this->repaint();
}
void ColorButton::paintEvent(QPaintEvent * /*event*/)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
auto rect = this->rect();
if (this->currentColor_.alpha() != 255)
{
if (!this->checkerboardCacheValid_)
{
QPixmap cache(this->size());
cache.fill(Qt::transparent);
QPainter cachePainter(&cache);
cachePainter.setRenderHint(QPainter::Antialiasing);
QPainterPath path;
path.addRoundedRect(QRect(1, 1, this->size().width() - 2,
this->size().height() - 2),
5, 5);
cachePainter.setClipPath(path);
drawCheckerboard(cachePainter, this->size(),
std::min(this->height() / 2, 10));
cachePainter.end();
this->checkerboardCache_ = std::move(cache);
this->checkerboardCacheValid_ = true;
}
painter.drawPixmap(rect.topLeft(), this->checkerboardCache_);
}
painter.setBrush(this->currentColor_);
painter.setPen({QColor(255, 255, 255, 127), 1});
painter.drawRoundedRect(rect.x() + 1, rect.y() + 1, rect.width() - 2,
rect.height() - 2, 5, 5);
}
} // namespace chatterino
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <QAbstractButton>
namespace chatterino {
class ColorButton : public QAbstractButton
{
Q_OBJECT
public:
ColorButton(QColor color, QWidget *parent = nullptr);
QSize sizeHint() const override;
QColor color() const;
// NOLINTNEXTLINE(readability-redundant-access-specifiers)
public slots:
void setColor(const QColor &color);
protected:
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private:
QColor currentColor_;
QPixmap checkerboardCache_;
bool checkerboardCacheValid_ = false;
};
} // namespace chatterino
+168
View File
@@ -0,0 +1,168 @@
#include "widgets/helper/color/ColorInput.hpp"
namespace {
// from qtools_p.h
int fromHex(char c) noexcept
{
if (c >= '0' && c <= '9')
{
return int(c - '0');
}
if (c >= 'A' && c <= 'F')
{
return int(c - 'A' + 10);
}
if (c >= 'a' && c <= 'f')
{
return int(c - 'a' + 10);
}
return -1;
}
QColor parseHexColor(const QString &text)
{
if (text.length() == 5) // #rgba
{
auto alphaHex = fromHex(text[4].toLatin1());
QStringView v(text);
v.chop(1);
QColor col(v);
col.setAlpha(alphaHex);
return col;
}
QColor col(text);
if (col.isValid() && text.length() == 9) // #rrggbbaa
{
auto rgba = col.rgba();
auto alpha = rgba & 0xff;
QColor actual(rgba >> 8);
actual.setAlpha((int)alpha);
return actual;
}
return col;
}
} // namespace
namespace chatterino {
ColorInput::ColorInput(QColor color, QWidget *parent)
: QWidget(parent)
, currentColor_(color)
, hexValidator_(QRegularExpression(
R"(^#([A-Fa-f\d]{3,4}|[A-Fa-f\d]{6}|[A-Fa-f\d]{8})$)"))
, layout_(this)
{
int row = 0;
const auto initComponent = [&](Component &component, auto label,
auto applyToColor) {
component.lbl.setText(label);
component.box.setRange(0, 255);
QObject::connect(&component.box,
qOverload<int>(&QSpinBox::valueChanged), this,
[this, &component, applyToColor](int value) {
if (component.value == value)
{
return;
}
applyToColor(this->currentColor_, value);
this->emitUpdate();
});
this->layout_.addWidget(&component.lbl, row, 0);
this->layout_.addWidget(&component.box, row, 1);
row++;
};
initComponent(this->red_, "Red:", [](auto &color, int value) {
color.setRed(value);
});
initComponent(this->green_, "Green:", [](auto &color, int value) {
color.setGreen(value);
});
initComponent(this->blue_, "Red:", [](auto &color, int value) {
color.setBlue(value);
});
initComponent(this->alpha_, "Alpha:", [](auto &color, int value) {
color.setAlpha(value);
});
this->hexLabel_.setText("Hex:");
this->hexInput_.setValidator(&this->hexValidator_);
QObject::connect(&this->hexInput_, &QLineEdit::editingFinished, [this]() {
auto css = parseHexColor(this->hexInput_.text());
if (!css.isValid() || this->currentColor_ == css)
{
return;
}
this->currentColor_ = css;
this->emitUpdate();
});
this->layout_.addWidget(&this->hexLabel_, row, 0);
this->layout_.addWidget(&this->hexInput_, row, 1);
this->updateComponents();
}
void ColorInput::updateComponents()
{
auto color = this->currentColor_.toRgb();
const auto updateComponent = [](Component &component, auto getValue) {
int value = getValue();
if (component.value != value)
{
component.value = value;
component.box.setValue(value);
}
};
updateComponent(this->red_, [&]() {
return color.red();
});
updateComponent(this->green_, [&]() {
return color.green();
});
updateComponent(this->blue_, [&]() {
return color.blue();
});
updateComponent(this->alpha_, [&]() {
return color.alpha();
});
this->updateHex();
}
void ColorInput::updateHex()
{
auto rgb = this->currentColor_.rgb();
rgb <<= 8;
rgb |= this->currentColor_.alpha();
// we always need to update the CSS color
this->hexInput_.setText(QStringLiteral("#%1").arg(rgb, 8, 16, QChar(u'0')));
}
QColor ColorInput::color() const
{
return this->currentColor_;
}
void ColorInput::setColor(QColor color)
{
if (this->currentColor_ == color)
{
return;
}
this->currentColor_ = color;
this->updateComponents();
// no emit, as we just got the updated color
}
void ColorInput::emitUpdate()
{
this->updateComponents();
// our components triggered this update, emit the new color
emit this->colorChanged(this->currentColor_);
}
} // namespace chatterino
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QSpinBox>
#include <QWidget>
namespace chatterino {
class ColorInput : public QWidget
{
Q_OBJECT
public:
ColorInput(QColor color, QWidget *parent = nullptr);
QColor color() const;
signals:
void colorChanged(QColor color);
public slots:
void setColor(QColor color);
private:
QColor currentColor_;
struct Component {
QLabel lbl;
QSpinBox box;
int value = -1;
};
Component red_;
Component green_;
Component blue_;
Component alpha_;
QLabel hexLabel_;
QLineEdit hexInput_;
QRegularExpressionValidator hexValidator_;
QGridLayout layout_;
void updateComponents();
void updateHex();
void emitUpdate();
};
} // namespace chatterino
@@ -0,0 +1,35 @@
#include "widgets/helper/color/ColorItemDelegate.hpp"
#include "widgets/helper/color/Checkerboard.hpp"
namespace chatterino {
ColorItemDelegate::ColorItemDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{
}
void ColorItemDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
auto data = index.data(Qt::DecorationRole);
if (data.type() != QVariant::Color)
{
return QStyledItemDelegate::paint(painter, option, index);
}
auto color = data.value<QColor>();
painter->save();
if (color.alpha() != 255)
{
drawCheckerboard(*painter, option.rect,
std::min(option.rect.height() / 2, 10));
}
painter->setBrush(color);
painter->drawRect(option.rect);
painter->restore();
}
} // namespace chatterino
@@ -0,0 +1,16 @@
#pragma once
#include <QStyledItemDelegate>
namespace chatterino {
class ColorItemDelegate : public QStyledItemDelegate
{
public:
explicit ColorItemDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
};
} // namespace chatterino
+165
View File
@@ -0,0 +1,165 @@
#include "widgets/helper/color/HueSlider.hpp"
#include <QMouseEvent>
#include <QPainter>
namespace {
constexpr int SLIDER_WIDTH = 256;
constexpr int SLIDER_HEIGHT = 12;
} // namespace
namespace chatterino {
HueSlider::HueSlider(QColor color, QWidget *parent)
: QWidget(parent)
{
this->setColor(color);
this->setSizePolicy({QSizePolicy::Expanding, QSizePolicy::Fixed});
}
void HueSlider::setColor(QColor color)
{
if (this->color_ == color)
{
return;
}
this->color_ = color.toHsv();
auto hue = std::max(this->color_.hue(), 0);
if (this->hue_ == hue)
{
return;
}
this->hue_ = hue;
this->update();
}
int HueSlider::hue() const
{
return this->hue_;
}
QSize HueSlider::sizeHint() const
{
return {SLIDER_WIDTH, SLIDER_HEIGHT};
}
void HueSlider::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
this->updatePixmap();
}
int HueSlider::xPosToHue(int xPos) const
{
return (xPos * 359) / (this->width() - this->height());
}
void HueSlider::mousePressEvent(QMouseEvent *event)
{
if (event->buttons().testFlag(Qt::MouseButton::LeftButton))
{
this->trackingMouseEvents_ = true;
this->updateFromEvent(event);
event->accept();
this->setFocus(Qt::FocusReason::MouseFocusReason);
}
}
void HueSlider::mouseMoveEvent(QMouseEvent *event)
{
if (this->trackingMouseEvents_)
{
this->updateFromEvent(event);
event->accept();
}
}
void HueSlider::mouseReleaseEvent(QMouseEvent *event)
{
if (this->trackingMouseEvents_ &&
event->buttons().testFlag(Qt::MouseButton::LeftButton))
{
this->updateFromEvent(event);
this->trackingMouseEvents_ = false;
event->accept();
}
}
void HueSlider::updateFromEvent(QMouseEvent *event)
{
int cornerRadius = this->height() / 2;
auto clampedX = std::clamp(event->pos().x(), cornerRadius,
this->width() - cornerRadius);
this->setHue(this->xPosToHue(clampedX - cornerRadius));
}
void HueSlider::updatePixmap()
{
constexpr int nStops = 10;
constexpr auto nStopsF = (qreal)nStops;
this->gradientPixmap_ = QPixmap(this->size());
this->gradientPixmap_.fill(Qt::transparent);
QPainter painter(&this->gradientPixmap_);
painter.setRenderHint(QPainter::Antialiasing);
qreal cornerRadius = (qreal)this->height() / 2.0;
QLinearGradient gradient(cornerRadius, 0.0,
(qreal)this->width() - cornerRadius, 0.0);
for (int i = 0; i <= nStops; i++)
{
gradient.setColorAt(
(qreal)i / nStopsF,
QColor::fromHsv(std::min((i * 360) / nStops, 359), 255, 255));
}
painter.setPen({Qt::transparent, 0});
painter.setBrush(gradient);
painter.drawRoundedRect(QRect({0, 0}, this->size()), cornerRadius,
cornerRadius);
}
void HueSlider::paintEvent(QPaintEvent * /*event*/)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
if (this->gradientPixmap_.isNull())
{
this->updatePixmap();
}
painter.drawPixmap(this->rect().topLeft(), this->gradientPixmap_);
int cornerRadius = this->height() / 2;
QPoint circ = {
cornerRadius + (this->hue() * (this->width() - 2 * cornerRadius)) / 360,
cornerRadius};
auto circleColor = 0;
painter.setPen({QColor(circleColor, circleColor, circleColor), 2});
painter.setBrush(QColor::fromHsv(this->hue(), 255, 255));
painter.drawEllipse(circ, cornerRadius - 1, cornerRadius - 1);
}
void HueSlider::setHue(int hue)
{
if (this->hue_ == hue)
{
return;
}
this->hue_ = hue;
// ugh
int h{};
int s{};
int v{};
int a{};
this->color_.getHsv(&h, &s, &v, &a);
this->color_.setHsv(this->hue_, s, v, a);
emit this->colorChanged(this->color_);
this->update();
}
} // namespace chatterino
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <QWidget>
namespace chatterino {
class HueSlider : public QWidget
{
Q_OBJECT
public:
HueSlider(QColor color, QWidget *parent = nullptr);
QSize sizeHint() const override;
int hue() const;
signals:
void colorChanged(QColor color) const;
public slots:
void setColor(QColor color);
protected:
void resizeEvent(QResizeEvent *event) override;
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
private:
int hue_ = 0;
QColor color_;
QPixmap gradientPixmap_;
bool trackingMouseEvents_ = false;
void updatePixmap();
int xPosToHue(int xPos) const;
void updateFromEvent(QMouseEvent *event);
void setHue(int hue);
};
} // namespace chatterino
+192
View File
@@ -0,0 +1,192 @@
#include "widgets/helper/color/SBCanvas.hpp"
#include <QMouseEvent>
#include <QPainter>
namespace {
constexpr int PICKER_WIDTH = 256;
constexpr int PICKER_HEIGHT = 256;
} // namespace
namespace chatterino {
SBCanvas::SBCanvas(QColor color, QWidget *parent)
: QWidget(parent)
{
this->setColor(color);
this->setSizePolicy({QSizePolicy::Fixed, QSizePolicy::Fixed});
}
void SBCanvas::setColor(QColor color)
{
color = color.toHsv();
if (this->color_ == color)
{
return;
}
this->color_ = color;
int h{};
int s{};
int v{};
color.getHsv(&h, &s, &v);
h = std::max(h, 0);
if (this->hue_ == h && this->saturation_ == s && this->brightness_ == v)
{
return; // alpha changed
}
this->hue_ = h;
this->saturation_ = s;
this->brightness_ = v;
this->gradientPixmap_ = {};
this->update();
}
int SBCanvas::saturation() const
{
return this->saturation_;
}
int SBCanvas::brightness() const
{
return this->brightness_;
}
QSize SBCanvas::sizeHint() const
{
return {PICKER_WIDTH, PICKER_HEIGHT};
}
void SBCanvas::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
this->updatePixmap();
}
int SBCanvas::xPosToSaturation(int xPos) const
{
return (xPos * 255) / this->width();
}
int SBCanvas::yPosToBrightness(int yPos) const
{
return 255 - (yPos * 255) / this->height();
}
void SBCanvas::mousePressEvent(QMouseEvent *event)
{
if (event->buttons().testFlag(Qt::MouseButton::LeftButton))
{
this->trackingMouseEvents_ = true;
this->updateFromEvent(event);
event->accept();
this->setFocus(Qt::FocusReason::MouseFocusReason);
}
}
void SBCanvas::mouseMoveEvent(QMouseEvent *event)
{
if (this->trackingMouseEvents_)
{
this->updateFromEvent(event);
event->accept();
}
}
void SBCanvas::mouseReleaseEvent(QMouseEvent *event)
{
if (this->trackingMouseEvents_ &&
event->buttons().testFlag(Qt::MouseButton::LeftButton))
{
this->updateFromEvent(event);
this->trackingMouseEvents_ = false;
event->accept();
}
}
void SBCanvas::updateFromEvent(QMouseEvent *event)
{
auto clampedX = std::clamp(event->pos().x(), 0, this->width());
auto clampedY = std::clamp(event->pos().y(), 0, this->height());
bool updated = this->setSaturation(this->xPosToSaturation(clampedX));
updated |= this->setBrightness(this->yPosToBrightness(clampedY));
if (updated)
{
this->emitUpdatedColor();
this->update();
}
}
void SBCanvas::updatePixmap()
{
int w = this->width();
int h = this->height();
QImage img(w, h, QImage::Format_RGB32);
uint *pixel = (uint *)img.scanLine(0);
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
QColor c = QColor::fromHsv(this->hue_, this->xPosToSaturation(x),
this->yPosToBrightness(y));
*pixel = c.rgb();
pixel++;
}
}
this->gradientPixmap_ = QPixmap::fromImage(img);
}
void SBCanvas::paintEvent(QPaintEvent * /*event*/)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
if (this->gradientPixmap_.isNull())
{
this->updatePixmap();
}
painter.drawPixmap(this->rect().topLeft(), this->gradientPixmap_);
QPoint circ = {(this->saturation() * this->width()) / 256,
((255 - this->brightness()) * this->height()) / 256};
auto circleColor = this->brightness() >= 128 ? 50 : 200;
painter.setPen({QColor(circleColor, circleColor, circleColor), 2});
painter.setBrush(
QColor::fromHsv(this->hue_, this->saturation_, this->brightness_));
painter.drawEllipse(circ, 5, 5);
}
bool SBCanvas::setSaturation(int saturation)
{
if (this->saturation_ == saturation)
{
return false;
}
this->saturation_ = saturation;
return true;
}
bool SBCanvas::setBrightness(int brightness)
{
if (this->brightness_ == brightness)
{
return false;
}
this->brightness_ = brightness;
return true;
}
void SBCanvas::emitUpdatedColor()
{
this->color_.setHsv(this->hue_, this->saturation_, this->brightness_,
this->color_.alpha());
emit this->colorChanged(this->color_);
}
} // namespace chatterino
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <QWidget>
namespace chatterino {
/// 2D canvas for saturation (x-axis) and brightness (y-axis)
class SBCanvas : public QWidget
{
Q_OBJECT
public:
SBCanvas(QColor color, QWidget *parent = nullptr);
QSize sizeHint() const override;
int saturation() const;
int brightness() const;
signals:
void colorChanged(QColor color) const;
public slots:
void setColor(QColor color);
protected:
void resizeEvent(QResizeEvent *event) override;
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
private:
int hue_ = 0;
int saturation_ = 0;
int brightness_ = 0;
QColor color_;
QPixmap gradientPixmap_;
bool trackingMouseEvents_ = false;
void updatePixmap();
int xPosToSaturation(int xPos) const;
int yPosToBrightness(int yPos) const;
void updateFromEvent(QMouseEvent *event);
[[nodiscard]] bool setSaturation(int saturation);
[[nodiscard]] bool setBrightness(int brightness);
void emitUpdatedColor();
};
} // namespace chatterino