• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

paulmthompson / WhiskerToolbox / 18096850108

29 Sep 2025 12:24PM UTC coverage: 70.143% (-0.09%) from 70.231%
18096850108

push

github

paulmthompson
better line drawing selection

15 of 92 new or added lines in 3 files covered. (16.3%)

21 existing lines in 3 files now uncovered.

44250 of 63085 relevant lines covered (70.14%)

1121.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

17.42
/src/WhiskerToolbox/Media_Widget/Media_Window/Media_Window.cpp
1
#include "Media_Window.hpp"
2

3
#include "CoreGeometry/line_geometry.hpp"
4
#include "CoreGeometry/lines.hpp"
5
#include "CoreGeometry/masks.hpp"
6
#include "DataManager/DataManager.hpp"
7
#include "DataManager/DigitalTimeSeries/Digital_Interval_Series.hpp"
8
#include "DataManager/Lines/Line_Data.hpp"
9
#include "DataManager/Masks/Mask_Data.hpp"
10
#include "DataManager/Media/Media_Data.hpp"
11
#include "DataManager/Points/Point_Data.hpp"
12
#include "ImageProcessing/OpenCVUtility.hpp"
13
#include "Media_Widget/DisplayOptions/DisplayOptions.hpp"
14
#include "Media_Widget/MediaProcessing_Widget/MediaProcessing_Widget.hpp"
15
#include "Media_Widget/MediaText_Widget/MediaText_Widget.hpp"
16
#include "Media_Widget/Media_Widget.hpp"
17
#include "TimeFrame/TimeFrame.hpp"
18
#include "GroupManagementWidget/GroupManager.hpp"
19

20
//https://stackoverflow.com/questions/72533139/libtorch-errors-when-used-with-qt-opencv-and-point-cloud-library
21
#undef slots
22
#include "DataManager/Tensors/Tensor_Data.hpp"
23
#define slots Q_SLOTS
24

25
#include <QAction>
26
#include <QElapsedTimer>
27
#include <QFont>
28
#include <QGraphicsPixmapItem>
29
#include <QGraphicsSceneContextMenuEvent>
30
#include <QGraphicsSceneMouseEvent>
31
#include <QGraphicsTextItem>
32
#include <QImage>
33
#include <QMenu>
34
#include <QPainter>
35
#include <algorithm>
36

37
#include <iostream>
38

39
/*
40

41
The Media_Window class
42

43
*/
44

45

46
Media_Window::Media_Window(std::shared_ptr<DataManager> data_manager, QObject * parent)
3✔
47
    : QGraphicsScene(parent),
48
      _data_manager{std::move(data_manager)} {
3✔
49

50
    _data_manager->addObserver([this]() {
3✔
51
        _addRemoveData();
1✔
52
    });
1✔
53

54
    _canvasImage = QImage(_canvasWidth, _canvasHeight, QImage::Format_ARGB32);
3✔
55
    _canvasPixmap = addPixmap(QPixmap::fromImage(_canvasImage));
3✔
56

57
    _createContextMenu();
3✔
58
}
3✔
59

60
Media_Window::~Media_Window() {
6✔
61
    // Clean up context menu
62
    if (_context_menu) {
3✔
63
        delete _context_menu;
3✔
64
        _context_menu = nullptr;
3✔
65
    }
66

67
    // Clear temporary line items before clearing the scene
68
    clearTemporaryLine();
3✔
69

70
    // Clear all items from the scene - this automatically removes and deletes all QGraphicsItems
71
    clear();
3✔
72

73
    // Just clear the containers since the items are already deleted by clear()
74
    _line_paths.clear();
3✔
75
    _masks.clear();
3✔
76
    _mask_bounding_boxes.clear();
3✔
77
    _mask_outlines.clear();
3✔
78
    _points.clear();
3✔
79
    _intervals.clear();
3✔
80
    _tensors.clear();
3✔
81
    _text_items.clear();
3✔
82
}
6✔
83

84
void Media_Window::addMediaDataToScene(std::string const & media_key) {
3✔
85
    auto media_config = std::make_unique<MediaDisplayOptions>();
3✔
86

87
    _media_configs[media_key] = std::move(media_config);
3✔
88

89
    UpdateCanvas();
3✔
90
}
6✔
91

92
void Media_Window::_clearMedia() {
16✔
93
    // Set to black
94
    _canvasImage.fill(Qt::black);
16✔
95
    _canvasPixmap->setPixmap(QPixmap::fromImage(_canvasImage));
16✔
96
}
16✔
97

98
void Media_Window::removeMediaDataFromScene(std::string const & media_key) {
×
99
    auto mediaItem = _media_configs.find(media_key);
×
100
    if (mediaItem != _media_configs.end()) {
×
101
        _media_configs.erase(mediaItem);
×
102
    }
103

104
    UpdateCanvas();
×
105
}
×
106

107
void Media_Window::addLineDataToScene(std::string const & line_key) {
×
108
    auto line_config = std::make_unique<LineDisplayOptions>();
×
109

110
    // Assign color based on the current number of line configs
111
    line_config->hex_color = DefaultDisplayValues::getColorForIndex(_line_configs.size());
×
112

113
    _line_configs[line_key] = std::move(line_config);
×
114

115
    UpdateCanvas();
×
116
}
×
117

118
void Media_Window::_clearLines() {
16✔
119
    for (auto pathItem: _line_paths) {
16✔
120
        removeItem(pathItem);
×
121
    }
122
    for (auto pathItem: _line_paths) {
16✔
123
        delete pathItem;
×
124
    }
125
    _line_paths.clear();
16✔
126
}
16✔
127

128
void Media_Window::removeLineDataFromScene(std::string const & line_key) {
×
129
    auto lineItem = _line_configs.find(line_key);
×
130
    if (lineItem != _line_configs.end()) {
×
131
        _line_configs.erase(lineItem);
×
132
    }
133

134
    UpdateCanvas();
×
135
}
×
136

137
void Media_Window::addMaskDataToScene(std::string const & mask_key) {
3✔
138
    auto mask_config = std::make_unique<MaskDisplayOptions>();
3✔
139

140
    // Assign color based on the current number of mask configs
141
    mask_config->hex_color = DefaultDisplayValues::getColorForIndex(_mask_configs.size());
3✔
142

143
    _mask_configs[mask_key] = std::move(mask_config);
3✔
144
    UpdateCanvas();
3✔
145
}
6✔
146

147
void Media_Window::_clearMasks() {
16✔
148
    for (auto maskItem: _masks) {
16✔
149
        removeItem(maskItem);
×
150
    }
151

152
    for (auto maskItem: _masks) {
16✔
153
        delete maskItem;
×
154
    }
155
    _masks.clear();
16✔
156
}
16✔
157

158
void Media_Window::_clearMaskBoundingBoxes() {
16✔
159
    for (auto boundingBoxItem: _mask_bounding_boxes) {
16✔
160
        removeItem(boundingBoxItem);
×
161
    }
162

163
    for (auto boundingBoxItem: _mask_bounding_boxes) {
16✔
164
        delete boundingBoxItem;
×
165
    }
166
    _mask_bounding_boxes.clear();
16✔
167
}
16✔
168

169
void Media_Window::_clearMaskOutlines() {
16✔
170
    for (auto outlineItem: _mask_outlines) {
16✔
171
        removeItem(outlineItem);
×
172
    }
173

174
    for (auto outlineItem: _mask_outlines) {
16✔
175
        delete outlineItem;
×
176
    }
177
    _mask_outlines.clear();
16✔
178
}
16✔
179

180
void Media_Window::removeMaskDataFromScene(std::string const & mask_key) {
×
181
    auto maskItem = _mask_configs.find(mask_key);
×
182
    if (maskItem != _mask_configs.end()) {
×
183
        _mask_configs.erase(maskItem);
×
184
    }
185

186
    UpdateCanvas();
×
187
}
×
188

189
void Media_Window::addPointDataToScene(std::string const & point_key) {
×
190
    auto point_config = std::make_unique<PointDisplayOptions>();
×
191

192
    // Assign color based on the current number of point configs
193
    point_config->hex_color = DefaultDisplayValues::getColorForIndex(_point_configs.size());
×
194

195
    _point_configs[point_key] = std::move(point_config);
×
196
    UpdateCanvas();
×
197
}
×
198

199
void Media_Window::_clearPoints() {
16✔
200
    if (_debug_performance) {
16✔
201
        std::cout << "CLEARING POINTS - Count before: " << _points.size() << std::endl;
×
202
    }
203

204
    for (auto pathItem: _points) {
16✔
205
        removeItem(pathItem);
×
206
    }
207
    for (auto pathItem: _points) {
16✔
208
        delete pathItem;
×
209
    }
210
    _points.clear();
16✔
211

212
    if (_debug_performance) {
16✔
213
        std::cout << "  Points cleared. Count after: " << _points.size() << std::endl;
×
214
        std::cout << "  Hover circle item still exists: " << (_hover_circle_item ? "YES" : "NO") << std::endl;
×
215
    }
216
}
16✔
217

218
void Media_Window::removePointDataFromScene(std::string const & point_key) {
×
219
    auto pointItem = _point_configs.find(point_key);
×
220
    if (pointItem != _point_configs.end()) {
×
221
        _point_configs.erase(pointItem);
×
222
    }
223

224
    UpdateCanvas();
×
225
}
×
226

227
void Media_Window::addDigitalIntervalSeries(std::string const & key) {
×
228
    auto interval_config = std::make_unique<DigitalIntervalDisplayOptions>();
×
229

230
    // Assign color based on the current number of interval configs
231
    interval_config->hex_color = DefaultDisplayValues::getColorForIndex(_interval_configs.size());
×
232

233
    _interval_configs[key] = std::move(interval_config);
×
234
    UpdateCanvas();
×
235
}
×
236

237
void Media_Window::removeDigitalIntervalSeries(std::string const & key) {
×
238
    auto item = _interval_configs.find(key);
×
239
    if (item != _interval_configs.end()) {
×
240
        _interval_configs.erase(item);
×
241
    }
242

243
    UpdateCanvas();
×
244
}
×
245

246
void Media_Window::_clearIntervals() {
16✔
247
    for (auto item: _intervals) {
16✔
248
        removeItem(item);
×
249
    }
250

251
    for (auto item: _intervals) {
16✔
252
        delete item;
×
253
    }
254
    _intervals.clear();
16✔
255
}
16✔
256

257
void Media_Window::addTensorDataToScene(std::string const & tensor_key) {
×
258
    auto tensor_config = std::make_unique<TensorDisplayOptions>();
×
259

260
    // Assign color based on the current number of tensor configs
261
    tensor_config->hex_color = DefaultDisplayValues::getColorForIndex(_tensor_configs.size());
×
262

263
    _tensor_configs[tensor_key] = std::move(tensor_config);
×
264

265
    UpdateCanvas();
×
266
}
×
267

268
void Media_Window::removeTensorDataFromScene(std::string const & tensor_key) {
×
269
    auto tensorItem = _tensor_configs.find(tensor_key);
×
270
    if (tensorItem != _tensor_configs.end()) {
×
271
        _tensor_configs.erase(tensorItem);
×
272
    }
273

274
    UpdateCanvas();
×
275
}
×
276

277
void Media_Window::_clearTensors() {
16✔
278
    for (auto item: _tensors) {
16✔
279
        removeItem(item);
×
280
    }
281

282
    for (auto item: _tensors) {
16✔
283
        delete item;
×
284
    }
285
    _tensors.clear();
16✔
286
}
16✔
287

288
void Media_Window::setTextWidget(MediaText_Widget * text_widget) {
6✔
289
    _text_widget = text_widget;
6✔
290
}
6✔
291

292
void Media_Window::setGroupManager(GroupManager * group_manager) {
×
293
    // Disconnect from previous group manager if any
294
    if (_group_manager) {
×
295
        disconnect(_group_manager, nullptr, this, nullptr);
×
296
    }
297
    
298
    _group_manager = group_manager;
×
299
    
300
    // Connect to new group manager signals if available
301
    if (_group_manager) {
×
302
        connect(_group_manager, &GroupManager::groupCreated, this, &Media_Window::onGroupChanged);
×
303
        connect(_group_manager, &GroupManager::groupRemoved, this, &Media_Window::onGroupChanged);
×
304
        connect(_group_manager, &GroupManager::groupModified, this, &Media_Window::onGroupChanged);
×
305
    }
306
}
×
307

