AdvancedControllerRemoteCPP/panel/panelwidget.cpp

456 lines
15 KiB
C++

// panelwidget.cpp - the front panel of the AdvancedController (see panelwidget.h)
#include "panelwidget.h"
#include "remoteclient.h"
#include "style.h"
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPolygonF>
#include <algorithm>
#include <cmath>
#include <initializer_list>
namespace {
// Panel geometry in "units" (1 unit = distance between button centres)
const double PanelW = 12.5;
const double PanelH = 7.0;
const double Btn = 0.86; // button side
const double LedR = 0.045; // led radius
const double Pi = 3.14159265358979323846;
enum class Icon {
LedOn, LedAuto, Agc, Sun, Zoom, Mirror, Flip, Focus,
Sliders, Sliders1, Sliders2, Save1, Save2, Xh1, Xh2, Temp, Camera, Gear,
Up, Down, Left, Right, Enter
};
struct Button
{
const char *name; // name for "KEY"
const char *longName; // name for a long press, nullptr = none
double x; // centre, units
double y;
Icon icon;
const char *leds[2]; // leds shown on this button
};
const Button Buttons[] = {
{"LEDON", nullptr, 2.75, 1.55, Icon::LedOn, {"LEDON", nullptr}},
{"LEDAUTO", nullptr, 3.75, 1.55, Icon::LedAuto, {"LEDAUTO", nullptr}},
{"AGC", nullptr, 4.75, 1.55, Icon::Agc, {"AGC", nullptr}},
{"BRIGHT", nullptr, 5.75, 1.55, Icon::Sun, {"BRIGHT", nullptr}},
{"ZOOM", nullptr, 6.75, 1.55, Icon::Zoom, {"ZOOM", nullptr}},
{"MIRROR", nullptr, 7.75, 1.55, Icon::Mirror, {"MIRROR", nullptr}},
{"FLIP", nullptr, 8.75, 1.55, Icon::Flip, {"FLIP", nullptr}},
{"FOCUS", nullptr, 9.75, 1.55, Icon::Focus, {"FOCUS", nullptr}},
{"CAMRESET", nullptr, 5.75, 2.80, Icon::Sliders, {"CAMMENU", nullptr}}, // camera reset (its led blinks during the reset)
{"LOAD1", nullptr, 6.75, 2.80, Icon::Sliders1, {"LOAD1", nullptr}},
{"LOAD2", nullptr, 7.75, 2.80, Icon::Sliders2, {"LOAD2", nullptr}},
{"SAVE1", nullptr, 8.75, 2.80, Icon::Save1, {"SAVE1", nullptr}},
{"SAVE2", nullptr, 9.75, 2.80, Icon::Save2, {"SAVE2", nullptr}},
{"XH1", "XH1LONG", 7.75, 4.05, Icon::Xh1, {"XH1", nullptr}},
{"XH2", "XH2LONG", 8.75, 4.05, Icon::Xh2, {"XH2", nullptr}},
{"TEMP", nullptr, 9.75, 4.05, Icon::Temp, {"TEMP", nullptr}},
{"CAMSEL", nullptr, 8.75, 5.30, Icon::Camera, {"CAM1SEL", "CAM2SEL"}},
{"SYSRESET", nullptr, 9.75, 5.30, Icon::Gear, {"SYSRESET", nullptr}}, // gear: leave all adjust modes; led = remote-control sign
{"UP", nullptr, 3.75, 3.25, Icon::Up, {"UP", nullptr}},
{"LEFT", nullptr, 2.75, 4.25, Icon::Left, {"LEFT", nullptr}},
{"ENTER", "LONGENTER", 3.75, 4.25, Icon::Enter, {"ENTER", nullptr}},
{"RIGHT", nullptr, 4.75, 4.25, Icon::Right, {"RIGHT", nullptr}},
{"DOWN", nullptr, 3.75, 5.30, Icon::Down, {"DOWN", nullptr}},
};
// Every icon is drawn inside the unit square (-0.5..0.5) around the button centre,
// line widths in pixels as on the device drawing (2 px, thick lines 3 px).
class IconPainter
{
public:
IconPainter(QPainter &painter, const QPointF &centre, double size)
: m_p(painter), m_c(centre), m_s(size)
{
}
void draw(Icon icon)
{
switch (icon) {
case Icon::LedOn: headlight(QString()); break;
case Icon::LedAuto: headlight(QStringLiteral("A")); break;
case Icon::Agc: agc(); break;
case Icon::Sun: sun(); break;
case Icon::Zoom: zoom(); break;
case Icon::Mirror: mirror(); break;
case Icon::Flip: flip(); break;
case Icon::Focus: focus(); break;
case Icon::Sliders: sliders(QString()); break;
case Icon::Sliders1: sliders(QStringLiteral("1")); break;
case Icon::Sliders2: sliders(QStringLiteral("2")); break;
case Icon::Save1: save(QStringLiteral("1")); break;
case Icon::Save2: save(QStringLiteral("2")); break;
case Icon::Xh1: xh1(); break;
case Icon::Xh2: xh2(); break;
case Icon::Temp: temp(); break;
case Icon::Camera: camera(); break;
case Icon::Gear: gear(); break;
case Icon::Up: arrow(0, -1); break;
case Icon::Down: arrow(0, 1); break;
case Icon::Left: arrow(-1, 0); break;
case Icon::Right: arrow(1, 0); break;
case Icon::Enter: enter(); break;
}
}
private:
QPointF p(double x, double y) const
{
return QPointF(m_c.x() + x * m_s, m_c.y() + y * m_s);
}
QPolygonF points(std::initializer_list<QPointF> unit) const
{
QPolygonF result;
for (const QPointF &u : unit)
result << p(u.x(), u.y());
return result;
}
QPen pen(double width, Qt::PenJoinStyle join = Qt::RoundJoin) const
{
return QPen(Style::Line, width, Qt::SolidLine, Qt::RoundCap, join);
}
void line(const QPolygonF &pts, double width = 2)
{
m_p.setPen(pen(width));
m_p.setBrush(Qt::NoBrush);
m_p.drawPolyline(pts);
}
void dashedLine(const QPolygonF &pts, double width = 2)
{
QPen dashed(Style::Line, width, Qt::CustomDashLine, Qt::FlatCap);
dashed.setDashPattern({6 / width, 4 / width}); // 6 px dash, 4 px gap
m_p.setPen(dashed);
m_p.setBrush(Qt::NoBrush);
m_p.drawPolyline(pts);
}
void poly(const QPolygonF &pts, bool filled = false, double width = 2)
{
m_p.setPen(pen(width));
m_p.setBrush(filled ? QBrush(Style::Line) : QBrush(Qt::NoBrush));
m_p.drawPolygon(pts);
}
void circle(double x, double y, double r, bool filled = false, double width = 2)
{
m_p.setPen(pen(width));
m_p.setBrush(filled ? QBrush(Style::Line) : QBrush(Qt::NoBrush));
m_p.drawEllipse(p(x, y), r * m_s, r * m_s);
}
void rect(double x0, double y0, double x1, double y1, bool filled = false, double width = 2)
{
m_p.setPen(pen(width, Qt::MiterJoin));
m_p.setBrush(filled ? QBrush(Style::Line) : QBrush(Qt::NoBrush));
m_p.drawRect(QRectF(p(x0, y0), p(x1, y1)));
}
void text(double x, double y, const QString &s, double size)
{
QFont font(QStringLiteral("Arial"));
font.setStyleHint(QFont::SansSerif);
font.setBold(true);
font.setPointSize(std::max(6, int(m_s * size)));
m_p.setFont(font);
m_p.setPen(Style::Line);
const QPointF c = p(x, y);
m_p.drawText(QRectF(c.x() - m_s, c.y() - m_s, 2 * m_s, 2 * m_s), Qt::AlignCenter, s);
}
// A line with an arrowhead at its end. shape = (d1, d2, d3) in pixels: distance from the
// neck to the tip, from the trailing points to the tip, from the line edge to the trailing points.
void arrowLine(QPolygonF pts, double width, double d1, double d2, double d3)
{
const QPointF tip = pts.last();
QPointF dir = tip - pts.at(pts.size() - 2);
dir /= std::hypot(dir.x(), dir.y());
const QPointF normal(-dir.y(), dir.x());
const QPointF neck = tip - dir * d1;
const QPointF base = tip - dir * d2;
const double half = width / 2 + d3;
pts.last() = neck;
line(pts, width);
m_p.setPen(Qt::NoPen);
m_p.setBrush(Style::Line);
m_p.drawPolygon(QPolygonF({tip, base + normal * half, neck, base - normal * half}));
}
// --- the icons -------------------------------------------------------------
void headlight(const QString &letter)
{
// "D" shaped lamp + three beams
QPolygonF pts = points({{-0.30, -0.19}, {-0.06, -0.19}});
for (int i = 1; i < 8; ++i) {
const double a = -Pi / 2 + Pi * i / 8;
pts << p(-0.06 + 0.19 * std::cos(a), 0.19 * std::sin(a));
}
pts << p(-0.06, 0.19) << p(-0.30, 0.19);
poly(pts);
for (const double y : {-0.14, 0.0, 0.14})
line(points({{0.17, y}, {0.34, y}}));
if (!letter.isEmpty())
text(-0.12, 0.0, letter, 0.24);
}
void agc()
{
rect(-0.30, -0.26, 0.06, 0.02);
rect(-0.06, -0.02, 0.30, 0.26, true);
}
void sun()
{
circle(0, 0, 0.13);
for (int i = 0; i < 8; ++i) {
const double a = i * Pi / 4;
line(points({{0.20 * std::cos(a), 0.20 * std::sin(a)}, {0.31 * std::cos(a), 0.31 * std::sin(a)}}));
}
}
void zoom()
{
circle(-0.06, -0.06, 0.17);
line(points({{0.07, 0.07}, {0.29, 0.29}}), 3);
}
void mirror()
{
poly(points({{-0.06, -0.27}, {-0.06, 0.27}, {-0.33, 0.0}}));
poly(points({{0.06, -0.27}, {0.06, 0.27}, {0.33, 0.0}}), true);
}
void flip()
{
poly(points({{-0.27, -0.06}, {0.27, -0.06}, {0.0, -0.33}}));
poly(points({{-0.27, 0.06}, {0.27, 0.06}, {0.0, 0.33}}), true);
}
void focus()
{
circle(0, 0, 0.22);
circle(0, 0, 0.06, true);
const int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (const auto &d : dirs)
line(points({{0.15 * d[0], 0.15 * d[1]}, {0.30 * d[0], 0.30 * d[1]}}));
}
void sliders(const QString &label)
{
double ys[3] = {0.22, 0.22, 0.22};
if (label == QLatin1String("1")) {
ys[0] = -0.02; ys[1] = 0.12; ys[2] = -0.14;
} else if (label == QLatin1String("2")) {
ys[0] = 0.10; ys[1] = -0.12; ys[2] = 0.02;
}
const double xs[3] = {-0.20, 0.0, 0.20};
for (int i = 0; i < 3; ++i) {
line(points({{xs[i], -0.30}, {xs[i], 0.28}}));
circle(xs[i], ys[i], 0.055, true);
}
if (!label.isEmpty())
text(0.30, 0.30, label, 0.18);
}
void save(const QString &label)
{
arrowLine(points({{0.0, -0.32}, {0.0, 0.06}}), 3, 10, 12, 5);
line(points({{-0.28, 0.10}, {-0.28, 0.24}, {0.28, 0.24}, {0.28, 0.10}}));
text(0.0, 0.17, label, 0.16);
}
void xh1()
{
// crosshair 1: long horizontal line near the top, long vertical line on the left
line(points({{-0.36, -0.18}, {0.36, -0.18}}));
line(points({{-0.17, -0.36}, {-0.17, 0.36}}));
}
void xh2()
{
// crosshair 1 (dashed) + crosshair 2 (solid, lower right) - as on the device
dashedLine(points({{-0.36, -0.18}, {0.36, -0.18}}));
dashedLine(points({{-0.17, -0.36}, {-0.17, 0.36}}));
line(points({{-0.36, 0.30}, {0.36, 0.30}}));
line(points({{0.26, -0.36}, {0.26, 0.36}}));
}
void temp()
{
rect(-0.08, -0.32, 0.08, 0.10);
circle(0.0, 0.18, 0.14, true);
line(points({{0.0, -0.10}, {0.0, 0.10}}), 3);
}
void camera()
{
// two cameras as on the device: body + a lens that narrows to the right;
// the upper one outlined, the lower one filled
const struct { double y0; bool filled; } cams[2] = {{-0.35, false}, {0.03, true}};
for (const auto &cam : cams) {
const double y0 = cam.y0;
const double y1 = y0 + 0.30;
rect(-0.33, y0, 0.11, y1, cam.filled);
poly(points({{0.11, y0 + 0.04}, {0.27, y0 + 0.09}, {0.27, y1 - 0.09}, {0.11, y1 - 0.04}}), cam.filled);
}
}
void gear()
{
QPolygonF pts;
for (int i = 0; i < 16; ++i) {
const double r = i % 2 == 0 ? 0.31 : 0.23;
const double a = i * Pi / 8;
pts << p(r * std::cos(a), r * std::sin(a));
}
poly(pts);
circle(0, 0, 0.09);
}
void arrow(int dx, int dy)
{
arrowLine(points({{-0.28 * dx, -0.28 * dy}, {0.28 * dx, 0.28 * dy}}), 3, 12, 14, 6);
}
void enter()
{
arrowLine(points({{0.24, -0.24}, {0.24, 0.06}, {-0.26, 0.06}}), 3, 12, 14, 6);
}
QPainter &m_p;
QPointF m_c;
double m_s;
};
} // namespace
PanelWidget::PanelWidget(QWidget *parent)
: QWidget(parent)
{
setMinimumSize(200, 120);
}
void PanelWidget::setLeds(quint32 mask)
{
if (mask == m_mask)
return;
m_mask = mask;
update();
}
void PanelWidget::setInteractive(bool on)
{
m_interactive = on;
}
PanelWidget::Geometry PanelWidget::geometry() const
{
const double w = std::max(width(), 200);
const double h = std::max(height(), 120);
const double scale = std::min(w / PanelW, h / PanelH);
return {scale, (w - PanelW * scale) / 2, (h - PanelH * scale) / 2};
}
void PanelWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.fillRect(rect(), Style::Background);
const Geometry g = geometry();
painter.setPen(QPen(Qt::black, 3));
painter.setBrush(Style::Panel);
painter.drawRect(QRectF(g.ox, g.oy, PanelW * g.scale, PanelH * g.scale));
const double s = Btn * g.scale;
for (const Button &b : Buttons) {
const QPointF c(g.ox + b.x * g.scale, g.oy + b.y * g.scale);
const bool held = m_pressed.contains(QLatin1String(b.name));
painter.setPen(QPen(Style::Line, held ? 3 : 1, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin));
painter.setBrush(Style::Panel);
painter.drawRect(QRectF(c.x() - s / 2, c.y() - s / 2, s, s));
IconPainter(painter, c, s).draw(b.icon);
for (int i = 0; i < 2 && b.leds[i]; ++i) {
const qsizetype bit = AdvCtrl::RemoteClient::LedNames.indexOf(QLatin1String(b.leds[i]));
const bool on = bit >= 0 && (m_mask & (1u << bit));
const QPointF led(c.x() + s / 2 - 0.11 * s, c.y() - s / 2 + 0.11 * s + i * 0.16 * s);
const double r = LedR * g.scale;
painter.setPen(Qt::NoPen);
painter.setBrush(on ? Style::LedOn : Style::LedOff);
painter.drawEllipse(led, r, r);
}
}
}
QString PanelWidget::buttonAt(const QPointF &pos) const
{
const Geometry g = geometry();
const double s = Btn * g.scale;
for (const Button &b : Buttons) {
const QPointF c(g.ox + b.x * g.scale, g.oy + b.y * g.scale);
if (QRectF(c.x() - s / 2, c.y() - s / 2, s, s).contains(pos))
return QLatin1String(b.name);
}
return QString();
}
void PanelWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton)
return;
m_mouseButton = buttonAt(event->position());
if (!m_mouseButton.isEmpty())
press(m_mouseButton);
}
void PanelWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton || m_mouseButton.isEmpty())
return;
const QString button = m_mouseButton;
m_mouseButton.clear();
release(button); // the button pressed, also when the mouse moved away
}
void PanelWidget::press(const QString &button)
{
if (!m_interactive || m_pressed.contains(button))
return;
QElapsedTimer held;
held.start();
m_pressed.insert(button, held);
update();
}
void PanelWidget::release(const QString &button)
{
const auto it = m_pressed.constFind(button);
if (it == m_pressed.constEnd())
return;
const qint64 heldMs = it->elapsed();
m_pressed.erase(it);
update();
QString key = button;
for (const Button &b : Buttons) {
if (button == QLatin1String(b.name) && b.longName && heldMs >= LongPressMs)
key = QLatin1String(b.longName);
}
emit keyPressed(key);
}