| 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 |