308
void Media_Window::_plotTextOverlays() {
16✔
309
    if (!_text_widget) {
16✔
310
        return;
×
311
    }
312

313
    // Get enabled text overlays from the widget
314
    auto text_overlays = _text_widget->getEnabledTextOverlays();
16✔
315

316
    for (auto const & overlay: text_overlays) {
16✔
317
        if (!overlay.enabled) {
×
318
            continue;
×
319
        }
320

321
        // Calculate position based on relative coordinates (0.0-1.0)
322
        float const x_pos = overlay.x_position * static_cast<float>(_canvasWidth);
×
323
        float const y_pos = overlay.y_position * static_cast<float>(_canvasHeight);
×
324

325
        // Create text item
326
        auto text_item = addText(overlay.text);
×
327

328
        // Set font and size
329
        QFont font = text_item->font();
×
330
        font.setPointSize(overlay.font_size);
×
331
        text_item->setFont(font);
×
332

333
        // Set color
334
        QColor const text_color(overlay.color);
×
335
        text_item->setDefaultTextColor(text_color);
×
336

337
        // Handle orientation
338
        if (overlay.orientation == TextOrientation::Vertical) {
×
339
            text_item->setRotation(90.0);// Rotate 90 degrees for vertical text
×
340
        }
341

342
        // Set position
343
        text_item->setPos(x_pos, y_pos);
×
344

345
        // Add to our collection for cleanup
346
        _text_items.append(text_item);
×
347
    }
×
348
}
16✔
349

350
void Media_Window::_clearTextOverlays() {
16✔
351
    for (auto text_item: _text_items) {
16✔
352
        removeItem(text_item);
×
353
    }
354
    for (auto text_item: _text_items) {
16✔
355
        delete text_item;
×
356
    }
357
    _text_items.clear();
16✔
358
}
16✔
359

360
void Media_Window::LoadFrame(int frame_id) {
×
361
    // Get MediaData using the active media key
362
    for (auto const & [media_key, media_config]: _media_configs) {
×
363
        if (!media_config.get()->is_visible) {
×
364
            continue;
×
365
        }
366

367
        auto media = _data_manager->getData<MediaData>(media_key);
×
368
        if (!media) {
×
369
            std::cerr << "Warning: No media data found for key '" << media_key << "'" << std::endl;
×
370
            return;
×
371
        }
372
        media->LoadFrame(frame_id);
×
373
    }
×
374

375
    // Clear any accumulated drawing points when changing frames
376
    // This ensures no cross-frame accumulation and explains why lag disappears on frame change
377
    _drawing_points.clear();
×
378
    _is_drawing = false;
×
379

380
    UpdateCanvas();
×
381
}
382

383
void Media_Window::UpdateCanvas() {
16✔
384

385
    if (_debug_performance) {
16✔
386
        std::cout << "========== Update Canvas called ==========" << std::endl;
×
387

388
        // Debug: Show current item counts before clearing
389
        std::cout << "BEFORE CLEAR - Items in scene: " << items().size() << std::endl;
×
390
        std::cout << "  Lines: " << _line_paths.size() << std::endl;
×
391
        std::cout << "  Points: " << _points.size() << std::endl;
×
392
        std::cout << "  Masks: " << _masks.size() << std::endl;
×
393
        std::cout << "  Mask bounding boxes: " << _mask_bounding_boxes.size() << std::endl;
×
394
        std::cout << "  Mask outlines: " << _mask_outlines.size() << std::endl;
×
395
        std::cout << "  Intervals: " << _intervals.size() << std::endl;
×
396
        std::cout << "  Tensors: " << _tensors.size() << std::endl;
×
397
        std::cout << "  Text items: " << _text_items.size() << std::endl;
×
398
        std::cout << "  Drawing points accumulated: " << _drawing_points.size() << std::endl;
×
399
        std::cout << "  Hover circle item exists: " << (_hover_circle_item ? "YES" : "NO") << std::endl;
×
400
    }
401

402
    _clearLines();
16✔
403
    _clearPoints();
16✔
404
    _clearMasks();
16✔
405
    _clearMaskBoundingBoxes();
16✔
406
    _clearMaskOutlines();
16✔
407
    _clearIntervals();
16✔
408
    _clearTensors();
16✔
409
    _clearTextOverlays();
16✔
410
    _clearMedia();
16✔
411

412
    _plotMediaData();
16✔
413

414
    _plotLineData();
16✔
415

416
    _plotMaskData();
16✔
417

418
    _plotPointData();
16✔
419

420
    _plotDigitalIntervalSeries();
16✔
421

422
    _plotDigitalIntervalBorders();
16✔
423

424
    _plotTensorData();
16✔
425

426
    _plotTextOverlays();
16✔
427

428
    // Note: Hover circle is now handled efficiently via _updateHoverCirclePosition()
429
    // and doesn't need to be redrawn on every UpdateCanvas() call
430

431
    if (_debug_performance) {
16✔
432
        // Debug: Show item counts after plotting
433
        std::cout << "AFTER PLOTTING - Items in scene: " << items().size() << std::endl;
×
434
        std::cout << "  Lines plotted: " << _line_paths.size() << std::endl;
×
435
        std::cout << "  Points plotted: " << _points.size() << std::endl;
×
436
        std::cout << "  Masks plotted: " << _masks.size() << std::endl;
×
437
        std::cout << "  Mask bounding boxes plotted: " << _mask_bounding_boxes.size() << std::endl;
×
438
        std::cout << "  Mask outlines plotted: " << _mask_outlines.size() << std::endl;
×
439
        std::cout << "  Intervals plotted: " << _intervals.size() << std::endl;
×
440
        std::cout << "  Tensors plotted: " << _tensors.size() << std::endl;
×
441
        std::cout << "  Text items plotted: " << _text_items.size() << std::endl;
×
442
    }
443

444
    // Save the entire QGraphicsScene as an image
445
    QImage scene_image(_canvasWidth, _canvasHeight, QImage::Format_ARGB32);
16✔
446
    scene_image.fill(Qt::transparent);// Optional: fill with transparent background
16✔
447
    QPainter painter(&scene_image);
16✔
448

449
    // Set the scene rect to match the canvas dimensions
450
    this->setSceneRect(0, 0, _canvasWidth, _canvasHeight);
16✔
451

452
    // Render the scene with proper viewport mapping
453
    this->render(&painter, QRectF(0, 0, _canvasWidth, _canvasHeight),
48✔
454
                 QRect(0, 0, _canvasWidth, _canvasHeight));
32✔
455

456
    emit canvasUpdated(scene_image);
16✔
457
}
32✔
458

459

460
QImage::Format Media_Window::_getQImageFormat(std::string const & media_key) {
×
461

462
    auto _media = _data_manager->getData<MediaData>(media_key);
×
463
    if (!_media) {
×
464
        // Return a default format if no media is available
465
        return QImage::Format_Grayscale8;
×
466
    }
467

468
    // Check bit depth for grayscale images
469
    if (_media->getFormat() == MediaData::DisplayFormat::Gray) {
×
470
        if (_media->is32Bit()) {
×
471
            return QImage::Format_Grayscale16;// Use 16-bit for higher precision
×
472
        } else {
473
            return QImage::Format_Grayscale8;// Default 8-bit
×
474
        }
475
    } else {
476
        // Color format
477
        return QImage::Format_RGBA8888;
×
478
    }
479
}
×
480

481
void Media_Window::_plotMediaData() {
16✔
482

483
    auto const current_time = _data_manager->getCurrentTime();
16✔
484

485
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
16✔
486

487
    int total_visible_media = 0;
16✔
488
    std::string active_media_key;
16✔
489
    for (auto const & [media_key, _media_config]: _media_configs) {
32✔
490
        if (!_media_config.get()->is_visible) continue;
16✔
491
        total_visible_media++;
×
492
        active_media_key = media_key;
×
493
    }
494

495
    if (total_visible_media == 0) {
16✔
496
        return;
16✔
497
    }
498

499
    QImage unscaled_image;
×
500

501
    if (total_visible_media == 1) {
×
502
        auto media = _data_manager->getData<MediaData>(active_media_key);
×
503
        if (!media) {
×
504
            std::cerr << "Warning: No media data found for key '" << active_media_key << "'" << std::endl;
×
505
            return;
×
506
        }
507

508
        if (media->getFormat() == MediaData::DisplayFormat::Gray) {
×
509
            // Handle grayscale images with potential colormap application
510
            bool apply_colormap = _media_configs[active_media_key].get()->colormap_options.active &&
×
511
                                  _media_configs[active_media_key].get()->colormap_options.colormap != ColormapType::None;
×
512

513
            if (media->is8Bit()) {
×
514
                // 8-bit grayscale processing
515
                auto unscaled_image_data_8bit = media->getProcessedData8(current_time);
×
516

517
                if (apply_colormap) {
×
518
                    auto colormap_data = ImageProcessing::apply_colormap_for_display(
×
519
                            unscaled_image_data_8bit,
520
                            media->getImageSize(),
521
                            _media_configs[active_media_key].get()->colormap_options);
×
522

523
                    // Apply colormap and get BGRA data (OpenCV returns BGRA format)
524
                    unscaled_image = QImage(colormap_data.data(),
×
525
                                            media->getWidth(),
526
                                            media->getHeight(),
527
                                            QImage::Format_ARGB32)
528
                                             .copy();
×
529
                } else {
×
530
                    // No colormap, use original 8-bit grayscale data
531
                    unscaled_image = QImage(unscaled_image_data_8bit.data(),
×
532
                                            media->getWidth(),
533
                                            media->getHeight(),
534
                                            QImage::Format_Grayscale8)
535
                                             .copy();
×
536
                }
537
            } else if (media->is32Bit()) {
×
538
                // 32-bit float processing
539
                auto unscaled_image_data_32bit = media->getProcessedData32(current_time);
×
540

541
                if (apply_colormap) {
×
542
                    // TODO: Need to implement apply_colormap_for_display for float data
543
                    // For now, convert to 8-bit and apply colormap
544
                    std::vector<uint8_t> converted_8bit;
×
545
                    converted_8bit.reserve(unscaled_image_data_32bit.size());
×
546

547
                    for (float pixel_value: unscaled_image_data_32bit) {
×
548
                        // Clamp to 0-255 range and convert to uint8_t
549
                        uint8_t byte_value = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, pixel_value)));
×
550
                        converted_8bit.push_back(byte_value);
×
551
                    }
552

553
                    auto colormap_data = ImageProcessing::apply_colormap_for_display(
×
554
                            converted_8bit,
555
                            media->getImageSize(),
556
                            _media_configs[active_media_key].get()->colormap_options);
×
557

558
                    // Apply colormap and get BGRA data - make a deep copy to avoid use-after-free
559
                    unscaled_image = QImage(colormap_data.data(),
×
560
                                            media->getWidth(),
561
                                            media->getHeight(),
562
                                            QImage::Format_ARGB32)
563
                                             .copy();
×
564
                } else {
×
565
                    // No colormap, convert 32-bit float to 16-bit for higher precision display
566
                    std::vector<uint16_t> converted_16bit;
×
567
                    converted_16bit.reserve(unscaled_image_data_32bit.size());
×
568

569
                    for (float pixel_value: unscaled_image_data_32bit) {
×
570
                        // Scale from 0-255 range to 0-65535 range
571
                        uint16_t value_16bit = static_cast<uint16_t>(std::max(0.0f, std::min(255.0f, pixel_value)) * 257.0f);
×
572
                        converted_16bit.push_back(value_16bit);
×
573
                    }
574

575
                    // Create QImage and make a deep copy to avoid use-after-free
576
                    unscaled_image = QImage(reinterpret_cast<uchar const *>(converted_16bit.data()),
×
577
                                            media->getWidth(),
578
                                            media->getHeight(),
579
                                            media->getWidth() * sizeof(uint16_t),
×
580
                                            QImage::Format_Grayscale16)
581
                                             .copy();
×
582
                }
×
583
            }
×
584
        } else {
585
            // Color image processing (always 8-bit for now)
586
            auto unscaled_image_data = media->getProcessedData8(current_time);
×
587
            unscaled_image = QImage(unscaled_image_data.data(),
×
588
                                    media->getWidth(),
589
                                    media->getHeight(),
590
                                    QImage::Format_RGBA8888);
×
591
        }
×
592
    }
×
593

594

595
    // Check for multi-channel mode (multiple enabled grayscale media)
596
    if (total_visible_media > 1) {
×
597
        // Multi-channel mode: combine multiple media with colormaps
598
        unscaled_image = _combineMultipleMedia();
×
599
    }
600

601
    auto new_image = unscaled_image.scaled(
×
602
            _canvasWidth,
603
            _canvasHeight,
604
            Qt::IgnoreAspectRatio,
605
            Qt::SmoothTransformation);
×
606

607
    std::cout << "Scaled image" << std::endl;
×
608

609
    // Check if any masks are in transparency mode
610
    bool has_transparency_mask = false;
×
611
    for (auto const & [mask_key, mask_config]: _mask_configs) {
×
612
        if (mask_config->is_visible && mask_config->use_as_transparency) {
×
613
            has_transparency_mask = true;
×
614
            break;
×
615
        }
616
    }
617

618
    // If we have transparency masks, modify the new_image
619
    if (has_transparency_mask) {
×
620
        new_image = _applyTransparencyMasks(new_image);
×
621
    }
622

623
    _canvasPixmap->setPixmap(QPixmap::fromImage(new_image));
×
624
    _canvasImage = new_image;
×
625
}
32✔
626

627

