GCC Code Coverage Report


Directory: ./
File: frontend/src/widgets/stream_cell.cpp
Date: 2025-11-24 00:30:48
Exec Total Coverage
Lines: 0 459 0.0%
Functions: 0 47 0.0%
Branches: 0 438 0.0%

Line Branch Exec Source
1 #include "widgets/stream_cell.hpp"
2
3 #include <QHBoxLayout>
4 #include <QLabel>
5 #include <QMouseEvent>
6 #include <QPaintEvent>
7 #include <QPainter>
8 #include <QPen>
9 #include <QPolygonF>
10 #include <QPushButton>
11 #include <QStyle>
12 #include <QStyleOption>
13 #include <QVBoxLayout>
14 #include <algorithm>
15
16 #include "helpers/icon_loader.hpp"
17
18 stream_cell::stream_cell(const QString& name, QWidget* parent)
19 : QWidget(parent)
20 , name(name)
21 , close_btn(nullptr)
22 , focus_btn(nullptr)
23 , name_label(nullptr)
24 , player(nullptr)
25 , sink(nullptr)
26 , camera(nullptr)
27 , session(nullptr) {
28 build_ui();
29 setFocusPolicy(Qt::StrongFocus);
30 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
31 repaint_timer.start();
32 }
33
34 const QString& stream_cell::get_name() const { return name; }
35
36 bool stream_cell::is_active() const { return active; }
37
38 std::vector<QPointF> stream_cell::draft_points_pct() const {
39 return draft_line_points_pct;
40 }
41
42 bool stream_cell::draft_closed() const { return draft_line_closed; }
43
44 QString stream_cell::draft_name() const { return draft_line_name; }
45
46 QColor stream_cell::draft_color() const { return draft_line_color; }
47
48 bool stream_cell::is_draft_preview() const { return draft_preview; }
49
50 void stream_cell::set_active(const bool val) {
51 if (active == val) {
52 return;
53 }
54 active = val;
55 if (!active) {
56 set_drawing_enabled(false);
57 clear_draft();
58 }
59 update_icon();
60 update();
61 }
62
63 void stream_cell::set_drawing_enabled(const bool on) {
64 drawing_enabled = on;
65 if (!on) {
66 hover_point_pct.reset();
67 }
68 setMouseTracking(drawing_enabled);
69 update();
70 }
71
72 void stream_cell::set_draft_params(
73 const QString& n, const QColor& color, const bool closed
74 ) {
75 draft_line_name = n;
76 draft_line_color = color;
77 draft_line_closed = closed;
78 update();
79 }
80
81 void stream_cell::set_draft_points_pct(const std::vector<QPointF>& pts) {
82 draft_line_points_pct = pts;
83 update();
84 }
85
86 void stream_cell::clear_draft() {
87 draft_line_points_pct.clear();
88 hover_point_pct.reset();
89 draft_preview = false;
90 update();
91 }
92
93 void stream_cell::set_persistent_lines(
94 const std::vector<line_instance>& lines
95 ) {
96 persistent_lines = lines;
97 update();
98 }
99
100 void stream_cell::add_persistent_line(const line_instance& line) {
101 persistent_lines.push_back(line);
102 update();
103 }
104
105 void stream_cell::clear_persistent_lines() {
106 persistent_lines.clear();
107 update();
108 }
109
110 void stream_cell::set_draft_preview(const bool on) {
111 draft_preview = on;
112 update();
113 }
114
115 void stream_cell::set_labels_enabled(const bool on) {
116 if (labels_enabled == on) {
117 return;
118 }
119 labels_enabled = on;
120 update();
121 }
122
123 void stream_cell::set_source(const QUrl& source) {
124 if (!player) {
125 return;
126 }
127
128 last_error.clear();
129 last_frame = QImage();
130
131 player->setSource(source);
132 player->play();
133 }
134
135 void stream_cell::set_loop(const bool on) { loop_enabled = on; }
136
137 void stream_cell::set_camera_id(const QByteArray& id) {
138 camera_id = id;
139
140 last_error.clear();
141 last_frame = QImage();
142
143 if (player) {
144 player->stop();
145 }
146
147 if (camera) {
148 camera->stop();
149 camera->deleteLater();
150 camera = nullptr;
151 }
152
153 if (!session) {
154 session = new QMediaCaptureSession(this);
155 session->setVideoSink(sink);
156 }
157
158 QCameraDevice device;
159 const auto cams = QMediaDevices::videoInputs();
160 for (const auto& c : cams) {
161 if (c.id() == id) {
162 device = c;
163 break;
164 }
165 }
166
167 if (device.isNull()) {
168 last_error = tr("camera not found");
169 update();
170 return;
171 }
172
173 camera = new QCamera(device, this);
174 session->setCamera(camera);
175
176 connect(
177 camera, &QCamera::errorOccurred, this, &stream_cell::on_camera_error
178 );
179
180 camera->start();
181 }
182
183 void stream_cell::add_event(const QPointF& pos_pct, const QColor& color) {
184 event_instance e;
185 e.pos_pct = pos_pct;
186 e.color = color;
187 e.ts = QDateTime::currentDateTime();
188
189 events.push_back(e);
190 update();
191 }
192
193 void stream_cell::set_repaint_interval_ms(const int ms) {
194 if (ms <= 0) {
195 return;
196 }
197 repaint_interval_ms = ms;
198 }
199
200 void stream_cell::highlight_line(const QString& line_name) {
201 if (line_name.isEmpty()) {
202 return;
203 }
204 line_highlights[line_name] = QDateTime::currentDateTime();
205 update();
206 }
207
208 void stream_cell::highlight_line_at(
209 const QString& line_name, const QPointF& pos_pct
210 ) {
211 if (line_name.isEmpty()) {
212 return;
213 }
214
215 hit_info h;
216 h.pos_pct = pos_pct;
217 h.ts = QDateTime::currentDateTime();
218 line_hits[line_name] = h;
219 line_highlights[line_name] = h.ts;
220 update();
221 }
222
223 void stream_cell::paintEvent(QPaintEvent* event) {
224 Q_UNUSED(event);
225
226 QStyleOption opt;
227 opt.initFrom(this);
228
229 QPainter p(this);
230 style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
231
232 if (!last_frame.isNull()) {
233 p.drawImage(rect(), last_frame);
234 } else {
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);
239 }
240
241 draw_stream_name(p);
242
243 p.drawRect(rect().adjusted(0, 0, -1, -1));
244 p.setRenderHint(QPainter::Antialiasing, true);
245
246 const auto now = QDateTime::currentDateTime();
247
248 for (auto it = line_highlights.begin(); it != line_highlights.end();) {
249 const int age = static_cast<int>(it.value().msecsTo(now));
250 if (age >= line_highlight_ttl_ms) {
251 it = line_highlights.erase(it);
252 } else {
253 ++it;
254 }
255 }
256
257 draw_events(p);
258 draw_persistent(p);
259 draw_draft(p);
260 draw_hover_point(p);
261 draw_hover_coords(p);
262 draw_preview_segment(p);
263 }
264
265 void stream_cell::mousePressEvent(QMouseEvent* event) {
266 if (!drawing_enabled || !active) {
267 QWidget::mousePressEvent(event);
268 return;
269 }
270
271 auto* child = childAt(event->pos());
272 if (child == close_btn || child == focus_btn) {
273 QWidget::mousePressEvent(event);
274 return;
275 }
276
277 if (event->button() == Qt::LeftButton) {
278 setFocus();
279 draft_line_points_pct.push_back(to_pct(event->pos()));
280 update();
281 event->accept();
282 return;
283 }
284
285 QWidget::mousePressEvent(event);
286 }
287
288 void stream_cell::mouseMoveEvent(QMouseEvent* event) {
289 if (!drawing_enabled || !active) {
290 QWidget::mouseMoveEvent(event);
291 return;
292 }
293
294 hover_point_pct = to_pct(event->pos());
295 update();
296 event->accept();
297 }
298
299 void stream_cell::leaveEvent(QEvent* event) {
300 hover_point_pct.reset();
301 update();
302 QWidget::leaveEvent(event);
303 }
304
305 void stream_cell::keyPressEvent(QKeyEvent* event) {
306 if (!(drawing_enabled && active)) {
307 QWidget::keyPressEvent(event);
308 return;
309 }
310
311 const bool undo_key = (event->key() == Qt::Key_Backspace)
312 || (event->key() == Qt::Key_Z
313 && (event->modifiers() & Qt::ControlModifier));
314
315 if (!undo_key) {
316 QWidget::keyPressEvent(event);
317 return;
318 }
319
320 if (!draft_line_points_pct.empty()) {
321 draft_line_points_pct.pop_back();
322 hover_point_pct.reset();
323 update();
324 }
325
326 event->accept();
327 }
328
329 void stream_cell::build_ui() {
330 const auto root = new QVBoxLayout(this);
331 root->setContentsMargins(6, 6, 6, 6);
332 root->setSpacing(6);
333
334 const auto top_row = new QHBoxLayout();
335 top_row->setContentsMargins(0, 0, 0, 0);
336 top_row->setSpacing(4);
337
338 top_row->addStretch();
339
340 focus_btn = new QPushButton(this);
341 focus_btn->setFixedSize(24, 24);
342 focus_btn->setIconSize(QSize(16, 16));
343 focus_btn->setFlat(true);
344 focus_btn->setFocusPolicy(Qt::NoFocus);
345 update_icon();
346 top_row->addWidget(focus_btn);
347
348 close_btn = new QPushButton(this);
349 close_btn->setFixedSize(24, 24);
350 close_btn->setIconSize(QSize(16, 16));
351 close_btn->setToolTip(tr("close"));
352 close_btn->setFlat(true);
353 close_btn->setFocusPolicy(Qt::NoFocus);
354
355 #if defined(KC_KDE)
356 close_btn->setIcon(
357 icon_loader::themed(
358 { "window-close", "dialog-close", "edit-delete" },
359 QStyle::SP_TitleBarCloseButton
360 )
361 );
362 #else
363 close_btn->setIcon(
364 icon_loader::themed(
365 { "window-close", "dialog-close" }, QStyle::SP_TitleBarCloseButton
366 )
367 );
368 #endif
369 top_row->addWidget(close_btn);
370 root->addLayout(top_row);
371 root->addStretch(1);
372
373 sink = new QVideoSink(this);
374 connect(
375 sink, &QVideoSink::videoFrameChanged, this,
376 &stream_cell::on_frame_changed
377 );
378
379 player = new QMediaPlayer(this);
380 player->setVideoOutput(sink);
381 // player->setSource(QUrl::fromLocalFile("/home/yarro/Pictures/kino/1080.mp4"));
382 // player->play();
383
384 connect(close_btn, &QPushButton::clicked, this, [this]() {
385 emit request_close(name);
386 });
387 connect(focus_btn, &QPushButton::clicked, this, [this]() {
388 emit request_focus(name);
389 });
390 connect(
391 player, &QMediaPlayer::mediaStatusChanged, this,
392 &stream_cell::on_media_status_changed
393 );
394 connect(
395 player, &QMediaPlayer::errorOccurred, this,
396 &stream_cell::on_player_error
397 );
398 }
399
400 void stream_cell::update_icon() {
401 if (!focus_btn) {
402 return;
403 }
404 if (active) {
405 focus_btn->setToolTip(tr("shrink"));
406 #if defined(KC_KDE)
407 focus_btn->setIcon(
408 icon_loader::themed(
409 { "view-restore", "window-restore", "transform-scale" },
410 QStyle::SP_TitleBarNormalButton
411 )
412 );
413 #else
414 focus_btn->setIcon(
415 icon_loader::themed(
416 { "view-restore", "window-restore" },
417 QStyle::SP_TitleBarNormalButton
418 )
419 );
420 #endif
421 } else {
422 focus_btn->setToolTip(tr("enlarge"));
423 #if defined(KC_KDE)
424 focus_btn->setIcon(
425 icon_loader::themed(
426 { "view-fullscreen", "window-maximize", "transform-scale" },
427 QStyle::SP_TitleBarMaxButton
428 )
429 );
430 #else
431 focus_btn->setIcon(
432 icon_loader::themed(
433 { "view-fullscreen", "fullscreen", "window-maximize" },
434 QStyle::SP_TitleBarMaxButton
435 )
436 );
437 #endif
438 }
439 }
440
441 void stream_cell::draw_poly_with_points(
442 QPainter& p, const std::vector<QPointF>& pts_pct, const QColor& color,
443 bool closed, Qt::PenStyle style, qreal width
444 ) const {
445 if (pts_pct.size() < 2) {
446 return;
447 }
448
449 QPen pen(color);
450 pen.setWidthF(width);
451 pen.setStyle(style);
452 p.setPen(pen);
453
454 QPolygonF poly;
455 poly.reserve(static_cast<int>(pts_pct.size()));
456 for (const auto& pt_pct : pts_pct) {
457 poly << to_px(pt_pct);
458 }
459
460 if (closed && poly.size() >= 3) {
461 p.drawPolygon(poly);
462 } else {
463 p.drawPolyline(poly);
464 }
465
466 for (const auto& pt_px : poly) {
467 p.drawEllipse(pt_px, 3.0, 3.0);
468 }
469 }
470
471 void stream_cell::draw_persistent(QPainter& p) const {
472 const auto now = QDateTime::currentDateTime();
473
474 for (const auto& l : persistent_lines) {
475 const auto key = l.template_name.trimmed();
476
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) {
480 double ktime
481 = 1.0 - static_cast<double>(age) / line_highlight_ttl_ms;
482 if (ktime < 0.0) {
483 ktime = 0.0;
484 }
485
486 const double falloff_pct = 30.0;
487 const double base_w = 2.0;
488 const double peak_w = 22.0;
489
490 bool has_hit = false;
491 QPointF hit_pct;
492
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) {
497 has_hit = true;
498 hit_pct = h.pos_pct;
499 }
500 }
501
502 if (!has_hit) {
503 QColor hc = l.color;
504 int a = static_cast<int>(255.0 * ktime);
505 if (a < 0) {
506 a = 0;
507 }
508 hc.setAlpha(a);
509
510 const double w = base_w + (peak_w - base_w) * ktime;
511
512 draw_poly_with_points(
513 p, l.pts_pct, hc, l.closed, Qt::SolidLine, w
514 );
515 } else {
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];
519
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);
524
525 double kspace = 1.0 - dist / falloff_pct;
526 if (kspace < 0.0) {
527 kspace = 0.0;
528 }
529
530 kspace = kspace * kspace;
531
532 const double k = ktime * kspace;
533 if (k <= 0.0) {
534 continue;
535 }
536
537 QColor hc = l.color;
538 int a = static_cast<int>(255.0 * k);
539 if (a < 0) {
540 a = 0;
541 }
542 hc.setAlpha(a);
543
544 const double w = base_w + (peak_w - base_w) * k;
545
546 std::vector<QPointF> seg { a_pct, b_pct };
547 draw_poly_with_points(
548 p, seg, hc, false, Qt::SolidLine, w
549 );
550 }
551 }
552 }
553 }
554
555 draw_poly_with_points(
556 p, l.pts_pct, l.color, l.closed, Qt::SolidLine, 2.0
557 );
558
559 if (!(active && labels_enabled)) {
560 continue;
561 }
562
563 const auto text = key;
564 if (text.isEmpty()) {
565 continue;
566 }
567
568 p.setPen(l.color);
569 p.drawText(label_pos_px(l), text);
570 }
571 }
572
573 void stream_cell::draw_draft(QPainter& p) const {
574 if (draft_line_points_pct.empty()) {
575 return;
576 }
577
578 draw_poly_with_points(
579 p, draft_line_points_pct, draft_line_color, draft_line_closed,
580 Qt::DashLine, 2.0
581 );
582 }
583
584 void stream_cell::draw_hover_point(QPainter& p) const {
585 if (!hover_point_pct.has_value()) {
586 return;
587 }
588
589 QPen hpen(draft_line_color);
590 hpen.setWidthF(1.0);
591 hpen.setStyle(Qt::DashLine);
592 p.setPen(hpen);
593
594 p.drawEllipse(to_px(*hover_point_pct), 4.0, 4.0);
595 }
596
597 void stream_cell::draw_hover_coords(QPainter& p) const {
598 if (!(hover_point_pct.has_value() && drawing_enabled && active)) {
599 return;
600 }
601
602 const auto& hp = *hover_point_pct;
603 QString txt
604 = QString("x=%1 y=%2").arg(hp.x(), 0, 'f', 1).arg(hp.y(), 0, 'f', 1);
605
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);
609 }
610
611 void stream_cell::draw_preview_segment(QPainter& p) const {
612 if (!(drawing_enabled && active && hover_point_pct.has_value()
613 && !draft_line_points_pct.empty())) {
614 return;
615 }
616
617 QPen pen(draft_line_color);
618 pen.setWidthF(1.5);
619 pen.setStyle(Qt::DashLine);
620 p.setPen(pen);
621
622 const QPointF last_px = to_px(draft_line_points_pct.back());
623 const QPointF hover_px = to_px(*hover_point_pct);
624
625 p.drawLine(last_px, hover_px);
626
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);
630 }
631 }
632
633 void stream_cell::draw_stream_name(QPainter& p) const {
634 if (name.isEmpty()) {
635 return;
636 }
637
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);
641 }
642
643 QPointF stream_cell::label_pos_px(const line_instance& l) const {
644 if (l.pts_pct.empty()) {
645 return {};
646 }
647
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 };
651 }
652
653 QPointF stream_cell::to_pct(const QPointF& pos_px) const {
654 if (width() <= 0 || height() <= 0) {
655 return {};
656 }
657
658 float x
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())
661 * 100.0f;
662
663 x = std::clamp(x, 0.f, 100.f);
664 y = std::clamp(y, 0.f, 100.f);
665
666 return { x, y };
667 }
668
669 QPointF stream_cell::to_px(const QPointF& pos_pct) const {
670 return { pos_pct.x() / 100.0 * width(), pos_pct.y() / 100.0 * height() };
671 }
672
673 void stream_cell::draw_events(QPainter& p) {
674 const auto now = QDateTime::currentDateTime();
675 const int ttl_ms = 2000;
676
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;
682
683 QVector<event_instance> alive;
684 alive.reserve(events.size());
685
686 for (const auto& e : events) {
687 const int age = static_cast<int>(e.ts.msecsTo(now));
688 if (age >= ttl_ms) {
689 continue;
690 }
691
692 alive.push_back(e);
693
694 const double k = 1.0 - static_cast<double>(age) / ttl_ms;
695 int a = static_cast<int>(120.0 * k);
696 if (a < 0) {
697 a = 0;
698 }
699
700 QColor c = e.color;
701 c.setAlpha(a);
702
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);
705
706 p.setPen(Qt::NoPen);
707 p.setBrush(c);
708 p.drawEllipse(QPointF(x, y), radius, radius);
709 }
710
711 events = std::move(alive);
712 }
713
714 void stream_cell::on_frame_changed(const QVideoFrame& frame) {
715 if (!frame.isValid()) {
716 return;
717 }
718
719 QVideoFrame copy(frame);
720 if (!copy.map(QVideoFrame::ReadOnly)) {
721 return;
722 }
723
724 last_frame = copy.toImage();
725 copy.unmap();
726
727 emit frame_ready(name, last_frame);
728
729 if (!repaint_timer.isValid()) {
730 repaint_timer.start();
731 update();
732 return;
733 }
734
735 if (repaint_timer.elapsed() < repaint_interval_ms) {
736 return;
737 }
738
739 repaint_timer.restart();
740 update();
741 }
742
743 void stream_cell::on_media_status_changed(
744 const QMediaPlayer::MediaStatus status
745 ) {
746 if (!loop_enabled) {
747 return;
748 }
749 if (status != QMediaPlayer::EndOfMedia) {
750 return;
751 }
752 if (!player) {
753 return;
754 }
755
756 player->setPosition(0);
757 player->play();
758 }
759
760 void stream_cell::on_player_error(
761 const QMediaPlayer::Error error, const QString& error_string
762 ) {
763 Q_UNUSED(error);
764 last_error = error_string;
765 update();
766 }
767
768 void stream_cell::on_camera_error(const QCamera::Error error) {
769 Q_UNUSED(error);
770
771 if (!camera) {
772 return;
773 }
774
775 last_error = camera->errorString();
776 update();
777 }
778