YODAU 1.0
YEAR OF THE DEPEND ADULT UNDERGARMENT
Loading...
Searching...
No Matches
stream_cell.cpp
Go to the documentation of this file.
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
18stream_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) {
29 setFocusPolicy(Qt::StrongFocus);
30 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
31 repaint_timer.start();
32}
33
34const QString& stream_cell::get_name() const { return name; }
35
36bool stream_cell::is_active() const { return active; }
37
39 return draft_line_points_pct;
40}
41
43
44QString stream_cell::draft_name() const { return draft_line_name; }
45
46QColor stream_cell::draft_color() const { return draft_line_color; }
47
49
50void stream_cell::set_active(const bool val) {
51 if (active == val) {
52 return;
53 }
54 active = val;
55 if (!active) {
58 }
60 update();
61}
62
63void 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
72void 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
81void stream_cell::set_draft_points_pct(const std::vector<QPointF>& pts) {
82 draft_line_points_pct = pts;
83 update();
84}
85
87 draft_line_points_pct.clear();
88 hover_point_pct.reset();
89 draft_preview = false;
90 update();
91}
92
94 const std::vector<line_instance>& lines
95) {
96 persistent_lines = lines;
97 update();
98}
99
100void stream_cell::add_persistent_line(const line_instance& line) {
101 persistent_lines.push_back(line);
102 update();
103}
104
106 persistent_lines.clear();
107 update();
108}
109
110void stream_cell::set_draft_preview(const bool on) {
111 draft_preview = on;
112 update();
113}
114
115void stream_cell::set_labels_enabled(const bool on) {
116 if (labels_enabled == on) {
117 return;
118 }
119 labels_enabled = on;
120 update();
121}
122
123void 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
135void stream_cell::set_loop(const bool on) { loop_enabled = on; }
136
137void 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
183void stream_cell::add_event(const QPointF& pos_pct, const QColor& color) {
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
194 if (ms <= 0) {
195 return;
196 }
198}
199
200void 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
208void 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
223void 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
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
263}
264
265void 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
288void 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
299void stream_cell::leaveEvent(QEvent* event) {
300 hover_point_pct.reset();
301 update();
302 QWidget::leaveEvent(event);
303}
304
305void 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
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);
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
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
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
471void 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
573void 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
584void 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
597void 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
611void 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
633void 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
643QPointF 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
653QPointF 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
669QPointF 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
673void 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
714void 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
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
760void 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
768void 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}
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.