628
QImage Media_Window::_combineMultipleMedia() {
×
629

630
    auto current_time = _data_manager->getCurrentTime();
×
631

632
    // Loop through configs and get the largest image size
633
    std::vector<ImageSize> media_sizes;
×
634
    for (auto const & [media_key, media_config]: _media_configs) {
×
635
        if (!media_config->is_visible) continue;
×
636

637
        auto media = _data_manager->getData<MediaData>(media_key);
×
638
        if (!media) continue;
×
639

640
        media_sizes.push_back(media->getImageSize());
×
641
    }
×
642

643
    if (media_sizes.empty()) return QImage();
×
644

645
    // Find the maximum width and height
646
    int width = 0;
×
647
    int height = 0;
×
648
    for (auto const & size: media_sizes) {
×
649
        width = std::max(width, size.width);
×
650
        height = std::max(height, size.height);
×
651
    }
652

653
    // Create combined RGBA image
654
    QImage combined_image(width, height, QImage::Format_RGBA8888);
×
655
    combined_image.fill(qRgba(0, 0, 0, 255));// Start with black background
×
656

657
    for (auto const & [media_key, media_config]: _media_configs) {
×
658
        if (!media_config->is_visible) continue;
×
659

660
        auto media = _data_manager->getData<MediaData>(media_key);
×
661
        if (!media || media->getFormat() != MediaData::DisplayFormat::Gray) {
×
662
            continue;// Skip non-grayscale media
×
663
        }
664

665
        bool apply_colormap = media_config.get()->colormap_options.active &&
×
666
                              media_config.get()->colormap_options.colormap != ColormapType::None;
×
667

668
        if (media->is8Bit()) {
×
669
            // Handle 8-bit media data
670
            auto media_data_8bit = media->getProcessedData8(current_time);
×
671

672
            if (apply_colormap) {
×
673
                auto colormap_data = ImageProcessing::apply_colormap_for_display(
×
674
                        media_data_8bit,
675
                        media->getImageSize(),
676
                        media_config.get()->colormap_options);
×
677

678
                // Use colormap data (BGRA format from OpenCV)
679
                for (int y = 0; y < media->getHeight(); ++y) {
×
680
                    for (int x = 0; x < media->getWidth(); ++x) {
×
681
                        int const pixel_idx = (y * media->getWidth() + x) * 4;
×
682

683
                        uint8_t const b = colormap_data[pixel_idx];    // Blue channel
×
684
                        uint8_t const g = colormap_data[pixel_idx + 1];// Green channel
×
685
                        uint8_t const r = colormap_data[pixel_idx + 2];// Red channel
×
686
                        uint8_t const a = colormap_data[pixel_idx + 3];// Alpha channel
×
687

688
                        // Get current pixel from combined image
689
                        QRgb current_pixel = combined_image.pixel(x, y);
×
690

691
                        // Additive blending (common for multi-channel microscopy)
692
                        uint8_t const new_r = std::min(255, qRed(current_pixel) + r);
×
693
                        uint8_t const new_g = std::min(255, qGreen(current_pixel) + g);
×
694
                        uint8_t const new_b = std::min(255, qBlue(current_pixel) + b);
×
695

696
                        combined_image.setPixel(x, y, qRgba(new_r, new_g, new_b, 255));
×
697
                    }
698
                }
699
            } else {
×
700
                // Use 8-bit grayscale data directly (no colormap)
701
                for (int y = 0; y < media->getHeight(); ++y) {
×
702
                    for (int x = 0; x < media->getWidth(); ++x) {
×
703
                        int const pixel_idx = y * media->getWidth() + x;
×
704
                        uint8_t const gray_value = media_data_8bit[pixel_idx];
×
705

706
                        // Get current pixel from combined image
707
                        QRgb current_pixel = combined_image.pixel(x, y);
×
708

709
                        // Additive blending
710
                        uint8_t const new_r = std::min(255, qRed(current_pixel) + gray_value);
×
711
                        uint8_t const new_g = std::min(255, qGreen(current_pixel) + gray_value);
×
712
                        uint8_t const new_b = std::min(255, qBlue(current_pixel) + gray_value);
×
713

714
                        combined_image.setPixel(x, y, qRgba(new_r, new_g, new_b, 255));
×
715
                    }
716
                }
717
            }
718
        } else if (media->is32Bit()) {
×
719
            // Handle 32-bit float media data
720
            auto media_data_32bit = media->getProcessedData32(current_time);
×
721

722
            if (apply_colormap) {
×
723
                // Convert to 8-bit for colormap application (temporary until float colormap is implemented)
724
                std::vector<uint8_t> converted_8bit;
×
725
                converted_8bit.reserve(media_data_32bit.size());
×
726

727
                for (float pixel_value: media_data_32bit) {
×
728
                    uint8_t byte_value = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, pixel_value)));
×
729
                    converted_8bit.push_back(byte_value);
×
730
                }
731

732
                auto colormap_data = ImageProcessing::apply_colormap_for_display(
×
733
                        converted_8bit,
734
                        media->getImageSize(),
735
                        media_config.get()->colormap_options);
×
736

737
                // Use colormap data (BGRA format from OpenCV)
738
                for (int y = 0; y < media->getHeight(); ++y) {
×
739
                    for (int x = 0; x < media->getWidth(); ++x) {
×
740
                        int const pixel_idx = (y * media->getWidth() + x) * 4;
×
741

742
                        uint8_t const b = colormap_data[pixel_idx];    // Blue channel
×
743
                        uint8_t const g = colormap_data[pixel_idx + 1];// Green channel
×
744
                        uint8_t const r = colormap_data[pixel_idx + 2];// Red channel
×
745
                        uint8_t const a = colormap_data[pixel_idx + 3];// Alpha channel
×
746

747
                        // Get current pixel from combined image
748
                        QRgb current_pixel = combined_image.pixel(x, y);
×
749

750
                        // Additive blending
751
                        uint8_t const new_r = std::min(255, qRed(current_pixel) + r);
×
752
                        uint8_t const new_g = std::min(255, qGreen(current_pixel) + g);
×
753
                        uint8_t const new_b = std::min(255, qBlue(current_pixel) + b);
×
754

755
                        combined_image.setPixel(x, y, qRgba(new_r, new_g, new_b, 255));
×
756
                    }
757
                }
758
            } else {
×
759
                // Use 32-bit float data directly (no colormap)
760
                for (int y = 0; y < media->getHeight(); ++y) {
×
761
                    for (int x = 0; x < media->getWidth(); ++x) {
×
762
                        int const pixel_idx = y * media->getWidth() + x;
×
763
                        float const float_value = media_data_32bit[pixel_idx];
×
764
                        uint8_t const gray_value = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, float_value)));
×
765

766
                        // Get current pixel from combined image
767
                        QRgb current_pixel = combined_image.pixel(x, y);
×
768

769
                        // Additive blending
770
                        uint8_t const new_r = std::min(255, qRed(current_pixel) + gray_value);
×
771
                        uint8_t const new_g = std::min(255, qGreen(current_pixel) + gray_value);
×
772
                        uint8_t const new_b = std::min(255, qBlue(current_pixel) + gray_value);
×
773

774
                        combined_image.setPixel(x, y, qRgba(new_r, new_g, new_b, 255));
×
775
                    }
776
                }
777
            }
778
        }
×
779
    }
×
780

781
    return combined_image;
×
782
}
×
783

784
void Media_Window::mousePressEvent(QGraphicsSceneMouseEvent * event) {
×
785
    if (_debug_performance) {
×
786
        std::cout << "Mouse PRESS - Button: " << (event->button() == Qt::LeftButton ? "LEFT" : "RIGHT")
×
787
                  << ", Drawing mode: " << _drawing_mode << ", Current drawing points: " << _drawing_points.size() << std::endl;
×
788
    }
789

790
    if (event->button() == Qt::LeftButton) {
×
791
        if (_drawing_mode) {
×
792
            auto pos = event->scenePos();
×
793
            _drawing_points.clear();
×
794
            _drawing_points.push_back(pos);
×
795
            _is_drawing = true;
×
796
            if (_debug_performance) {
×
797
                std::cout << "  Started drawing - cleared and added first point" << std::endl;
×
798
            }
799
        } else if (_group_selection_enabled) {
×
800
            // Handle selection on left click (when not in drawing mode and group selection is enabled)
801
            std::string data_key, data_type;
×
802
            EntityId entity_id = _findEntityAtPosition(event->scenePos(), data_key, data_type);
×
803
            
804
            if (entity_id != 0) {
×
805
                // Use group-based selection for all entity types
806
                // Check if Ctrl is held for multi-selection
807
                if (event->modifiers() & Qt::ControlModifier) {
×
808
                    if (_selected_entities.count(entity_id)) {
×
809
                        _selected_entities.erase(entity_id);
×
810
                    } else {
811
                        _selected_entities.insert(entity_id);
×
812
                        _selected_data_key = data_key;
×
813
                        _selected_data_type = data_type;
×
814
                    }
815
                } else {
816
                    // Single selection
817
                    _selected_entities.clear();
×
818
                    _selected_entities.insert(entity_id);
×
819
                    _selected_data_key = data_key;
×
820
                    _selected_data_type = data_type;
×
821
                }
822
                UpdateCanvas(); // Refresh to show selection
×
823
            } else if (!(event->modifiers() & Qt::ControlModifier)) {
×
824
                // Clear selection if clicking on empty area without Ctrl
825
                clearAllSelections();
×
826
            }
827
        }
×
828

829
        // Emit legacy signals (qreal values)
830
        emit leftClick(event->scenePos().x(), event->scenePos().y());
×
831
        emit leftClickMedia(
×
832
                event->scenePos().x() / getXAspect(),
×
833
                event->scenePos().y() / getYAspect());
×
834
        
835
        // Emit media click signal with modifier information
836
        emit leftClickMediaWithEvent(
×
837
                event->scenePos().x() / getXAspect(),
×
838
                event->scenePos().y() / getYAspect(),
×
839
                event->modifiers());
840

841
        // Emit strong-typed coordinate signals
842
        CanvasCoordinates const canvas_coords(static_cast<float>(event->scenePos().x()),
×
843
                                              static_cast<float>(event->scenePos().y()));
×
844
        MediaCoordinates const media_coords(static_cast<float>(event->scenePos().x() / getXAspect()),
×
845
                                            static_cast<float>(event->scenePos().y() / getYAspect()));
×
846
        emit leftClickCanvas(canvas_coords);
×
847
        emit leftClickMediaCoords(media_coords);
×
848

849
    } else if (event->button() == Qt::RightButton) {
×
850
        if (_drawing_mode) {
×
851
            auto pos = event->scenePos();
×
852
            _drawing_points.clear();
×
853
            _drawing_points.push_back(pos);
×
854
            _is_drawing = true;
×
855
        }
856

857
        // Emit legacy signals (qreal values)
858
        emit rightClick(event->scenePos().x(), event->scenePos().y());
×
859
        emit rightClickMedia(
×
860
                event->scenePos().x() / getXAspect(),
×
861
                event->scenePos().y() / getYAspect());
×
862

863
        // Emit strong-typed coordinate signals
864
        CanvasCoordinates const canvas_coords(static_cast<float>(event->scenePos().x()),
×
865
                                              static_cast<float>(event->scenePos().y()));
×
866
        MediaCoordinates const media_coords(static_cast<float>(event->scenePos().x() / getXAspect()),
×
867
                                            static_cast<float>(event->scenePos().y() / getYAspect()));
×
868
        emit rightClickCanvas(canvas_coords);
×
869
        emit rightClickMediaCoords(media_coords);
×
870

871
    } else {
872
        QGraphicsScene::mousePressEvent(event);
×
873
    }
874
}
×
875
void Media_Window::mouseReleaseEvent(QGraphicsSceneMouseEvent * event) {
×
876
    if (_debug_performance) {
×
877
        std::cout << "Mouse RELEASE - Button: " << (event->button() == Qt::LeftButton ? "LEFT" : "RIGHT")
×
878
                  << ", Was drawing: " << _is_drawing << ", Drawing points: " << _drawing_points.size() << std::endl;
×
879
    }
880

881
    if (event->button() == Qt::LeftButton) {
×
882
        // Always emit leftRelease signal
883
        emit leftRelease();
×
884

885
        // Only emit drawing-specific signal and reset drawing state when in drawing mode
886
        if (_is_drawing) {
×
887
            _is_drawing = false;
×
888
            emit leftReleaseDrawing();
×
889
            if (_debug_performance) {
×
890
                std::cout << "  Drawing finished - emitted leftReleaseDrawing signal" << std::endl;
×
891
            }
892
        }
893
    } else if (event->button() == Qt::RightButton) {
×
894
        // Always emit rightRelease signal
895
        emit rightRelease();
×
896

897
        // Only emit drawing-specific signal and reset drawing state when in drawing mode
898
        if (_is_drawing) {
×
899
            _is_drawing = false;
×
900
            emit rightReleaseDrawing();
×
901
        }
902
    }
903
    QGraphicsScene::mouseReleaseEvent(event);
×
904
}
×
905
void Media_Window::mouseMoveEvent(QGraphicsSceneMouseEvent * event) {
×
906
    static int move_count = 0;
907
    move_count++;
×
908

909
    auto pos = event->scenePos();
×
910

911
    _hover_position = pos;
×
912

913
    if (_is_drawing) {
×
914
        _drawing_points.push_back(pos);
×
915
        if (_debug_performance && move_count % 10 == 0) {// Only print every 10th move to avoid spam
×
916
            std::cout << "Mouse MOVE #" << move_count << " - Drawing: adding point (total: "
×
917
                      << _drawing_points.size() << ")" << std::endl;
×
918
        }
919
    } else if (_debug_performance && move_count % 50 == 0) {// Print every 50th move when not drawing
×
920
        std::cout << "Mouse MOVE #" << move_count << " - Hover only" << std::endl;
×
921
    }
922

923
    // Emit legacy signal
924
    emit mouseMove(event->scenePos().x(), event->scenePos().y());
×
925

926
    // Emit strong-typed coordinate signal
927
    CanvasCoordinates const canvas_coords(static_cast<float>(event->scenePos().x()),
×
928
                                          static_cast<float>(event->scenePos().y()));
×
929
    emit mouseMoveCanvas(canvas_coords);
×
930

931
    QGraphicsScene::mouseMoveEvent(event);
×
932
}
×
933

