1#include "widgets/stream_cell.hpp"
12#include <QStyleOption>
16#include "helpers/icon_loader.hpp"
29 setFocusPolicy(Qt::StrongFocus);
30 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
31 repaint_timer.start();
39 return draft_line_points_pct;
66 hover_point_pct.reset();
73 const QString& n,
const QColor& color,
const bool closed
76 draft_line_color = color;
82 draft_line_points_pct = pts;
87 draft_line_points_pct.clear();
88 hover_point_pct.reset();
94 const std::vector<line_instance>& lines
96 persistent_lines = lines;
101 persistent_lines.push_back(line);
106 persistent_lines.clear();
129 last_frame = QImage();
131 player->setSource(source);
141 last_frame = QImage();
149 camera->deleteLater();
154 session =
new QMediaCaptureSession(
this);
155 session->setVideoSink(sink);
158 QCameraDevice device;
159 const auto cams = QMediaDevices::videoInputs();
160 for (
const auto& c : cams) {
167 if (device.isNull()) {
168 last_error = tr(
"camera not found");
173 camera =
new QCamera(device,
this);
174 session->setCamera(camera);
177 camera, &QCamera::errorOccurred,
this, &stream_cell::on_camera_error
187 e.ts = QDateTime::currentDateTime();
201 if (line_name.isEmpty()) {
204 line_highlights[line_name] = QDateTime::currentDateTime();
209 const QString& line_name,
const QPointF& pos_pct
211 if (line_name.isEmpty()) {
217 h.ts = QDateTime::currentDateTime();
218 line_hits[line_name] = h;
219 line_highlights[line_name] = h.ts;
230 style()->drawPrimitive(QStyle::PE_Widget, &opt, &p,
this);
232 if (!last_frame.isNull()) {
233 p.drawImage(rect(), last_frame);
235 const QString txt = last_error.isEmpty() ?
"no signal" : last_error;
236 const QRect r = rect().adjusted(6, 6, -6, -6);
237 p.setPen(palette().color(QPalette::Text));
238 p.drawText(r, Qt::AlignCenter, txt);
243 p.drawRect(rect().adjusted(0, 0, -1, -1));
244 p.setRenderHint(QPainter::Antialiasing,
true);
246 const auto now = QDateTime::currentDateTime();
248 for (
auto it = line_highlights.begin(); it != line_highlights.end();) {
249 const int age =
static_cast<
int>(it.value().msecsTo(now));
251 it = line_highlights.erase(it);
267 QWidget::mousePressEvent(event);
271 auto* child = childAt(event->pos());
273 QWidget::mousePressEvent(event);
277 if (event->button() == Qt::LeftButton) {
279 draft_line_points_pct.push_back(to_pct(event->pos()));
285 QWidget::mousePressEvent(event);
290 QWidget::mouseMoveEvent(event);
294 hover_point_pct = to_pct(event->pos());
300 hover_point_pct.reset();
302 QWidget::leaveEvent(event);
307 QWidget::keyPressEvent(event);
311 const bool undo_key = (event->key() == Qt::Key_Backspace)
312 || (event->key() == Qt::Key_Z
313 && (event->modifiers() & Qt::ControlModifier));
316 QWidget::keyPressEvent(event);
320 if (!draft_line_points_pct.empty()) {
321 draft_line_points_pct.pop_back();
322 hover_point_pct.reset();
330 const auto root =
new QVBoxLayout(
this);
331 root->setContentsMargins(6, 6, 6, 6);
334 const auto top_row =
new QHBoxLayout();
335 top_row->setContentsMargins(0, 0, 0, 0);
336 top_row->setSpacing(4);
338 top_row->addStretch();
340 focus_btn =
new QPushButton(
this);
344 focus_btn->setFocusPolicy(Qt::NoFocus);
348 close_btn =
new QPushButton(
this);
353 close_btn->setFocusPolicy(Qt::NoFocus);
358 {
"window-close",
"dialog-close",
"edit-delete" },
359 QStyle::SP_TitleBarCloseButton
365 {
"window-close",
"dialog-close" }, QStyle::SP_TitleBarCloseButton
370 root->addLayout(top_row);
373 sink =
new QVideoSink(
this);
375 sink, &QVideoSink::videoFrameChanged,
this,
376 &stream_cell::on_frame_changed
379 player =
new QMediaPlayer(
this);
380 player->setVideoOutput(sink);
384 connect(close_btn, &QPushButton::clicked,
this, [
this]() {
385 emit request_close(name);
387 connect(focus_btn, &QPushButton::clicked,
this, [
this]() {
388 emit request_focus(name);
391 player, &QMediaPlayer::mediaStatusChanged,
this,
392 &stream_cell::on_media_status_changed
395 player, &QMediaPlayer::errorOccurred,
this,
396 &stream_cell::on_player_error
409 {
"view-restore",
"window-restore",
"transform-scale" },
410 QStyle::SP_TitleBarNormalButton
416 {
"view-restore",
"window-restore" },
417 QStyle::SP_TitleBarNormalButton
426 {
"view-fullscreen",
"window-maximize",
"transform-scale" },
427 QStyle::SP_TitleBarMaxButton
433 {
"view-fullscreen",
"fullscreen",
"window-maximize" },
434 QStyle::SP_TitleBarMaxButton
442 QPainter& p,
const std::vector<QPointF>& pts_pct,
const QColor& color,
443 bool closed, Qt::PenStyle style, qreal width
445 if (pts_pct.size() < 2) {
450 pen.setWidthF(width);
455 poly.reserve(
static_cast<
int>(pts_pct.size()));
456 for (
const auto& pt_pct : pts_pct) {
457 poly << to_px(pt_pct);
460 if (closed && poly.size() >= 3) {
463 p.drawPolyline(poly);
466 for (
const auto& pt_px : poly) {
467 p.drawEllipse(pt_px, 3.0, 3.0);
472 const auto now = QDateTime::currentDateTime();
474 for (
const auto& l : persistent_lines) {
475 const auto key = l.template_name.trimmed();
477 if (!key.isEmpty() && line_highlights.contains(key)) {
478 const int age =
static_cast<
int>(line_highlights[key].msecsTo(now));
479 if (age < line_highlight_ttl_ms) {
481 = 1.0 -
static_cast<
double>(age) / line_highlight_ttl_ms;
486 const double falloff_pct = 30.0;
487 const double base_w = 2.0;
488 const double peak_w = 22.0;
490 bool has_hit =
false;
493 if (line_hits.contains(key)) {
494 const auto& h = line_hits[key];
495 const int hit_age =
static_cast<
int>(h.ts.msecsTo(now));
496 if (hit_age < line_highlight_ttl_ms) {
504 int a =
static_cast<
int>(255.0 * ktime);
510 const double w = base_w + (peak_w - base_w) * ktime;
512 draw_poly_with_points(
513 p, l.pts_pct, hc, l.closed, Qt::SolidLine, w
516 for (size_t i = 1; i < l.pts_pct.size(); ++i) {
517 const QPointF a_pct = l.pts_pct[i - 1];
518 const QPointF b_pct = l.pts_pct[i];
520 const QPointF mid = (a_pct + b_pct) * 0.5;
521 const double dx = mid.x() - hit_pct.x();
522 const double dy = mid.y() - hit_pct.y();
523 double dist = std::sqrt(dx * dx + dy * dy);
525 double kspace = 1.0 - dist / falloff_pct;
530 kspace = kspace * kspace;
532 const double k = ktime * kspace;
538 int a =
static_cast<
int>(255.0 * k);
544 const double w = base_w + (peak_w - base_w) * k;
546 std::vector<QPointF> seg { a_pct, b_pct };
547 draw_poly_with_points(
548 p, seg, hc,
false, Qt::SolidLine, w
555 draw_poly_with_points(
556 p, l.pts_pct, l.color, l.closed, Qt::SolidLine, 2.0
559 if (!(active && labels_enabled)) {
563 const auto text = key;
564 if (text.isEmpty()) {
569 p.drawText(label_pos_px(l), text);
574 if (draft_line_points_pct.empty()) {
578 draw_poly_with_points(
579 p, draft_line_points_pct, draft_line_color, draft_line_closed,
585 if (!hover_point_pct.has_value()) {
589 QPen hpen(draft_line_color);
591 hpen.setStyle(Qt::DashLine);
594 p.drawEllipse(to_px(*hover_point_pct), 4.0, 4.0);
598 if (!(hover_point_pct.has_value() && drawing_enabled && active)) {
602 const auto& hp = *hover_point_pct;
604 = QString(
"x=%1 y=%2").arg(hp.x(), 0,
'f', 1).arg(hp.y(), 0,
'f', 1);
606 QRect r = rect().adjusted(6, 6, -6, -6);
607 p.setPen(palette().color(QPalette::Text));
608 p.drawText(r, Qt::AlignLeft | Qt::AlignBottom, txt);
612 if (!(drawing_enabled && active && hover_point_pct.has_value()
613 && !draft_line_points_pct.empty())) {
617 QPen pen(draft_line_color);
619 pen.setStyle(Qt::DashLine);
622 const QPointF last_px = to_px(draft_line_points_pct.back());
623 const QPointF hover_px = to_px(*hover_point_pct);
625 p.drawLine(last_px, hover_px);
627 if (draft_line_closed && draft_line_points_pct.size() >= 2) {
628 const QPointF first_px = to_px(draft_line_points_pct.front());
629 p.drawLine(hover_px, first_px);
634 if (name.isEmpty()) {
638 QRect r = rect().adjusted(6, 6, -6, -6);
639 p.setPen(palette().color(QPalette::Text));
640 p.drawText(r, Qt::AlignLeft | Qt::AlignTop, name);
644 if (l.pts_pct.empty()) {
648 const QPointF anchor_pct = l.closed ? l.pts_pct.back() : l.pts_pct.front();
649 const QPointF anchor_px = to_px(anchor_pct);
650 return { anchor_px.x() + 6.0, anchor_px.y() + 14.0 };
654 if (width() <= 0 || height() <= 0) {
659 =
static_cast<
float>(pos_px.x()) /
static_cast<
float>(width()) * 100.0f;
660 float y =
static_cast<
float>(pos_px.y()) /
static_cast<
float>(height())
663 x = std::clamp(x, 0.f, 100.f);
664 y = std::clamp(y, 0.f, 100.f);
670 return { pos_pct.x() / 100.0 * width(), pos_pct.y() / 100.0 * height() };
674 const auto now = QDateTime::currentDateTime();
675 const int ttl_ms = 2000;
677 const QRect r = rect();
678 const double w =
static_cast<
double>(r.width());
679 const double h =
static_cast<
double>(r.height());
680 const double base = std::min(w, h);
681 const double radius = base * 0.015;
683 QVector<event_instance> alive;
684 alive.reserve(events.size());
686 for (
const auto& e : events) {
687 const int age =
static_cast<
int>(e.ts.msecsTo(now));
694 const double k = 1.0 -
static_cast<
double>(age) / ttl_ms;
695 int a =
static_cast<
int>(120.0 * k);
703 const double x = r.left() + w * (e.pos_pct.x() / 100.0);
704 const double y = r.top() + h * (e.pos_pct.y() / 100.0);
708 p.drawEllipse(QPointF(x, y), radius, radius);
711 events = std::move(alive);
715 if (!frame.isValid()) {
719 QVideoFrame copy(frame);
720 if (!copy.map(QVideoFrame::ReadOnly)) {
724 last_frame = copy.toImage();
727 emit frame_ready(name, last_frame);
729 if (!repaint_timer.isValid()) {
730 repaint_timer.start();
735 if (repaint_timer.elapsed() < repaint_interval_ms) {
739 repaint_timer.restart();
744 const QMediaPlayer::MediaStatus status
749 if (status != QMediaPlayer::EndOfMedia) {
756 player->setPosition(0);
761 const QMediaPlayer::Error error,
const QString& error_string
764 last_error = error_string;
775 last_error = camera->errorString();
void add_event(const QPointF &pos_pct, const QColor &color)
Add a transient event marker.
void mousePressEvent(QMouseEvent *event) override
Mouse press handler for drawing draft points.
int repaint_interval_ms
Minimum repaint interval in ms.
void set_loop(bool on)
Enable or disable looping for file-based playback.
void mouseMoveEvent(QMouseEvent *event) override
Mouse move handler for hover updates while drawing.
void update_icon()
Update focus button icon/tooltip based on active state.
QPushButton * close_btn
UI close button (top-right).
void draw_hover_point(QPainter &p) const
Draw hover point indicator (if any).
bool is_active() const
Check whether this cell is currently active (focused).
bool active
Whether the cell is focused/active.
bool drawing_enabled
Whether interactive drawing is enabled.
bool labels_enabled
Whether persistent line labels are shown when active.
void set_active(bool val)
Set active (focused) state.
void set_repaint_interval_ms(int ms)
Set minimum repaint interval for video frame updates.
void draw_stream_name(QPainter &p) const
Draw stream name overlay at top-left.
void set_source(const QUrl &source)
Set media player source.
void set_persistent_lines(const std::vector< line_instance > &lines)
Replace all persistent lines.
bool draft_closed() const
Get whether current draft line is closed.
void set_camera_id(const QByteArray &id)
Switch to camera input by device id.
void paintEvent(QPaintEvent *event) override
Paint handler.
void draw_preview_segment(QPainter &p) const
Draw preview segment from last draft point to hover point.
void set_labels_enabled(bool on)
Enable or disable rendering of persistent line labels.
void draw_draft(QPainter &p) const
Draw the draft line (if any).
int line_highlight_ttl_ms
Highlight time-to-live in ms.
void clear_draft()
Clear all draft data (points, hover point, preview flag).
void clear_persistent_lines()
Remove all persistent lines.
QLabel * name_label
Optional name label (unused in current implementation).
void leaveEvent(QEvent *event) override
Leave handler to clear hover state.
void draw_persistent(QPainter &p) const
Draw all persistent lines and their labels/highlights.
void on_camera_error(QCamera::Error error)
Slot called on camera errors.
void draw_events(QPainter &p)
Draw transient events and prune expired ones.
void set_draft_preview(bool on)
Enable or disable draft preview mode.
QPointF to_px(const QPointF &pos_pct) const
Convert percentage coordinates to pixel position.
void set_draft_points_pct(const std::vector< QPointF > &pts)
Replace current draft points (percentage coordinates).
const QString & get_name() const
Get logical name of this stream cell.
bool loop_enabled
Whether playback looping is enabled.
QPushButton * focus_btn
UI focus/enlarge button (top-right).
void draw_poly_with_points(QPainter &p, const std::vector< QPointF > &pts_pct, const QColor &color, bool closed, Qt::PenStyle style, qreal width) const
Draw a polyline/polygon with point markers.
void add_persistent_line(const line_instance &line)
Append a persistent line to the list.
void draw_hover_coords(QPainter &p) const
Draw hover coordinate text (if enabled).
QColor draft_color() const
Get current draft line color.
QPointF label_pos_px(const line_instance &l) const
Compute label anchor position for a line in pixel coordinates.
void set_drawing_enabled(bool on)
Enable or disable interactive drawing on this cell.
bool draft_line_closed
Draft line closed flag.
QString draft_name() const
Get current draft line name.
void keyPressEvent(QKeyEvent *event) override
Key press handler for draft undo.
void build_ui()
Build child UI widgets (buttons, sink/player connections).
std::vector< QPointF > draft_points_pct() const
Get current draft polyline points (percentage coordinates).
void on_media_status_changed(QMediaPlayer::MediaStatus status)
Slot called on media status changes.
bool draft_preview
Whether draft line is shown in preview mode.
QPointF to_pct(const QPointF &pos_px) const
Convert pixel position to percentage coordinates.
bool is_draft_preview() const
Check whether the draft is in preview-only mode.
Instance of a transient visual event marker.
Hit info attached to a line highlight.
Instance of a persistent (saved) line to be rendered on the stream.