456 lines
15 KiB
C++
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 ¢re, 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);
|
|
}
|