934
void Media_Window::contextMenuEvent(QGraphicsSceneContextMenuEvent * event) {
×
935
    // Only show context menu if we have selections and a group manager
936
    if (!hasSelections() || !_group_manager) {
×
937
        QGraphicsScene::contextMenuEvent(event);
×
938
        return;
×
939
    }
940

941
    _updateContextMenuActions();
×
942
    _showContextMenu(event->screenPos());
×
943
}
944

945
float Media_Window::getXAspect() const {
16✔
946

947
    std::string active_media_key;
16✔
948
    for (auto const & [config_key, config]: _media_configs) {
32✔
949
        if (config->is_visible) {
16✔
950
            active_media_key = config_key;
×
951
            break;
×
952
        }
953
    }
954
    if (active_media_key.empty()) {
16✔
955
        // No active media, return default aspect ratio
956
        return 1.0f;
16✔
957
    }
958

959
    auto _media = _data_manager->getData<MediaData>(active_media_key);
×
960
    if (!_media) {
×
961
        return 1.0f;// Default aspect ratio
×
962
    }
963

964
    float const scale_width = static_cast<float>(_canvasWidth) / static_cast<float>(_media->getWidth());
×
965

966
    return scale_width;
×
967
}
16✔
968

969
float Media_Window::getYAspect() const {
16✔
970

971
    std::string active_media_key;
16✔
972
    for (auto const & [config_key, config]: _media_configs) {
32✔
973
        if (config->is_visible) {
16✔
974
            active_media_key = config_key;
×
975
            break;
×
976
        }
977
    }
978
    if (active_media_key.empty()) {
16✔
979
        // No active media, return default aspect ratio
980
        return 1.0f;
16✔
981
    }
982

983
    auto _media = _data_manager->getData<MediaData>(active_media_key);
×
984
    if (!_media) {
×
985
        return 1.0f;// Default aspect ratio
×
986
    }
987

988
    float const scale_height = static_cast<float>(_canvasHeight) / static_cast<float>(_media->getHeight());
×
989

990
    return scale_height;
×
991
}
16✔
992

993
void Media_Window::_plotLineData() {
16✔
994
    auto const current_time = _data_manager->getCurrentTime();
16✔
995

996
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
16✔
997

998
    auto xAspect = getXAspect();
16✔
999
    auto yAspect = getYAspect();
16✔
1000

1001
    for (auto const & [line_key, _line_config]: _line_configs) {
16✔
1002

1003
        if (!_line_config.get()->is_visible) continue;
×
1004

1005
        auto plot_color = plot_color_with_alpha(_line_config.get());
×
1006

1007
        auto line_timeframe_key = _data_manager->getTimeKey(line_key);
×
1008
        auto line_timeframe = _data_manager->getTime(line_timeframe_key);
×
1009

1010
        auto line_data = _data_manager->getData<LineData>(line_key);
×
1011
        auto lineData = line_data->getAtTime(TimeFrameIndex(current_time), video_timeframe.get(), line_timeframe.get());
×
1012
        auto entityIds = line_data->getEntityIdsAtTime(TimeFrameIndex(current_time), video_timeframe.get(), line_timeframe.get());
×
1013

1014
        // Check for line-specific image size scaling
1015
        auto image_size = line_data->getImageSize();
×
1016

1017
        if (image_size.height != -1) {
×
1018
            auto const line_height = static_cast<float>(image_size.height);
×
1019
            yAspect = static_cast<float>(_canvasHeight) / line_height;
×
1020
        }
1021

1022
        if (image_size.width != -1) {
×
1023
            auto const line_width = static_cast<float>(image_size.width);
×
1024
            xAspect = static_cast<float>(_canvasWidth) / line_width;
×
1025
        }
1026

1027
        if (lineData.empty()) {
×
1028
            continue;
×
1029
        }
1030

1031
        // Ensure we have matching line data and entity IDs
1032
        size_t line_count = std::min(lineData.size(), entityIds.size());
×
1033

1034
        for (int line_idx = 0; line_idx < static_cast<int>(line_count); ++line_idx) {
×
1035
            auto const & single_line = lineData[line_idx];
×
1036
            EntityId entity_id = static_cast<size_t>(line_idx) < entityIds.size() ? entityIds[line_idx] : 0;
×
1037

1038
            if (single_line.empty()) {
×
1039
                continue;
×
1040
            }
1041

1042
            // Use group-aware color if available, otherwise use default plot color
1043
            QColor line_color = _getGroupAwareColor(entity_id, QColor::fromRgba(plot_color));
×
1044

1045
            // Use segment if enabled, otherwise use full line
1046
            Line2D line_to_plot;
×
1047
            if (_line_config.get()->show_segment) {
×
1048
                float const start_percentage = static_cast<float>(_line_config.get()->segment_start_percentage) / 100.0f;
×
1049
                float const end_percentage = static_cast<float>(_line_config.get()->segment_end_percentage) / 100.0f;
×
1050
                line_to_plot = get_segment_between_percentages(single_line, start_percentage, end_percentage);
×
1051

1052
                // If segment is empty (invalid percentages), skip this line
1053
                if (line_to_plot.empty()) {
×
1054
                    continue;
×
1055
                }
1056
            } else {
1057
                line_to_plot = single_line;
×
1058
            }
1059

1060
            QPainterPath path = QPainterPath();
×
1061

1062
            auto single_line_thres = 1000.0;
×
1063

1064
            path.moveTo(QPointF(static_cast<float>(line_to_plot[0].x) * xAspect, static_cast<float>(line_to_plot[0].y) * yAspect));
×
1065

1066
            for (size_t i = 1; i < line_to_plot.size(); i++) {
×
1067
                auto dx = line_to_plot[i].x - line_to_plot[i - 1].x;
×
1068
                auto dy = line_to_plot[i].y - line_to_plot[i - 1].y;
×
1069
                auto d = std::sqrt((dx * dx) + (dy * dy));
×
1070
                if (d > single_line_thres) {
×
1071
                    path.moveTo(QPointF(static_cast<float>(line_to_plot[i].x) * xAspect, static_cast<float>(line_to_plot[i].y) * yAspect));
×
1072
                } else {
1073
                    path.lineTo(QPointF(static_cast<float>(line_to_plot[i].x) * xAspect, static_cast<float>(line_to_plot[i].y) * yAspect));
×
1074
                }
1075
            }
1076

1077
            // Create pen with group-aware color and configurable thickness
1078
            QPen linePen;
×
1079
            linePen.setColor(line_color);
×
1080
            linePen.setWidth(_line_config.get()->line_thickness);
×
1081

1082
            auto linePath = addPath(path, linePen);
×
1083
            _line_paths.append(linePath);
×
1084

1085
            // Add dot at line base (always filled) - use group-aware color
1086
            QColor const dot_color = line_color;
×
1087
            auto ellipse = addEllipse(
×
1088
                    static_cast<float>(line_to_plot[0].x) * xAspect - 2.5,
×
1089
                    static_cast<float>(line_to_plot[0].y) * yAspect - 2.5,
×
1090
                    5.0, 5.0,
1091
                    QPen(dot_color),
×
1092
                    QBrush(dot_color));
×
1093
            _points.append(ellipse);
×
1094

1095
            // If show_points is enabled, add open circles at each point on the line
1096
            if (_line_config.get()->show_points) {
×
1097
                // Create pen and brush for open circles
1098
                QPen pointPen(dot_color);
×
1099
                pointPen.setWidth(1);
×
1100

1101
                // Empty brush for open circles
1102
                QBrush const emptyBrush(Qt::NoBrush);
×
1103

1104
                // Start from the second point (first one is already shown as filled)
1105
                for (size_t i = 1; i < line_to_plot.size(); i++) {
×
1106
                    auto ellipse = addEllipse(
×
1107
                            static_cast<float>(line_to_plot[i].x) * xAspect - 2.5,
×
1108
                            static_cast<float>(line_to_plot[i].y) * yAspect - 2.5,
×
1109
                            5.0, 5.0,
1110
                            pointPen,
1111
                            emptyBrush);
1112
                    _points.append(ellipse);
×
1113
                }
1114
            }
×
1115

1116
            // If position marker is enabled, add a marker at the specified percentage
1117
            if (_line_config.get()->show_position_marker) {
×
1118
                float const percentage = static_cast<float>(_line_config.get()->position_percentage) / 100.0f;
×
1119
                Point2D<float> const marker_pos = get_position_at_percentage(line_to_plot, percentage);
×
1120

1121
                float const marker_x = marker_pos.x * xAspect;
×
1122
                float const marker_y = marker_pos.y * yAspect;
×
1123

1124
                // Create a distinctive marker (filled circle with border)
1125
                QPen markerPen(QColor(255, 255, 255));// White border
×
1126
                markerPen.setWidth(2);
×
1127
                QBrush const markerBrush(dot_color);// Same color as line but filled
×
1128

1129
                auto marker = addEllipse(
×
1130
                        marker_x - 4.0f,
×
1131
                        marker_y - 4.0f,
×
1132
                        8.0f, 8.0f,
1133
                        markerPen,
1134
                        markerBrush);
1135
                _points.append(marker);
×
1136
            }
×
1137
        }
×
1138
    }
×
1139
}
32✔
1140

1141
void Media_Window::_plotMaskData() {
16✔
1142
    // Note: MaskData does not currently support EntityIds for group-aware coloring
1143
    // This would need to be implemented similar to PointData and LineData
1144
    auto const current_time = _data_manager->getCurrentTime();
16✔
1145

1146
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
16✔
1147

1148
    for (auto const & [mask_key, _mask_config]: _mask_configs) {
29✔
1149
        if (!_mask_config.get()->is_visible) continue;
13✔
1150

1151
        auto plot_color = plot_color_with_alpha(_mask_config.get());
×
1152

1153
        auto mask = _data_manager->getData<MaskData>(mask_key);
×
1154
        auto image_size = mask->getImageSize();
×
1155

1156
        auto mask_timeframe_key = _data_manager->getTimeKey(mask_key);
×
1157
        auto mask_timeframe = _data_manager->getTime(mask_timeframe_key);
×
1158

1159
        // Check for preview data first
1160
        std::vector<Mask2D> maskData;
×
1161
        std::vector<Mask2D> maskData2;
×
1162

1163
        if (_mask_preview_active && _preview_mask_data.count(mask_key) > 0) {
×
1164
            // Use preview data
1165
            maskData = _preview_mask_data[mask_key];
×
1166
            maskData2.clear();// No time -1 data for preview
×
1167
        } else {
1168
            // Use original data
1169
            maskData = mask->getAtTime(TimeFrameIndex(current_time), video_timeframe.get(), mask_timeframe.get());
×
1170
            maskData2 = mask->getAtTime(TimeFrameIndex(-1));
×
1171
        }
1172

1173
        _plotSingleMaskData(maskData, image_size, plot_color, _mask_config.get());
×
1174
        _plotSingleMaskData(maskData2, image_size, plot_color, _mask_config.get());
×
1175

1176
        // Plot bounding boxes if enabled
1177
        if (_mask_config.get()->show_bounding_box) {
×
1178
            // Calculate scaling factors based on mask image size, not media aspect ratio
1179
            float const xAspect = static_cast<float>(_canvasWidth) / static_cast<float>(image_size.width);
×
1180
            float const yAspect = static_cast<float>(_canvasHeight) / static_cast<float>(image_size.height);
×
1181

1182
            // For current time masks
1183
            for (auto const & single_mask: maskData) {
×
1184
                if (!single_mask.empty()) {
×
1185
                    auto bounding_box = get_bounding_box(single_mask);
×
1186
                    auto min_point = bounding_box.first;
×
1187
                    auto max_point = bounding_box.second;
×
1188

1189
                    // Scale coordinates to canvas using mask image size
1190
                    float const min_x = static_cast<float>(min_point.x) * xAspect;
×
1191
                    float const min_y = static_cast<float>(min_point.y) * yAspect;
×
1192
                    float const max_x = static_cast<float>(max_point.x) * xAspect;
×
1193
                    float const max_y = static_cast<float>(max_point.y) * yAspect;
×
1194

1195
                    // Draw bounding box rectangle (no fill, just outline)
1196
                    QPen boundingBoxPen(plot_color);
×
1197
                    boundingBoxPen.setWidth(2);
×
1198
                    QBrush const emptyBrush(Qt::NoBrush);
×
1199

1200
                    auto boundingBoxRect = addRect(min_x, min_y, max_x - min_x, max_y - min_y,
×
1201
                                                   boundingBoxPen, emptyBrush);
1202
                    _mask_bounding_boxes.append(boundingBoxRect);
×
1203
                }
×
1204
            }
1205

1206
            // For time -1 masks
1207
            for (auto const & single_mask: maskData2) {
×
1208
                if (!single_mask.empty()) {
×
1209
                    auto bounding_box = get_bounding_box(single_mask);
×
1210
                    auto min_point = bounding_box.first;
×
1211
                    auto max_point = bounding_box.second;
×
1212

1213
                    // Scale coordinates to canvas using mask image size
1214
                    float const min_x = static_cast<float>(min_point.x) * xAspect;
×
1215
                    float const min_y = static_cast<float>(min_point.y) * yAspect;
×
1216
                    float const max_x = static_cast<float>(max_point.x) * xAspect;
×
1217
                    float const max_y = static_cast<float>(max_point.y) * yAspect;
×
1218

1219
                    // Draw bounding box rectangle (no fill, just outline)
1220
                    QPen boundingBoxPen(plot_color);
×
1221
                    boundingBoxPen.setWidth(2);
×
1222
                    QBrush const emptyBrush(Qt::NoBrush);
×
1223

1224
                    auto boundingBoxRect = addRect(min_x, min_y, max_x - min_x, max_y - min_y,
×
1225
                                                   boundingBoxPen, emptyBrush);
1226
                    _mask_bounding_boxes.append(boundingBoxRect);
×
1227
                }
×
1228
            }
1229
        }
1230

1231
        // Plot outlines if enabled
1232
        if (_mask_config.get()->show_outline) {
×
1233
            // Create a slightly darker color for outlines
1234
            QRgb const outline_color = plot_color;
×
1235

1236
            // For current time masks
1237
            for (auto const & single_mask: maskData) {
×
1238
                if (!single_mask.empty()) {
×
1239
                    // Generate outline mask with thickness of 2 pixels
1240
                    auto outline_mask = generate_outline_mask(single_mask, 2, image_size.width, image_size.height);
×
1241

1242
                    if (!outline_mask.empty()) {
×
1243
                        // Plot the outline mask using the same approach as regular masks
1244
                        _plotSingleMaskData({outline_mask}, image_size, outline_color, _mask_config.get());
×
1245
                    }
1246
                }
×
1247
            }
1248

1249
            // For time -1 masks
1250
            for (auto const & single_mask: maskData2) {
×
1251
                if (!single_mask.empty()) {
×
1252
                    // Generate outline mask with thickness of 2 pixels
1253
                    auto outline_mask = generate_outline_mask(single_mask, 2, image_size.width, image_size.height);
×
1254

1255
                    if (!outline_mask.empty()) {
×
1256
                        // Plot the outline mask using the same approach as regular masks
1257
                        _plotSingleMaskData({outline_mask}, image_size, outline_color, _mask_config.get());
×
1258
                    }
1259
                }
×
1260
            }
1261
        }
1262
    }
×
1263
}
32✔
1264

1265
void Media_Window::_plotSingleMaskData(std::vector<Mask2D> const & maskData, ImageSize mask_size, QRgb plot_color, MaskDisplayOptions const * mask_config) {
×
1266
    // Skip transparency masks as they are handled at the media level
1267
    if (mask_config && mask_config->use_as_transparency) {
×
1268
        return;
×
1269
    }
1270

1271
    for (auto const & single_mask: maskData) {
×
1272
        // Normal mode: overlay mask on top of media
1273
        QImage unscaled_mask_image(mask_size.width, mask_size.height, QImage::Format::Format_ARGB32);
×
1274
        unscaled_mask_image.fill(0);
×
1275

1276
        for (auto const point: single_mask) {
×
1277
            unscaled_mask_image.setPixel(
×
1278
                    QPoint(static_cast<int>(point.x), static_cast<int>(point.y)),
×
1279
                    plot_color);
1280
        }
1281

1282
        auto scaled_mask_image = unscaled_mask_image.scaled(_canvasWidth, _canvasHeight);
×
1283
        auto maskPixmap = addPixmap(QPixmap::fromImage(scaled_mask_image));
×
1284
        _masks.append(maskPixmap);
×
1285
    }
×
1286
}
1287

1288
QImage Media_Window::_applyTransparencyMasks(QImage const & media_image) {
×
1289
    std::cout << "Applying transparency masks..." << std::endl;
×
1290

1291
    std::cout << "Media image size: " << media_image.width() << "x" << media_image.height() << std::endl;
×
1292
    std::cout << "Canvas dimensions: " << _canvasWidth << "x" << _canvasHeight << std::endl;
×
1293

1294
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
×
1295

1296
    QImage final_image = media_image;
×
1297

1298
    int transparency_mask_count = 0;
×
1299
    int const total_mask_points = 0;
×
1300

1301
    // Process all transparency masks
1302
    for (auto const & [mask_key, mask_config]: _mask_configs) {
×
1303
        if (!mask_config->is_visible || !mask_config->use_as_transparency) {
×
1304
            continue;
×
1305
        }
1306

1307
        transparency_mask_count++;
×
1308
        std::cout << "Processing transparency mask: " << mask_key << std::endl;
×
1309

1310
        auto mask_data = _data_manager->getData<MaskData>(mask_key);
×
1311
        auto image_size = mask_data->getImageSize();
×
1312

1313
        auto mask_timeframe_key = _data_manager->getTimeKey(mask_key);
×
1314
        auto mask_timeframe = _data_manager->getTime(mask_timeframe_key);
×
1315

1316
        std::cout << "Mask image size: " << image_size.width << "x" << image_size.height << std::endl;
×
1317

1318
        auto const current_time = _data_manager->getCurrentTime();
×
1319
        auto maskData = mask_data->getAtTime(TimeFrameIndex(current_time), video_timeframe.get(), mask_timeframe.get());
×
1320

1321
        std::cout << "Mask data size: " << maskData.size() << std::endl;
×
1322

1323
        // Calculate scaling factors
1324
        float const xAspect = static_cast<float>(_canvasWidth) / static_cast<float>(image_size.width);
×
1325
        float const yAspect = static_cast<float>(_canvasHeight) / static_cast<float>(image_size.height);
×
1326

1327
        std::cout << "Scaling factors: x=" << xAspect << ", y=" << yAspect << std::endl;
×
1328

1329
        QImage unscaled_mask_image(image_size.width, image_size.height, QImage::Format::Format_ARGB32);
×
1330
        unscaled_mask_image.fill(0);
×
1331
        // Add mask points to combined mask
1332
        for (auto const & single_mask: maskData) {
×
1333
            for (auto const point: single_mask) {
×
1334
                unscaled_mask_image.setPixel(
×
1335
                        QPoint(static_cast<int>(point.x), static_cast<int>(point.y)),
×
1336
                        qRgba(255, 255, 255, 255));
1337
            }
1338
        }
1339

1340
        QImage const scaled_mask_image = unscaled_mask_image.scaled(_canvasWidth, _canvasHeight);
×
1341
        // I want to copy final_image where scaled_mask_image is white, and keep the rest of the image the same
1342
        for (int y = 0; y < _canvasHeight; ++y) {
×
1343
            for (int x = 0; x < _canvasWidth; ++x) {
×
1344
                if (scaled_mask_image.pixel(x, y) == qRgba(255, 255, 255, 255)) {
×
1345
                    final_image.setPixel(x, y, final_image.pixel(x, y));
×
1346
                } else {
1347
                    final_image.setPixel(x, y, qRgba(0, 0, 0, 255));
×
1348
                }
1349
            }
1350
        }
1351
    }
×
1352

1353

1354
    return final_image;
×
1355
}
×
1356

1357
void Media_Window::_plotPointData() {
16✔
1358

1359
    auto const current_time = TimeFrameIndex(_data_manager->getCurrentTime());
16✔
1360
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
16✔
1361

1362
    if (!video_timeframe) {
16✔
1363
        std::cerr << "Error: Could not get video timeframe 'time' for point conversion" << std::endl;
×
1364
        return;
×
1365
    }
1366

1367
    for (auto const & [point_key, _point_config]: _point_configs) {
16✔
1368
        if (!_point_config.get()->is_visible) continue;
×
1369

1370
        auto plot_color = plot_color_with_alpha(_point_config.get());
×
1371

1372
        auto point = _data_manager->getData<PointData>(point_key);
×
1373

1374
        auto point_timeframe_key = _data_manager->getTimeKey(point_key);
×
1375
        if (point_timeframe_key.empty()) {
×
1376
            std::cerr << "Error: No timeframe found for point data: " << point_key << std::endl;
×
1377
            continue;
×
1378
        }
1379

1380
        auto point_timeframe = _data_manager->getTime(point_timeframe_key);
×
1381

1382
        auto xAspect = getXAspect();
×
1383
        auto yAspect = getYAspect();
×
1384

1385
        auto image_size = point->getImageSize();
×
1386

1387
        if (image_size.height != -1) {
×
1388
            auto const mask_height = static_cast<float>(image_size.height);
×
1389
            yAspect = static_cast<float>(_canvasHeight) / mask_height;
×
1390
        }
1391

1392
        if (image_size.width != -1) {
×
1393
            auto const mask_width = static_cast<float>(image_size.width);
×
1394
            xAspect = static_cast<float>(_canvasWidth) / mask_width;
×
1395
        }
1396

1397
        auto pointData = point->getAtTime(current_time, video_timeframe.get(), point_timeframe.get());
×
1398
        auto entityIds = point->getEntityIdsAtTime(current_time);
×
1399

1400
        // Get configurable point size
1401
        float const point_size = static_cast<float>(_point_config.get()->point_size);
×
1402

1403
        // Ensure we have matching point data and entity IDs
1404
        size_t count = std::min(pointData.size(), entityIds.size());
×
1405
        
1406
        for (size_t i = 0; i < count; ++i) {
×
1407
            auto const & single_point = pointData[i];
×
1408
            EntityId entity_id = entityIds[i];
×
1409
            
1410
            float const x_pos = single_point.x * xAspect;
×
1411
            float const y_pos = single_point.y * yAspect;
×
1412

1413
            // Use group-aware color if available, otherwise use default plot color
1414
            QColor point_color = _getGroupAwareColor(entity_id, QColor::fromRgba(plot_color));
×
1415
            
1416
            // Check if this point is selected to add highlight
1417
            bool is_selected = _selected_entities.count(entity_id) > 0;
×
1418
            
1419
            // Add selection highlight if point is selected
1420
            if (is_selected) {
×
1421
                QPen highlight_pen(Qt::yellow);
×
1422
                highlight_pen.setWidth(4);
×
1423
                QBrush highlight_brush(Qt::transparent);
×
1424
                auto highlight_circle = addEllipse(x_pos - point_size, y_pos - point_size,
×
1425
                                                   point_size * 2, point_size * 2, 
×
1426
                                                   highlight_pen, highlight_brush);
1427
                _points.append(highlight_circle);
×
1428
            }
×
1429

1430
            // Create the appropriate marker shape based on configuration
1431
            switch (_point_config.get()->marker_shape) {
×
1432
                case PointMarkerShape::Circle: {
×
1433
                    QPen pen(point_color);
×
1434
                    pen.setWidth(2);
×
1435
                    QBrush const brush(point_color);
×
1436
                    auto ellipse = addEllipse(x_pos - point_size / 2, y_pos - point_size / 2,
×
1437
                                              point_size, point_size, pen, brush);
1438
                    _points.append(ellipse);
×
1439
                    break;
×
1440
                }
×
1441
                case PointMarkerShape::Square: {
×
1442
                    QPen pen(point_color);
×
1443
                    pen.setWidth(2);
×
1444
                    QBrush const brush(point_color);
×
1445
                    auto rect = addRect(x_pos - point_size / 2, y_pos - point_size / 2,
×
1446
                                        point_size, point_size, pen, brush);
1447
                    _points.append(rect);
×
1448
                    break;
×
1449
                }
×
1450
                case PointMarkerShape::Triangle: {
×
1451
                    QPen pen(point_color);
×
1452
                    pen.setWidth(2);
×
1453
                    QBrush const brush(point_color);
×
1454

1455
                    // Create triangle polygon
1456
                    QPolygonF triangle;
×
1457
                    float const half_size = point_size / 2;
×
1458
                    triangle << QPointF(x_pos, y_pos - half_size)             // Top point
×
1459
                             << QPointF(x_pos - half_size, y_pos + half_size) // Bottom left
×
1460
                             << QPointF(x_pos + half_size, y_pos + half_size);// Bottom right
×
1461

1462
                    auto polygon = addPolygon(triangle, pen, brush);
×
1463
                    _points.append(polygon);
×
1464
                    break;
×
1465
                }
×
1466
                case PointMarkerShape::Cross: {
×
1467
                    QPen pen(point_color);
×
1468
                    pen.setWidth(3);
×
1469

1470
                    float const half_size = point_size / 2;
×
1471
                    // Draw horizontal line
1472
                    auto hLine = addLine(x_pos - half_size, y_pos, x_pos + half_size, y_pos, pen);
×
1473
                    _points.append(hLine);
×
1474

1475
                    // Draw vertical line
1476
                    auto vLine = addLine(x_pos, y_pos - half_size, x_pos, y_pos + half_size, pen);
×
1477
                    _points.append(vLine);
×
1478
                    break;
×
1479
                }
×
1480
                case PointMarkerShape::X: {
×
1481
                    QPen pen(point_color);
×
1482
                    pen.setWidth(3);
×
1483

1484
                    float const half_size = point_size / 2;
×
1485
                    // Draw diagonal line (\)
1486
                    auto dLine1 = addLine(x_pos - half_size, y_pos - half_size,
×
1487
                                          x_pos + half_size, y_pos + half_size, pen);
×
1488
                    _points.append(dLine1);
×
1489

1490
                    // Draw diagonal line (/)
1491
                    auto dLine2 = addLine(x_pos - half_size, y_pos + half_size,
×
1492
                                          x_pos + half_size, y_pos - half_size, pen);
×
1493
                    _points.append(dLine2);
×
1494
                    break;
×
1495
                }
×
1496
                case PointMarkerShape::Diamond: {
×
1497
                    QPen pen(point_color);
×
1498
                    pen.setWidth(2);
×
1499
                    QBrush brush(point_color);
×
1500

1501
                    // Create diamond polygon (rotated square)
1502
                    QPolygonF diamond;
×
1503
                    float const half_size = point_size / 2;
×
1504
                    diamond << QPointF(x_pos, y_pos - half_size) // Top
×
1505
                            << QPointF(x_pos + half_size, y_pos) // Right
×
1506
                            << QPointF(x_pos, y_pos + half_size) // Bottom
×
1507
                            << QPointF(x_pos - half_size, y_pos);// Left
×
1508

1509
                    auto polygon = addPolygon(diamond, pen, brush);
×
1510
                    _points.append(polygon);
×
1511
                    break;
×
1512
                }
×
1513
            }
1514
        }
1515
    }
×
1516
}
16✔
1517

1518
void Media_Window::_plotDigitalIntervalSeries() {
16✔
1519
    auto const current_time = _data_manager->getCurrentTime();
16✔
1520
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
16✔
1521

1522
    for (auto const & [key, _interval_config]: _interval_configs) {
16✔
1523
        if (!_interval_config.get()->is_visible) continue;
×
1524

1525
        // Only render if using Box plotting style
1526
        if (_interval_config.get()->plotting_style != IntervalPlottingStyle::Box) continue;
×
1527

1528
        auto plot_color = plot_color_with_alpha(_interval_config.get());
×
1529

1530
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(key);
×
1531

1532
        // Get the timeframes for conversion
1533
        auto interval_timeframe_key = _data_manager->getTimeKey(key);
×
1534
        if (interval_timeframe_key.empty()) {
×
1535
            std::cerr << "Error: No timeframe found for digital interval series: " << key << std::endl;
×
1536
            continue;
×
1537
        }
1538

1539
        auto interval_timeframe = _data_manager->getTime(interval_timeframe_key);
×
1540

1541
        if (!video_timeframe) {
×
1542
            std::cerr << "Error: Could not get video timeframe 'time' for interval conversion" << std::endl;
×
1543
            continue;
×
1544
        }
1545
        if (!interval_timeframe) {
×
1546
            std::cerr << "Error: Could not get interval timeframe '" << interval_timeframe_key
×
1547
                      << "' for series: " << key << std::endl;
×
1548
            continue;
×
1549
        }
1550

1551
        bool const needs_conversion = _needsTimeFrameConversion(video_timeframe, interval_timeframe);
×
1552

1553
        // Generate relative times based on frame range setting
1554
        std::vector<int> relative_times;
×
1555
        int const frame_range = _interval_config->frame_range;
×
1556
        for (int i = -frame_range; i <= frame_range; ++i) {
×
1557
            relative_times.push_back(i);
×
1558
        }
1559

1560
        int const square_size = _interval_config->box_size;
×
1561

1562
        // Calculate position based on location setting
1563
        int start_x, start_y;
1564
        switch (_interval_config->location) {
×
1565
            case IntervalLocation::TopLeft:
×
1566
                start_x = 0;
×
1567
                start_y = 0;
×
1568
                break;
×
1569
            case IntervalLocation::TopRight:
×
1570
                start_x = _canvasWidth - square_size * static_cast<int>(relative_times.size());
×
1571
                start_y = 0;
×
1572
                break;
×
1573
            case IntervalLocation::BottomLeft:
×
1574
                start_x = 0;
×
1575
                start_y = _canvasHeight - square_size;
×
1576
                break;
×
1577
            case IntervalLocation::BottomRight:
×
1578
                start_x = _canvasWidth - square_size * static_cast<int>(relative_times.size());
×
1579
                start_y = _canvasHeight - square_size;
×
1580
                break;
×
1581
        }
1582

1583
        for (size_t i = 0; i < relative_times.size(); ++i) {
×
1584
            int const video_time = current_time + relative_times[i];
×
1585
            int query_time = video_time;// Default: no conversion needed
×
1586

1587
            if (needs_conversion) {
×
1588
                // Convert from video timeframe ("time") to interval series timeframe
1589
                // 1. Convert video time index to actual time value
1590
                int const video_time_value = video_timeframe->getTimeAtIndex(TimeFrameIndex(video_time));
×
1591

1592
                // 2. Convert time value to index in interval series timeframe
1593
                query_time = interval_timeframe->getIndexAtTime(static_cast<float>(video_time_value)).getValue();
×
1594
            }
1595

1596
            bool const event_present = interval_series->isEventAtTime(TimeFrameIndex(query_time));
×
1597

1598
            auto color = event_present ? plot_color : QColor(255, 255, 255, 10);// Transparent if no event
×
1599

1600
            auto intervalPixmap = addRect(
×
1601
                    start_x + i * square_size,
×
1602
                    start_y,
1603
                    square_size,
1604
                    square_size,
1605
                    QPen(Qt::black),// Black border
×
1606
                    QBrush(color)   // Fill with color if event is present
×
1607
            );
×
1608

1609
            _intervals.append(intervalPixmap);
×
1610
        }
1611
    }
×
1612
}
32✔
1613

1614
void Media_Window::_plotDigitalIntervalBorders() {
16✔
1615
    auto const current_time = _data_manager->getCurrentTime();
16✔
1616

1617
    for (auto const & [key, _interval_config]: _interval_configs) {
16✔
1618
        if (!_interval_config.get()->is_visible) continue;
×
1619

1620
        // Only render if using Border plotting style
1621
        if (_interval_config.get()->plotting_style != IntervalPlottingStyle::Border) continue;
×
1622

1623
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(key);
×
1624

1625
        // Get the timeframes for conversion
1626
        auto interval_timeframe_key = _data_manager->getTimeKey(key);
×
1627
        if (interval_timeframe_key.empty()) {
×
1628
            std::cerr << "Error: No timeframe found for digital interval series: " << key << std::endl;
×
1629
            continue;
×
1630
        }
1631

1632
        auto video_timeframe = _data_manager->getTime(TimeKey("time"));
×
1633
        auto interval_timeframe = _data_manager->getTime(interval_timeframe_key);
×
1634

1635
        if (!video_timeframe) {
×
1636
            std::cerr << "Error: Could not get video timeframe 'time' for interval conversion" << std::endl;
×
1637
            continue;
×
1638
        }
1639
        if (!interval_timeframe) {
×
1640
            std::cerr << "Error: Could not get interval timeframe '" << interval_timeframe_key
×
1641
                      << "' for series: " << key << std::endl;
×
1642
            continue;
×
1643
        }
1644

1645
        bool const needs_conversion = _needsTimeFrameConversion(video_timeframe, interval_timeframe);
×
1646

1647
        // Check if an interval is present at the current frame
1648
        bool interval_present = false;
×
1649
        if (needs_conversion) {
×
1650
            // Convert current video time to interval timeframe
1651
            auto video_time = video_timeframe->getTimeAtIndex(TimeFrameIndex(current_time));
×
1652
            auto interval_index = interval_timeframe->getIndexAtTime(video_time);
×
1653
            interval_present = interval_series->isEventAtTime(interval_index);
×
1654
        } else {
1655
            // Direct comparison (no timeframe conversion needed)
1656
            interval_present = interval_series->isEventAtTime(TimeFrameIndex(current_time));
×
1657
        }
1658

1659
        // If an interval is present, draw a border around the entire image
1660
        if (interval_present) {
×
1661
            auto plot_color = plot_color_with_alpha(_interval_config.get());
×
1662

1663
            // Get border thickness from config
1664
            int const thickness = _interval_config->border_thickness;
×
1665

1666
            QPen border_pen(plot_color);
×
1667
            border_pen.setWidth(thickness);
×
1668

1669
            // Draw border as 4 rectangles around the edges of the canvas
1670
            // Top border
1671
            auto top_border = addRect(0, 0, _canvasWidth, thickness, border_pen, QBrush(plot_color));
×
1672
            _intervals.append(top_border);
×
1673

1674
            // Bottom border
1675
            auto bottom_border = addRect(0, _canvasHeight - thickness, _canvasWidth, thickness, border_pen, QBrush(plot_color));
×
1676
            _intervals.append(bottom_border);
×
1677

1678
            // Left border
1679
            auto left_border = addRect(0, 0, thickness, _canvasHeight, border_pen, QBrush(plot_color));
×
1680
            _intervals.append(left_border);
×
1681

1682
            // Right border
1683
            auto right_border = addRect(_canvasWidth - thickness, 0, thickness, _canvasHeight, border_pen, QBrush(plot_color));
×
1684
            _intervals.append(right_border);
×
1685
        }
×
1686
    }
×
1687
}
16✔
1688

1689
void Media_Window::_plotTensorData() {
16✔
1690

1691
    auto const current_time = _data_manager->getCurrentTime();
16✔
1692

1693
    for (auto const & [key, config]: _tensor_configs) {
16✔
1694
        if (!config.get()->is_visible) continue;
×
1695

1696
        auto tensor_data = _data_manager->getData<TensorData>(key);
×
1697

1698
        auto tensor_shape = tensor_data->getFeatureShape();
×
1699

1700
        auto tensor_slice = tensor_data->getChannelSlice(TimeFrameIndex(current_time), config->display_channel);
×
1701

1702
        // Create a QImage from the tensor data
1703
        QImage tensor_image(static_cast<int>(tensor_shape[1]), static_cast<int>(tensor_shape[0]), QImage::Format::Format_ARGB32);
×
1704
        for (size_t y = 0; y < tensor_shape[0]; ++y) {
×
1705
            for (size_t x = 0; x < tensor_shape[1]; ++x) {
×
1706
                float const value = tensor_slice[y * tensor_shape[1] + x];
×
1707
                //int const pixel_value = static_cast<int>(value * 255);// Assuming the tensor values are normalized between 0 and 1
1708

1709
                // Use the config color with alpha
1710
                QColor const color(QString::fromStdString(config->hex_color));
×
1711
                int const alpha = std::lround(config->alpha * 255.0f * (value > 0 ? 1.0f : 0.0f));
×
1712
                QRgb const rgb = qRgba(color.red(), color.green(), color.blue(), alpha);
×
1713

1714
                tensor_image.setPixel(x, y, rgb);
×
1715
            }
1716
        }
1717

1718
        // Scale the tensor image to the size of the canvas
1719
        QImage const scaled_tensor_image = tensor_image.scaled(_canvasWidth, _canvasHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
×
1720

1721
        auto tensor_pixmap = addPixmap(QPixmap::fromImage(scaled_tensor_image));
×
1722

1723
        _tensors.append(tensor_pixmap);
×
1724
    }
×
1725
}
16✔
1726

1727
std::vector<uint8_t> Media_Window::getDrawingMask() {
×
1728
    // Create a QImage with _canvasWidth and _canvasHeight
1729
    QImage maskImage(_canvasWidth, _canvasHeight, QImage::Format_Grayscale8);
×
1730
    maskImage.fill(0);
×
1731

1732
    QPainter painter(&maskImage);
×
1733
    painter.setPen(Qt::white);
×
1734
    painter.setBrush(QBrush(Qt::white));// Fill the circles with white
×
1735

1736
    for (auto const & point: _drawing_points) {
×
1737
        // Draw a filled circle with the current brush size (hover circle radius)
1738
        float const radius = static_cast<float>(_hover_circle_radius);
×
1739
        painter.drawEllipse(point, radius, radius);
×
1740
    }
1741
    painter.end();
×
1742

1743
    // Scale the QImage to the size of the media
1744
    auto media = _data_manager->getData<MediaData>("media");
×
1745
    int const mediaWidth = media->getWidth();
×
1746
    int const mediaHeight = media->getHeight();
×
1747
    QImage scaledMaskImage = maskImage.scaled(mediaWidth, mediaHeight);
×
1748

1749
    // Convert the QImage to a std::vector<uint8_t>
1750
    std::vector<uint8_t> mask(scaledMaskImage.bits(), scaledMaskImage.bits() + scaledMaskImage.sizeInBytes());
×
1751

1752
    return mask;
×
1753
}
×
1754

1755
void Media_Window::setShowHoverCircle(bool show) {
19✔
1756
    _show_hover_circle = show;
19✔
1757
    if (_show_hover_circle) {
19✔
1758
        if (_debug_performance) {
2✔
1759
            std::cout << "Hover circle enabled" << std::endl;
×
1760
        }
1761

1762
        // Create the hover circle item if it doesn't exist
1763
        if (!_hover_circle_item) {
2✔
1764
            QPen circlePen(Qt::red);
2✔
1765
            circlePen.setWidth(2);
2✔
1766
            _hover_circle_item = addEllipse(0, 0, _hover_circle_radius * 2, _hover_circle_radius * 2, circlePen);
2✔
1767
            _hover_circle_item->setVisible(false);// Initially hidden until mouse moves
2✔
1768
            // DO NOT add to _points vector - hover circle is managed separately
1769
            if (_debug_performance) {
2✔
1770
                std::cout << "  Created new hover circle item" << std::endl;
×
1771
            }
1772
        }
2✔
1773

1774
        // Connect mouse move to efficient hover circle update instead of full canvas update
1775
        connect(this, &Media_Window::mouseMove, this, &Media_Window::_updateHoverCirclePosition);
2✔
1776
    } else {
1777
        if (_debug_performance) {
17✔
1778
            std::cout << "Hover circle disabled" << std::endl;
×
1779
        }
1780

1781
        // Remove the hover circle item
1782
        if (_hover_circle_item) {
17✔
1783
            removeItem(_hover_circle_item);
2✔
1784
            delete _hover_circle_item;
2✔
1785
            _hover_circle_item = nullptr;
2✔
1786
            if (_debug_performance) {
2✔
1787
                std::cout << "  Deleted hover circle item" << std::endl;
×
1788
            }
1789
        }
1790

1791
        // Disconnect the mouse move signal
1792
        disconnect(this, &Media_Window::mouseMove, this, &Media_Window::_updateHoverCirclePosition);
17✔
1793
    }
1794
}
19✔
1795

1796
void Media_Window::setHoverCircleRadius(int radius) {
2✔
1797
    _hover_circle_radius = radius;
2✔
1798

1799
    // Update the existing hover circle item if it exists
1800
    if (_hover_circle_item && _show_hover_circle) {
2✔
1801
        qreal const x = _hover_position.x() - _hover_circle_radius;
2✔
1802
        qreal const y = _hover_position.y() - _hover_circle_radius;
2✔
1803
        _hover_circle_item->setRect(x, y, _hover_circle_radius * 2, _hover_circle_radius * 2);
2✔
1804
    }
1805
}
2✔
1806

1807
void Media_Window::_updateHoverCirclePosition() {
×
1808
    static int call_count = 0;
1809
    call_count++;
×
1810

1811
    if (_hover_circle_item && _show_hover_circle) {
×
1812
        // Update the position of the existing hover circle item
1813
        qreal const x = _hover_position.x() - _hover_circle_radius;
×
1814
        qreal const y = _hover_position.y() - _hover_circle_radius;
×
1815
        _hover_circle_item->setRect(x, y, _hover_circle_radius * 2, _hover_circle_radius * 2);
×
1816
        _hover_circle_item->setVisible(true);
×
1817

1818
        if (_debug_performance) {
×
1819
            std::cout << "Hover circle updated (call #" << call_count << ") at ("
×
1820
                      << _hover_position.x() << ", " << _hover_position.y() << ")" << std::endl;
×
1821
        }
1822
    } else {
×
1823
        if (_debug_performance) {
×
1824
            std::cout << "Hover circle update skipped (call #" << call_count << ") - item: "
×
1825
                      << (_hover_circle_item ? "exists" : "null") << ", show: " << _show_hover_circle << std::endl;
×
1826
        }
1827
    }
1828
}
×
1829

1830
void Media_Window::setShowTemporaryLine(bool show) {
3✔
1831
    _show_temporary_line = show;
3✔
1832
    if (!_show_temporary_line) {
3✔
1833
        clearTemporaryLine();
3✔
1834
    }
1835
}
3✔
1836

NEW
1837
void Media_Window::updateTemporaryLine(std::vector<Point2D<float>> const & points, std::string const & line_key) {
×
NEW
1838
    if (!_show_temporary_line || points.empty()) {
×
NEW
1839
        return;
×
1840
    }
1841

1842
    // Clear existing temporary line
NEW
1843
    clearTemporaryLine();
×
1844

1845
    // Use the same aspect ratio calculation as the target line data
NEW
1846
    auto xAspect = getXAspect();
×
NEW
1847
    auto yAspect = getYAspect();
×
1848
    
1849
    // If we have a line key, use the same image size scaling as that line data
NEW
1850
    if (!line_key.empty()) {
×
NEW
1851
        auto line_data = _data_manager->getData<LineData>(line_key);
×
NEW
1852
        if (line_data) {
×
NEW
1853
            auto image_size = line_data->getImageSize();
×
1854
            
1855
        }
NEW
1856
    }
×
1857

NEW
1858
    if (points.size() < 2) {
×
1859
        // If only one point, just show a marker
1860
        // Convert media coordinates to canvas coordinates
NEW
1861
        float x = points[0].x * xAspect;
×
NEW
1862
        float y = points[0].y * yAspect;
×
1863
        
NEW
1864
        QPen pointPen(Qt::yellow);
×
NEW
1865
        pointPen.setWidth(2);
×
NEW
1866
        QBrush pointBrush(Qt::yellow);
×
1867
        
NEW
1868
        auto pointItem = addEllipse(x - 3, y - 3, 6, 6, pointPen, pointBrush);
×
NEW
1869
        _temporary_line_points.push_back(pointItem);
×
NEW
1870
        return;
×
NEW
1871
    }
×
1872

1873
    // Create path for the line
NEW
1874
    QPainterPath path;
×
1875

1876

1877
    // Move to first point (convert media coordinates to canvas coordinates)
NEW
1878
    path.moveTo(QPointF(points[0].x * xAspect, points[0].y * yAspect));
×
1879

1880
    // Add lines to subsequent points
NEW
1881
    for (size_t i = 1; i < points.size(); ++i) {
×
NEW
1882
        path.lineTo(QPointF(points[i].x * xAspect, points[i].y * yAspect));
×
1883
    }
1884

1885
    // Create the line path item
NEW
1886
    QPen linePen(Qt::yellow);
×
NEW
1887
    linePen.setWidth(2);
×
NEW
1888
    linePen.setStyle(Qt::DashLine); // Dashed line to distinguish from permanent lines
×
1889
    
NEW
1890
    _temporary_line_item = addPath(path, linePen);
×
1891

1892
    // Add point markers
NEW
1893
    QPen pointPen(Qt::yellow);
×
NEW
1894
    pointPen.setWidth(1);
×
NEW
1895
    QBrush pointBrush(Qt::NoBrush); // Open circles
×
1896

NEW
1897
    for (size_t i = 0; i < points.size(); ++i) {
×
1898
        // Convert media coordinates to canvas coordinates
NEW
1899
        float x = points[i].x * xAspect;
×
NEW
1900
        float y = points[i].y * yAspect;
×
1901
        
NEW
1902
        auto pointItem = addEllipse(x - 2.5, y - 2.5, 5, 5, pointPen, pointBrush);
×
NEW
1903
        _temporary_line_points.push_back(pointItem);
×
1904
    }
NEW
1905
}
×
1906

1907
void Media_Window::clearTemporaryLine() {
6✔
1908
    // Remove and delete the temporary line path
1909
    if (_temporary_line_item) {
6✔
NEW
1910
        removeItem(_temporary_line_item);
×
NEW
1911
        delete _temporary_line_item;
×
NEW
1912
        _temporary_line_item = nullptr;
×
1913
    }
1914

1915
    // Remove and delete all temporary line point markers
1916
    for (auto pointItem : _temporary_line_points) {
6✔
NEW
1917
        if (pointItem) {
×
NEW
1918
            removeItem(pointItem);
×
NEW
1919
            delete pointItem;
×
1920
        }
1921
    }
1922
    _temporary_line_points.clear();
6✔
1923
}
6✔
1924

1925
void Media_Window::_addRemoveData() {
1✔
1926
    //New data key was added. This is where we may want to repopulate a custom table
1927
}
1✔
1928

1929
bool Media_Window::_needsTimeFrameConversion(std::shared_ptr<TimeFrame> video_timeframe,
×
1930
                                             std::shared_ptr<TimeFrame> const & interval_timeframe) {
1931
    // If either timeframe is null, no conversion is possible/needed
1932
    if (!video_timeframe || !interval_timeframe) {
×
1933
        return false;
×
1934
    }
1935

1936
    // Conversion is needed if the timeframes are different objects
1937
    return video_timeframe.get() != interval_timeframe.get();
×
1938
}
1939

1940

1941
QRgb plot_color_with_alpha(BaseDisplayOptions const * opts) {
×
1942
    auto color = QColor(QString::fromStdString(opts->hex_color));
×
1943
    auto output_color = qRgba(color.red(), color.green(), color.blue(), std::lround(opts->alpha * 255.0f));
×
1944

1945
    return output_color;
×
1946
}
1947

1948
bool Media_Window::hasPreviewMaskData(std::string const & mask_key) const {
×
1949
    return _mask_preview_active && _preview_mask_data.count(mask_key) > 0;
×
1950
}
1951

1952
std::vector<Mask2D> Media_Window::getPreviewMaskData(std::string const & mask_key) const {
×
1953

1954
    if (hasPreviewMaskData(mask_key)) {
×
1955
        return _preview_mask_data.at(mask_key);
×
1956
    }
1957
    return {};
×
1958
}
1959

1960
void Media_Window::setPreviewMaskData(std::string const & mask_key,
×
1961
                                      std::vector<std::vector<Point2D<uint32_t>>> const & preview_data,
1962
                                      bool active) {
1963
    if (active) {
×
1964
        _preview_mask_data[mask_key] = preview_data;
×
1965
        _mask_preview_active = true;
×
1966
    } else {
1967
        _preview_mask_data.erase(mask_key);
×
1968
        _mask_preview_active = !_preview_mask_data.empty();
×
1969
    }
1970
}
×
1971

1972
void Media_Window::onGroupChanged() {
×
1973
    // Update the canvas when group assignments or properties change
1974
    UpdateCanvas();
×
1975
}
×
1976

1977
QColor Media_Window::_getGroupAwareColor(EntityId entity_id, QColor const & default_color) const {
×
1978
    // Handle selection highlighting first
1979
    if (_selected_entities.count(entity_id) > 0) {
×
1980
        return QColor(255, 255, 0); // Bright yellow for selected entities
×
1981
    }
1982
    
1983
    if (!_group_manager || entity_id == 0) {
×
1984
        return default_color;
×
1985
    }
1986
    
1987
    return _group_manager->getEntityColor(entity_id, default_color);
×
1988
}
1989

1990
QRgb Media_Window::_getGroupAwareColorRgb(EntityId entity_id, QRgb default_color) const {
×
1991
    // Handle selection highlighting first
1992
    if (_selected_entities.count(entity_id) > 0) {
×
1993
        return qRgba(255, 255, 0, 255); // Bright yellow for selected entities
×
1994
    }
1995
    
1996
    if (!_group_manager || entity_id == 0) {
×
1997
        return default_color;
×
1998
    }
1999
    
2000
    QColor group_color = _group_manager->getEntityColor(entity_id, QColor::fromRgba(default_color));
×
2001
    return group_color.rgba();
×
2002
}
2003

2004
// ===== Selection and Context Menu Implementation =====
2005

2006
void Media_Window::clearAllSelections() {
3✔
2007
    if (!_selected_entities.empty()) {
3✔
2008
        _selected_entities.clear();
×
2009
        _selected_data_key.clear();
×
2010
        _selected_data_type.clear();
×
2011
        UpdateCanvas(); // Refresh to remove selection highlights
×
2012
    }
2013
}
3✔
2014

2015
bool Media_Window::hasSelections() const {
×
2016
    return !_selected_entities.empty();
×
2017
}
2018

2019
std::unordered_set<EntityId> Media_Window::getSelectedEntities() const {
×
2020
    return _selected_entities;
×
2021
}
2022

2023
void Media_Window::setGroupSelectionEnabled(bool enabled) {
3✔
2024
    _group_selection_enabled = enabled;
3✔
2025
    if (!enabled) {
3✔
2026
        // Clear any existing selections when disabling group selection
2027
        clearAllSelections();
3✔
2028
    }
2029
}
3✔
2030

2031
bool Media_Window::isGroupSelectionEnabled() const {
×
2032
    return _group_selection_enabled;
×
2033
}
2034

2035
EntityId Media_Window::findPointAtPosition(QPointF const & scene_pos, std::string const & point_key) {
×
2036
    return _findPointAtPosition(scene_pos, point_key);
×
2037
}
2038

2039
EntityId Media_Window::findEntityAtPosition(QPointF const & scene_pos, std::string & data_key, std::string & data_type) {
×
2040
    return _findEntityAtPosition(scene_pos, data_key, data_type);
×
2041
}
2042

2043
void Media_Window::selectEntity(EntityId entity_id, std::string const & data_key, std::string const & data_type) {
×
2044
    _selected_entities.clear();
×
2045
    _selected_entities.insert(entity_id);
×
2046
    _selected_data_key = data_key;
×
2047
    _selected_data_type = data_type;
×
2048
    UpdateCanvas(); // Refresh to show selection
×
2049
}
×
2050

2051
EntityId Media_Window::_findEntityAtPosition(QPointF const & scene_pos, std::string & data_key, std::string & data_type) {
×
2052
    // Convert scene coordinates to media coordinates
2053
    float x_media = static_cast<float>(scene_pos.x() / getXAspect());
×
2054
    float y_media = static_cast<float>(scene_pos.y() / getYAspect());
×
2055

2056
    // Search through lines first (as they're typically most precise)
2057
    for (auto const & [key, config] : _line_configs) {
×
2058
        if (config->is_visible) {
×
2059
            EntityId entity_id = _findLineAtPosition(scene_pos, key);
×
2060
            if (entity_id != 0) {
×
2061
                data_key = key;
×
2062
                data_type = "line";
×
2063
                return entity_id;
×
2064
            }
2065
        }
2066
    }
2067

2068
    // Then search points
2069
    for (auto const & [key, config] : _point_configs) {
×
2070
        if (config->is_visible) {
×
2071
            EntityId entity_id = _findPointAtPosition(scene_pos, key);
×
2072
            if (entity_id != 0) {
×
2073
                data_key = key;
×
2074
                data_type = "point";
×
2075
                return entity_id;
×
2076
            }
2077
        }
2078
    }
2079

2080
    // Finally search masks (usually less precise)
2081
    for (auto const & [key, config] : _mask_configs) {
×
2082
        if (config->is_visible) {
×
2083
            EntityId entity_id = _findMaskAtPosition(scene_pos, key);
×
2084
            if (entity_id != 0) {
×
2085
                data_key = key;
×
2086
                data_type = "mask";
×
2087
                return entity_id;
×
2088
            }
2089
        }
2090
    }
2091

2092
    return 0; // No entity found
×
2093
}
2094

2095
EntityId Media_Window::_findLineAtPosition(QPointF const & scene_pos, std::string const & line_key) {
×
2096
    auto line_data = _data_manager->getData<LineData>(line_key);
×
2097
    if (!line_data) {
×
2098
        return 0;
×
2099
    }
2100

2101
    auto current_time = _data_manager->getCurrentTime();
×
2102
    auto const & lines = line_data->getAtTime(TimeFrameIndex(current_time));
×
2103
    auto const & entity_ids = line_data->getEntityIdsAtTime(TimeFrameIndex(current_time));
×
2104

2105
    if (lines.size() != entity_ids.size()) {
×
2106
        return 0;
×
2107
    }
2108

2109
    float const threshold = 10.0f; // pixels
×
2110

2111
    for (size_t i = 0; i < lines.size(); ++i) {
×
2112
        auto const & line = lines[i];
×
2113
        
2114
        // Check distance from each line segment
2115
        for (size_t j = 0; j < line.size(); ++j) {
×
2116
            if (j + 1 >= line.size()) continue;
×
2117
            
2118
            auto const & p1 = line[j];
×
2119
            auto const & p2 = line[j + 1];
×
2120
            
2121
            // Convert line points to scene coordinates
2122
            float x1_scene = p1.x * getXAspect();
×
2123
            float y1_scene = p1.y * getYAspect();
×
2124
            float x2_scene = p2.x * getXAspect();
×
2125
            float y2_scene = p2.y * getYAspect();
×
2126
            
2127
            // Calculate distance from click point to line segment
2128
            float dist = _calculateDistanceToLineSegment(
×
2129
                scene_pos.x(), scene_pos.y(),
×
2130
                x1_scene, y1_scene, x2_scene, y2_scene
2131
            );
2132
            
2133
            if (dist <= threshold) {
×
2134
                return entity_ids[i];
×
2135
            }
2136
        }
2137
    }
2138

2139
    return 0;
×
2140
}
×
2141

2142
EntityId Media_Window::_findPointAtPosition(QPointF const & scene_pos, std::string const & point_key) {
×
2143
    auto point_data = _data_manager->getData<PointData>(point_key);
×
2144
    if (!point_data) {
×
2145
        return 0;
×
2146
    }
2147

2148
    auto current_time = _data_manager->getCurrentTime();
×
2149
    auto const & points = point_data->getAtTime(TimeFrameIndex(current_time));
×
2150
    auto const & entity_ids = point_data->getEntityIdsAtTime(TimeFrameIndex(current_time));
×
2151

2152
    if (points.size() != entity_ids.size()) {
×
2153
        return 0;
×
2154
    }
2155

2156
    float const threshold = 15.0f; // pixels
×
2157

2158
    for (size_t i = 0; i < points.size(); ++i) {
×
2159
        auto const & point = points[i];
×
2160
        
2161
        // Convert point to scene coordinates
2162
        float x_scene = point.x * getXAspect();
×
2163
        float y_scene = point.y * getYAspect();
×
2164
        
2165
        // Calculate distance
2166
        float dx = scene_pos.x() - x_scene;
×
2167
        float dy = scene_pos.y() - y_scene;
×
2168
        float distance = std::sqrt(dx * dx + dy * dy);
×
2169
        
2170
        if (distance <= threshold) {
×
2171
            return entity_ids[i];
×
2172
        }
2173
    }
2174

2175
    return 0;
×
2176
}
×
2177

2178
EntityId Media_Window::_findMaskAtPosition(QPointF const & scene_pos, std::string const & mask_key) {
×
2179
    auto mask_data = _data_manager->getData<MaskData>(mask_key);
×
2180
    if (!mask_data) {
×
2181
        return 0;
×
2182
    }
2183

2184
    auto current_time = _data_manager->getCurrentTime();
×
2185
    auto const & masks = mask_data->getAtTime(TimeFrameIndex(current_time));
×
2186

2187
    // MaskData doesn't currently support EntityIds, so we'll use position-based indices for now
2188
    // This is a simplified implementation that can be improved when MaskData gets EntityId support
2189

2190
    float x_media = static_cast<float>(scene_pos.x() / getXAspect());
×
2191
    float y_media = static_cast<float>(scene_pos.y() / getYAspect());
×
2192

2193
    for (size_t i = 0; i < masks.size(); ++i) {
×
2194
        auto const & mask = masks[i];
×
2195
        
2196
        // Check if the point is inside any of the mask's polygons
2197
        for (auto const & point : mask) {
×
2198
            // Simple bounding box check for now (could be improved with proper point-in-polygon)
2199
            if (std::abs(static_cast<float>(point.x) - x_media) < 5.0f && 
×
2200
                std::abs(static_cast<float>(point.y) - y_media) < 5.0f) {
×
2201
                // Return a synthetic EntityId based on position and mask index
2202
                // This is temporary until MaskData supports proper EntityIds
2203
                return static_cast<EntityId>(1000000 + current_time * 1000 + i);
×
2204
            }
2205
        }
2206
    }
2207

2208
    return 0;
×
2209
}
×
2210

2211
void Media_Window::_createContextMenu() {
3✔
2212
    _context_menu = new QMenu();
3✔
2213
    
2214
    // Create actions
2215
    auto * create_group_action = new QAction("Create New Group", this);
3✔
2216
    auto * ungroup_action = new QAction("Ungroup Selected", this);
3✔
2217
    auto * clear_selection_action = new QAction("Clear Selection", this);
3✔
2218
    
2219
    // Add actions to menu
2220
    _context_menu->addAction(create_group_action);
3✔
2221
    _context_menu->addSeparator();
3✔
2222
    _context_menu->addAction(ungroup_action);
3✔
2223
    _context_menu->addSeparator();
3✔
2224
    _context_menu->addAction(clear_selection_action);
3✔
2225
    
2226
    // Connect actions
2227
    connect(create_group_action, &QAction::triggered, this, &Media_Window::onCreateNewGroup);
3✔
2228
    connect(ungroup_action, &QAction::triggered, this, &Media_Window::onUngroupSelected);
3✔
2229
    connect(clear_selection_action, &QAction::triggered, this, &Media_Window::onClearSelection);
3✔
2230
}
3✔
2231

2232
void Media_Window::_showContextMenu(QPoint const & global_pos) {
×
2233
    if (_context_menu) {
×
2234
        _context_menu->popup(global_pos);
×
2235
    }
2236
}
×
2237

2238
void Media_Window::_updateContextMenuActions() {
×
2239
    if (!_context_menu || !_group_manager) {
×
2240
        return;
×
2241
    }
2242

2243
    // Clear all dynamic actions by removing actions after the static ones
2244
    // The static menu structure is: Create New Group, Separator, Ungroup Selected, Separator, Clear Selection
2245
    // Everything after the second separator should be removed
2246
    auto actions = _context_menu->actions();
×
2247
    int separator_count = 0;
×
2248
    QList<QAction*> actions_to_remove;
×
2249
    
2250
    for (QAction* action : actions) {
×
2251
        if (action->isSeparator()) {
×
2252
            separator_count++;
×
2253
            if (separator_count > 2) {
×
2254
                actions_to_remove.append(action);
×
2255
            }
2256
        } else if (separator_count >= 2) {
×
2257
            // This is a dynamic action after the second separator
2258
            actions_to_remove.append(action);
×
2259
        }
2260
    }
2261
    
2262
    // Remove and delete the dynamic actions
2263
    for (QAction* action : actions_to_remove) {
×
2264
        _context_menu->removeAction(action);
×
2265
        action->deleteLater(); // Use deleteLater() for safer cleanup
×
2266
    }
2267

2268
    // Add dynamic group assignment actions
2269
    auto groups = _group_manager->getGroupsForContextMenu();
×
2270
    if (!groups.empty()) {
×
2271
        _context_menu->addSeparator();
×
2272
        
2273
        for (auto const & [group_id, group_name] : groups) {
×
2274
            auto * assign_action = new QAction(QString("Assign to %1").arg(group_name), this);
×
2275
            _context_menu->addAction(assign_action);
×
2276
            
2277
            connect(assign_action, &QAction::triggered, this, [this, group_id]() {
×
2278
                onAssignToGroup(group_id);
×
2279
            });
×
2280
        }
2281
    }
2282
}
×
2283

2284
float Media_Window::_calculateDistanceToLineSegment(float px, float py, float x1, float y1, float x2, float y2) {
×
2285
    float dx = x2 - x1;
×
2286
    float dy = y2 - y1;
×
2287
    
2288
    if (dx == 0 && dy == 0) {
×
2289
        // Point to point distance
2290
        float dpx = px - x1;
×
2291
        float dpy = py - y1;
×
2292
        return std::sqrt(dpx * dpx + dpy * dpy);
×
2293
    }
2294
    
2295
    float t = ((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy);
×
2296
    t = std::max(0.0f, std::min(1.0f, t));
×
2297
    
2298
    float projection_x = x1 + t * dx;
×
2299
    float projection_y = y1 + t * dy;
×
2300
    
2301
    float dist_x = px - projection_x;
×
2302
    float dist_y = py - projection_y;
×
2303
    
2304
    return std::sqrt(dist_x * dist_x + dist_y * dist_y);
×
2305
}
2306

2307
// Context menu slot implementations
2308
void Media_Window::onCreateNewGroup() {
×
2309
    if (!_group_manager || _selected_entities.empty()) {
×
2310
        return;
×
2311
    }
2312
    
2313
    int group_id = _group_manager->createGroupWithEntities(_selected_entities);
×
2314
    if (group_id != -1) {
×
2315
        clearAllSelections();
×
2316
    }
2317
}
2318

2319
void Media_Window::onAssignToGroup(int group_id) {
×
2320
    if (!_group_manager || _selected_entities.empty()) {
×
2321
        return;
×
2322
    }
2323
    
2324
    _group_manager->assignEntitiesToGroup(group_id, _selected_entities);
×
2325
    clearAllSelections();
×
2326
}
2327

2328
void Media_Window::onUngroupSelected() {
×
2329
    if (!_group_manager || _selected_entities.empty()) {
×
2330
        return;
×
2331
    }
2332
    
2333
    _group_manager->ungroupEntities(_selected_entities);
×
2334
    clearAllSelections();
×
2335
}
2336

2337
void Media_Window::onClearSelection() {
×
2338
    clearAllSelections();
×
2339
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc