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

paulmthompson / WhiskerToolbox / 18762825348

23 Oct 2025 09:42PM UTC coverage: 72.822% (+0.3%) from 72.522%
18762825348

push

github

paulmthompson
add boolean digital interval logic test

693 of 711 new or added lines in 5 files covered. (97.47%)

718 existing lines in 10 files now uncovered.

54997 of 75522 relevant lines covered (72.82%)

45740.9 hits per line

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

44.21
/src/WhiskerToolbox/DataViewer_Widget/DataViewer_Widget.cpp
1
#include "DataViewer_Widget.hpp"
2
#include "ui_DataViewer_Widget.h"
3

4
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
5
#include "AnalogTimeSeries/utils/statistics.hpp"
6
#include "DataManager.hpp"
7
#include "DigitalTimeSeries/Digital_Event_Series.hpp"
8
#include "DigitalTimeSeries/Digital_Interval_Series.hpp"
9

10
#include "DataViewer/AnalogTimeSeries/AnalogTimeSeriesDisplayOptions.hpp"
11
#include "DataViewer/AnalogTimeSeries/MVP_AnalogTimeSeries.hpp"
12
#include "DataViewer/DigitalEvent/DigitalEventSeriesDisplayOptions.hpp"
13
#include "DataViewer/DigitalEvent/MVP_DigitalEvent.hpp"
14
#include "DataViewer/DigitalInterval/DigitalIntervalSeriesDisplayOptions.hpp"
15
#include "DataViewer/DigitalInterval/MVP_DigitalInterval.hpp"
16
#include "Feature_Tree_Model.hpp"
17
#include "Feature_Tree_Widget/Feature_Tree_Widget.hpp"
18
#include "OpenGLWidget.hpp"
19
#include "TimeFrame/TimeFrame.hpp"
20
#include "TimeScrollBar/TimeScrollBar.hpp"
21

22
#include "AnalogTimeSeries/AnalogViewer_Widget.hpp"
23
#include "DigitalEvent/EventViewer_Widget.hpp"
24
#include "DigitalInterval/IntervalViewer_Widget.hpp"
25

26
#include <QMetaObject>
27
#include <QPointer>
28
#include <QTableWidget>
29
#include <QWheelEvent>
30
#include <QMenu>
31
#include <QFileDialog>
32
#include <QFile>
33

34
#include <algorithm>
35
#include <cmath>
36
#include <iostream>
37
#include <sstream>
38

39
DataViewer_Widget::DataViewer_Widget(std::shared_ptr<DataManager> data_manager,
20✔
40
                                     TimeScrollBar * time_scrollbar,
41
                                     MainWindow * main_window,
42
                                     QWidget * parent)
20✔
43
    : QWidget(parent),
44
      _data_manager{std::move(data_manager)},
20✔
45
      _time_scrollbar{time_scrollbar},
20✔
46
      _main_window{main_window},
20✔
47
      ui(new Ui::DataViewer_Widget) {
40✔
48

49
    ui->setupUi(this);
20✔
50

51
    // Initialize plotting manager with default viewport
52
    _plotting_manager = std::make_unique<PlottingManager>();
20✔
53

54
    // Provide PlottingManager reference to OpenGL widget
55
    ui->openGLWidget->setPlottingManager(_plotting_manager.get());
20✔
56

57
    // Initialize feature tree model
58
    _feature_tree_model = std::make_unique<Feature_Tree_Model>(this);
20✔
59
    _feature_tree_model->setDataManager(_data_manager);
20✔
60

61
    // Set up observer to automatically clean up data when it's deleted from DataManager
62
    // Queue the cleanup to the Qt event loop to avoid running during mid-update mutations
63
    _data_manager->addObserver([this]() {
20✔
64
        QPointer<DataViewer_Widget> self = this;
15✔
65
        QMetaObject::invokeMethod(self, [self]() {
15✔
66
            if (!self) return;
15✔
67
            self->cleanupDeletedData(); }, Qt::QueuedConnection);
15✔
68
    });
30✔
69

70
    // Configure Feature_Tree_Widget
71
    ui->feature_tree_widget->setTypeFilters({DM_DataType::Analog, DM_DataType::DigitalEvent, DM_DataType::DigitalInterval});
60✔
72
    ui->feature_tree_widget->setDataManager(_data_manager);
20✔
73

74
    // Connect Feature_Tree_Widget signals using the new interface
75
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::featureSelected, this, [this](std::string const & feature) {
20✔
76
        _handleFeatureSelected(QString::fromStdString(feature));
×
77
    });
×
78

79
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::addFeature, this, [this](std::string const & feature) {
20✔
80
        std::cout << "Adding single feature: " << feature << std::endl;
×
81
        _addFeatureToModel(QString::fromStdString(feature), true);
×
82
    });
×
83

84
    // Install context menu handling on the embedded tree widget
85
    ui->feature_tree_widget->treeWidget()->setContextMenuPolicy(Qt::CustomContextMenu);
20✔
86
    connect(ui->feature_tree_widget->treeWidget(), &QTreeWidget::customContextMenuRequested, this, [this](QPoint const & pos) {
20✔
87
        auto * tw = ui->feature_tree_widget->treeWidget();
×
88
        QTreeWidgetItem * item = tw->itemAt(pos);
×
89
        if (!item) return;
×
90
        std::string const key = item->text(0).toStdString();
×
91
        // Group items have children and are under Analog data type parent
92
        bool const hasChildren = item->childCount() > 0;
×
93
        if (hasChildren) {
×
94
            // Determine if parent is Analog data type or children are analog keys
95
            bool isAnalogGroup = false;
×
96
            if (auto * parent = item->parent()) {
×
97
                if (parent->text(0) == QString::fromStdString(convert_data_type_to_string(DM_DataType::Analog))) {
×
98
                    isAnalogGroup = true;
×
99
                }
100
            }
101
            if (isAnalogGroup) {
×
102
                QPoint const global_pos = tw->viewport()->mapToGlobal(pos);
×
103
                _showGroupContextMenu(key, global_pos);
×
104
            }
105
        }
106
    });
×
107

108
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::removeFeature, this, [this](std::string const & feature) {
20✔
109
        std::cout << "Removing single feature: " << feature << std::endl;
×
110
        _addFeatureToModel(QString::fromStdString(feature), false);
×
111
    });
×
112

113
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::addFeatures, this, [this](std::vector<std::string> const & features) {
20✔
114
        std::cout << "Adding " << features.size() << " features as group" << std::endl;
1✔
115

116
        // Mark batch add to suppress per-series auto-arrange
117
        _is_batch_add = true;
1✔
118
        // Process all features in the group without triggering individual canvas updates
119
        for (auto const & key: features) {
6✔
120
            _plotSelectedFeatureWithoutUpdate(key);
5✔
121
        }
122
        _is_batch_add = false;
1✔
123

124
        // Auto-arrange and auto-fill once after batch
125
        if (!features.empty()) {
1✔
126
            std::cout << "Auto-arranging and filling canvas for group toggle" << std::endl;
1✔
127
            autoArrangeVerticalSpacing();// includes auto-fill functionality
1✔
128
        }
129

130
        // Trigger a single canvas update at the end
131
        if (!features.empty()) {
1✔
132
            std::cout << "Triggering single canvas update for group toggle" << std::endl;
1✔
133
            ui->openGLWidget->updateCanvas();
1✔
134
        }
135
    });
1✔
136

137
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::removeFeatures, this, [this](std::vector<std::string> const & features) {
20✔
138
        std::cout << "Removing " << features.size() << " features as group" << std::endl;
×
139

140
        _is_batch_add = true;
×
141
        // Process all features in the group without triggering individual canvas updates
142
        for (auto const & key: features) {
×
143
            _removeSelectedFeatureWithoutUpdate(key);
×
144
        }
145
        _is_batch_add = false;
×
146

147
        // Auto-arrange and auto-fill once after batch
148
        if (!features.empty()) {
×
149
            std::cout << "Auto-arranging and filling canvas for group toggle" << std::endl;
×
150
            autoArrangeVerticalSpacing();// includes auto-fill functionality
×
151
        }
152

153
        // Trigger a single canvas update at the end
154
        if (!features.empty()) {
×
155
            std::cout << "Triggering single canvas update for group toggle" << std::endl;
×
156
            ui->openGLWidget->updateCanvas();
×
157
        }
158
    });
×
159

160
    // Connect color change signals from the model
161
    connect(_feature_tree_model.get(), &Feature_Tree_Model::featureColorChanged, this, &DataViewer_Widget::_handleColorChanged);
20✔
162

163
    // Connect color change signals from the tree widget to the model
164
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::colorChangeFeatures, this, [this](std::vector<std::string> const & features, std::string const & hex_color) {
20✔
165
        for (auto const & feature: features) {
×
166
            _feature_tree_model->setFeatureColor(feature, hex_color);
×
167
        }
168
    });
×
169

170
    connect(ui->x_axis_samples, QOverload<int>::of(&QSpinBox::valueChanged), this, &DataViewer_Widget::_handleXAxisSamplesChanged);
20✔
171

172
    connect(ui->global_zoom, &QDoubleSpinBox::valueChanged, this, &DataViewer_Widget::_updateGlobalScale);
20✔
173

174
    connect(ui->theme_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DataViewer_Widget::_handleThemeChanged);
20✔
175

176
    connect(time_scrollbar, &TimeScrollBar::timeChanged, this, &DataViewer_Widget::_updatePlot);
20✔
177

178
    //We should alwasy get the master clock because we plot
179
    // Check for master clock
180
    auto time_keys = _data_manager->getTimeFrameKeys();
20✔
181
    // if timekeys doesn't have master, we should throw an error
182
    if (std::find(time_keys.begin(), time_keys.end(), TimeKey("master")) == time_keys.end()) {
20✔
183
        std::cout << "No master clock found in DataManager" << std::endl;
20✔
184
        _time_frame = _data_manager->getTime(TimeKey("time"));
20✔
185
    } else {
186
        _time_frame = _data_manager->getTime(TimeKey("master"));
×
187
    }
188

189
    std::cout << "Setting GL limit to " << _time_frame->getTotalFrameCount() << std::endl;
20✔
190
    ui->openGLWidget->setXLimit(_time_frame->getTotalFrameCount());
20✔
191

192
    // Set the master time frame for proper coordinate conversion
193
    ui->openGLWidget->setMasterTimeFrame(_time_frame);
20✔
194

195
    // Set spinbox maximum to the actual data range (not the hardcoded UI limit)
196
    int const data_range = static_cast<int>(_time_frame->getTotalFrameCount());
20✔
197
    std::cout << "Setting x_axis_samples maximum to " << data_range << std::endl;
20✔
198
    ui->x_axis_samples->setMaximum(data_range);
20✔
199

200
    // Setup stacked widget with data-type specific viewers
201
    auto analog_widget = new AnalogViewer_Widget(_data_manager, ui->openGLWidget);
20✔
202
    auto interval_widget = new IntervalViewer_Widget(_data_manager, ui->openGLWidget);
20✔
203
    auto event_widget = new EventViewer_Widget(_data_manager, ui->openGLWidget);
20✔
204

205
    ui->stackedWidget->addWidget(analog_widget);
20✔
206
    ui->stackedWidget->addWidget(interval_widget);
20✔
207
    ui->stackedWidget->addWidget(event_widget);
20✔
208

209
    // Connect color change signals from sub-widgets
210
    connect(analog_widget, &AnalogViewer_Widget::colorChanged,
60✔
211
            this, &DataViewer_Widget::_handleColorChanged);
40✔
212
    connect(interval_widget, &IntervalViewer_Widget::colorChanged,
60✔
213
            this, &DataViewer_Widget::_handleColorChanged);
40✔
214
    connect(event_widget, &EventViewer_Widget::colorChanged,
60✔
215
            this, &DataViewer_Widget::_handleColorChanged);
40✔
216

217
    // Connect mouse hover signal from OpenGL widget
218
    connect(ui->openGLWidget, &OpenGLWidget::mouseHover,
60✔
219
            this, &DataViewer_Widget::_updateCoordinateDisplay);
40✔
220

221
    // Grid line connections
222
    connect(ui->grid_lines_enabled, &QCheckBox::toggled, this, &DataViewer_Widget::_handleGridLinesToggled);
20✔
223
    connect(ui->grid_spacing, QOverload<int>::of(&QSpinBox::valueChanged), this, &DataViewer_Widget::_handleGridSpacingChanged);
20✔
224

225
    // Vertical spacing connection
226
    connect(ui->vertical_spacing, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &DataViewer_Widget::_handleVerticalSpacingChanged);
20✔
227

228
    // Auto-arrange button connection
229
    connect(ui->auto_arrange_button, &QPushButton::clicked, this, &DataViewer_Widget::autoArrangeVerticalSpacing);
20✔
230

231
    // Initialize grid line UI to match OpenGLWidget defaults
232
    ui->grid_lines_enabled->setChecked(ui->openGLWidget->getGridLinesEnabled());
20✔
233
    ui->grid_spacing->setValue(ui->openGLWidget->getGridSpacing());
20✔
234

235
    // Initialize vertical spacing UI to match OpenGLWidget defaults
236
    ui->vertical_spacing->setValue(static_cast<double>(ui->openGLWidget->getVerticalSpacing()));
20✔
237

238
    // Configure splitter behavior
239
    ui->main_splitter->setStretchFactor(0, 0);  // Properties panel doesn't stretch
20✔
240
    ui->main_splitter->setStretchFactor(1, 1);  // Plot area stretches
20✔
241
    
242
    // Set initial sizes: properties panel gets enough space for controls, plot area gets the rest
243
    ui->main_splitter->setSizes({320, 1000});
20✔
244
    
245
    // Prevent plot area from collapsing, but allow properties panel to collapse
246
    ui->main_splitter->setCollapsible(0, true);  // Properties panel can collapse
20✔
247
    ui->main_splitter->setCollapsible(1, false); // Plot area cannot collapse
20✔
248
    
249
    // Connect hide button (on properties panel)
250
    connect(ui->hide_properties_button, &QPushButton::clicked, this, [this]() {
20✔
251
        _hidePropertiesPanel();
×
252
    });
×
253
    
254
    // Connect show button (on plot side) - initially hidden
255
    connect(ui->show_properties_button, &QPushButton::clicked, this, [this]() {
20✔
256
        _showPropertiesPanel();
×
257
    });
×
258
    
259
    // Initially hide the show button since properties are visible
260
    ui->show_properties_button->hide();
20✔
261
}
40✔
262

263
DataViewer_Widget::~DataViewer_Widget() {
39✔
264
    delete ui;
20✔
265
}
39✔
266

267
void DataViewer_Widget::openWidget() {
18✔
268
    std::cout << "DataViewer Widget Opened" << std::endl;
18✔
269

270
    // Tree is already populated by observer pattern in setDataManager()
271
    // Trigger refresh in case of manual opening
272
    ui->feature_tree_widget->refreshTree();
18✔
273

274
    this->show();
18✔
275
    _updateLabels();
18✔
276
}
18✔
277

278
void DataViewer_Widget::closeEvent(QCloseEvent * event) {
×
279
    static_cast<void>(event);
280

281
    std::cout << "Close event detected" << std::endl;
×
282
}
×
283

284
void DataViewer_Widget::resizeEvent(QResizeEvent * event) {
18✔
285
    QWidget::resizeEvent(event);
18✔
286

287
    // Update plotting manager dimensions when widget is resized
288
    _updatePlottingManagerDimensions();
18✔
289

290
    // The OpenGL widget will automatically get its resizeGL called by Qt
291
    // but we can trigger an additional update if needed
292
    if (ui->openGLWidget) {
18✔
293
        ui->openGLWidget->update();
18✔
294
    }
295
}
18✔
296

297
void DataViewer_Widget::_updatePlot(int time) {
1✔
298
    //std::cout << "Time is " << time;
299
    time = _data_manager->getTime(TimeKey("time"))->getTimeAtIndex(TimeFrameIndex(time));
1✔
300
    //std::cout << ""
301
    ui->openGLWidget->updateCanvas(time);
1✔
302

303
    _updateLabels();
1✔
304
}
1✔
305

306

307
void DataViewer_Widget::_addFeatureToModel(QString const & feature, bool enabled) {
28✔
308
    std::cout << "Feature toggle signal received: " << feature.toStdString() << " enabled: " << enabled << std::endl;
28✔
309

310
    if (enabled) {
28✔
311
        _plotSelectedFeature(feature.toStdString());
28✔
312
    } else {
313
        _removeSelectedFeature(feature.toStdString());
×
314
    }
315
}
28✔
316

317
void DataViewer_Widget::_plotSelectedFeature(std::string const & key) {
28✔
318
    std::cout << "Attempting to plot feature: " << key << std::endl;
28✔
319

320
    if (key.empty()) {
28✔
321
        std::cerr << "Error: empty key in _plotSelectedFeature" << std::endl;
×
322
        return;
×
323
    }
324

325
    if (!_data_manager) {
28✔
326
        std::cerr << "Error: null data manager in _plotSelectedFeature" << std::endl;
×
327
        return;
×
328
    }
329

330
    // Get color from model
331
    std::string color = _feature_tree_model->getFeatureColor(key);
28✔
332
    std::cout << "Using color: " << color << " for series: " << key << std::endl;
28✔
333

334
    auto data_type = _data_manager->getType(key);
28✔
335
    std::cout << "Feature type: " << convert_data_type_to_string(data_type) << std::endl;
28✔
336

337
    // Register with plotting manager for coordinated positioning
338
    if (_plotting_manager) {
28✔
339
        std::cout << "Registering series with plotting manager: " << key << std::endl;
28✔
340
    }
341

342
    if (data_type == DM_DataType::Analog) {
28✔
343

344
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
17✔
345
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
17✔
346
        if (!series) {
17✔
347
            std::cerr << "Error: failed to get AnalogTimeSeries for key: " << key << std::endl;
×
348
            return;
×
349
        }
350

351

352
        auto time_key = _data_manager->getTimeKey(key);
17✔
353
        std::cout << "Time frame key: " << time_key << std::endl;
17✔
354
        auto time_frame = _data_manager->getTime(time_key);
17✔
355
        if (!time_frame) {
17✔
356
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
357
            return;
×
358
        }
359

360
        std::cout << "Time frame has " << time_frame->getTotalFrameCount() << " frames" << std::endl;
17✔
361

362
        // Add to plotting manager first
363
        _plotting_manager->addAnalogSeries(key, series, time_frame, color);
17✔
364

365
        ui->openGLWidget->addAnalogTimeSeries(key, series, time_frame, color);
17✔
366
        std::cout << "Successfully added analog series to PlottingManager and OpenGL widget" << std::endl;
17✔
367

368
    } else if (data_type == DM_DataType::DigitalEvent) {
28✔
369

370
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
11✔
371
        auto series = _data_manager->getData<DigitalEventSeries>(key);
11✔
372
        if (!series) {
11✔
373
            std::cerr << "Error: failed to get DigitalEventSeries for key: " << key << std::endl;
×
374
            return;
×
375
        }
376

377
        auto time_key = _data_manager->getTimeKey(key);
11✔
378
        auto time_frame = _data_manager->getTime(time_key);
11✔
379
        if (!time_frame) {
11✔
380
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
381
            return;
×
382
        }
383

384
        // Add to plotting manager first
385
        _plotting_manager->addDigitalEventSeries(key, series, time_frame, color);
11✔
386

387
        ui->openGLWidget->addDigitalEventSeries(key, series, time_frame, color);
11✔
388

389
    } else if (data_type == DM_DataType::DigitalInterval) {
11✔
390

391
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
×
392
        auto series = _data_manager->getData<DigitalIntervalSeries>(key);
×
393
        if (!series) {
×
394
            std::cerr << "Error: failed to get DigitalIntervalSeries for key: " << key << std::endl;
×
395
            return;
×
396
        }
397

398
        auto time_key = _data_manager->getTimeKey(key);
×
399
        auto time_frame = _data_manager->getTime(time_key);
×
400
        if (!time_frame) {
×
401
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
402
            return;
×
403
        }
404

405
        // Add to plotting manager first
406
        _plotting_manager->addDigitalIntervalSeries(key, series, time_frame, color);
×
407

408
        ui->openGLWidget->addDigitalIntervalSeries(key, series, time_frame, color);
×
409

410
    } else {
×
411
        std::cout << "Feature type not supported: " << convert_data_type_to_string(data_type) << std::endl;
×
412
        return;
×
413
    }
414

415
    // Apply coordinated plotting manager allocation after adding to OpenGL widget
416
    if (_plotting_manager) {
28✔
417
        _applyPlottingManagerAllocation(key);
28✔
418
    }
419

420
    // Auto-arrange and auto-fill canvas to make optimal use of space
421
    if (!_is_batch_add) {
28✔
422
        std::cout << "Auto-arranging and filling canvas after adding series" << std::endl;
28✔
423
        autoArrangeVerticalSpacing();// This now includes auto-fill functionality
28✔
424
    }
425

426
    std::cout << "Series addition and auto-arrangement completed" << std::endl;
28✔
427
    // Trigger canvas update to show the new series
428
    if (!_is_batch_add) {
28✔
429
        std::cout << "Triggering canvas update" << std::endl;
28✔
430
        ui->openGLWidget->updateCanvas();
28✔
431
    }
432
    std::cout << "Canvas update completed" << std::endl;
28✔
433
}
28✔
434

435
void DataViewer_Widget::_removeSelectedFeature(std::string const & key) {
×
436
    std::cout << "Attempting to remove feature: " << key << std::endl;
×
437

438
    if (key.empty()) {
×
439
        std::cerr << "Error: empty key in _removeSelectedFeature" << std::endl;
×
440
        return;
×
441
    }
442

443
    if (!_data_manager) {
×
444
        std::cerr << "Error: null data manager in _removeSelectedFeature" << std::endl;
×
445
        return;
×
446
    }
447

448
    auto data_type = _data_manager->getType(key);
×
449

450
    // Remove from plotting manager first
451
    if (_plotting_manager) {
×
452
        bool removed = false;
×
453
        if (data_type == DM_DataType::Analog) {
×
454
            removed = _plotting_manager->removeAnalogSeries(key);
×
455
        } else if (data_type == DM_DataType::DigitalEvent) {
×
456
            removed = _plotting_manager->removeDigitalEventSeries(key);
×
457
        } else if (data_type == DM_DataType::DigitalInterval) {
×
458
            removed = _plotting_manager->removeDigitalIntervalSeries(key);
×
459
        }
460
        if (removed) {
×
461
            std::cout << "Unregistered '" << key << "' from plotting manager" << std::endl;
×
462
        }
463
    }
464

465
    if (data_type == DM_DataType::Analog) {
×
466
        ui->openGLWidget->removeAnalogTimeSeries(key);
×
467
    } else if (data_type == DM_DataType::DigitalEvent) {
×
468
        ui->openGLWidget->removeDigitalEventSeries(key);
×
469
    } else if (data_type == DM_DataType::DigitalInterval) {
×
470
        ui->openGLWidget->removeDigitalIntervalSeries(key);
×
471
    } else {
472
        std::cout << "Feature type not supported for removal: " << convert_data_type_to_string(data_type) << std::endl;
×
473
        return;
×
474
    }
475

476
    // Auto-arrange and auto-fill canvas to rescale remaining elements
477
    std::cout << "Auto-arranging and filling canvas after removing series" << std::endl;
×
478
    autoArrangeVerticalSpacing();// This now includes auto-fill functionality
×
479

480
    std::cout << "Series removal and auto-arrangement completed" << std::endl;
×
481
    // Trigger canvas update to reflect the removal
482
    std::cout << "Triggering canvas update after removal" << std::endl;
×
483
    ui->openGLWidget->updateCanvas();
×
484
}
485

486
void DataViewer_Widget::_handleFeatureSelected(QString const & feature) {
×
487
    std::cout << "Feature selected signal received: " << feature.toStdString() << std::endl;
×
488

489
    if (feature.isEmpty()) {
×
490
        std::cerr << "Error: empty feature name in _handleFeatureSelected" << std::endl;
×
491
        return;
×
492
    }
493

494
    if (!_data_manager) {
×
495
        std::cerr << "Error: null data manager in _handleFeatureSelected" << std::endl;
×
496
        return;
×
497
    }
498

499
    _highlighted_available_feature = feature;
×
500

501
    // Switch stacked widget based on data type
502
    auto const type = _data_manager->getType(feature.toStdString());
×
503
    auto key = feature.toStdString();
×
504

505
    std::cout << "Feature type for selection: " << convert_data_type_to_string(type) << std::endl;
×
506

507
    if (type == DM_DataType::Analog) {
×
508
        int const stacked_widget_index = 1;// Analog widget is at index 1 (after empty page)
×
509
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
510
        auto analog_widget = dynamic_cast<AnalogViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
511
        if (analog_widget) {
×
512
            analog_widget->setActiveKey(key);
×
513
            std::cout << "Selected Analog Time Series: " << key << std::endl;
×
514
        } else {
515
            std::cerr << "Error: failed to cast to AnalogViewer_Widget" << std::endl;
×
516
        }
517

518
    } else if (type == DM_DataType::DigitalInterval) {
×
519
        int const stacked_widget_index = 2;// Interval widget is at index 2
×
520
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
521
        auto interval_widget = dynamic_cast<IntervalViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
522
        if (interval_widget) {
×
523
            interval_widget->setActiveKey(key);
×
524
            std::cout << "Selected Digital Interval Series: " << key << std::endl;
×
525
        } else {
526
            std::cerr << "Error: failed to cast to IntervalViewer_Widget" << std::endl;
×
527
        }
528

529
    } else if (type == DM_DataType::DigitalEvent) {
×
530
        int const stacked_widget_index = 3;// Event widget is at index 3
×
531
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
532
        auto event_widget = dynamic_cast<EventViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
533
        if (event_widget) {
×
534
            event_widget->setActiveKey(key);
×
535
            std::cout << "Selected Digital Event Series: " << key << std::endl;
×
536
        } else {
537
            std::cerr << "Error: failed to cast to EventViewer_Widget" << std::endl;
×
538
        }
539

540
    } else {
541
        // No specific widget for this type, don't change the current index
542
        std::cout << "Unsupported feature type for detailed view: " << convert_data_type_to_string(type) << std::endl;
×
543
    }
544
}
×
545

546
void DataViewer_Widget::_handleXAxisSamplesChanged(int value) {
2✔
547
    // Use setRangeWidth for spinbox changes (absolute value)
548
    std::cout << "Spinbox requested range width: " << value << std::endl;
2✔
549
    int64_t const actual_range = ui->openGLWidget->setRangeWidth(static_cast<int64_t>(value));
2✔
550
    std::cout << "Actual range width achieved: " << actual_range << std::endl;
2✔
551

552
    // Update the spinbox with the actual range width achieved (in case it was clamped)
553
    if (actual_range != value) {
2✔
554
        std::cout << "Range was clamped, updating spinbox to: " << actual_range << std::endl;
1✔
555
        updateXAxisSamples(static_cast<int>(actual_range));
1✔
556
    }
557
}
2✔
558

559
void DataViewer_Widget::updateXAxisSamples(int value) {
1✔
560
    ui->x_axis_samples->blockSignals(true);
1✔
561
    ui->x_axis_samples->setValue(value);
1✔
562
    ui->x_axis_samples->blockSignals(false);
1✔
563
}
1✔
564

565
void DataViewer_Widget::_updateGlobalScale(double scale) {
23✔
566
    ui->openGLWidget->setGlobalScale(static_cast<float>(scale));
23✔
567

568
    // Also update PlottingManager zoom factor
569
    if (_plotting_manager) {
23✔
570
        _plotting_manager->setGlobalZoom(static_cast<float>(scale));
23✔
571

572
        // Apply updated positions to all registered series
573
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
23✔
574
        for (auto const & key: analog_keys) {
76✔
575
            _applyPlottingManagerAllocation(key);
53✔
576
        }
577
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
23✔
578
        for (auto const & key: event_keys) {
29✔
579
            _applyPlottingManagerAllocation(key);
6✔
580
        }
581
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
23✔
582
        for (auto const & key: interval_keys) {
23✔
583
            _applyPlottingManagerAllocation(key);
×
584
        }
585

586
        // Trigger canvas update
587
        ui->openGLWidget->updateCanvas();
23✔
588
    }
23✔
589
}
23✔
590

591
void DataViewer_Widget::wheelEvent(QWheelEvent * event) {
×
592
    // Disable zooming while dragging intervals
593
    if (ui->openGLWidget->isDraggingInterval()) {
×
594
        return;
×
595
    }
596

597
    auto const numDegrees = static_cast<float>(event->angleDelta().y()) / 8.0f;
×
598
    auto const numSteps = numDegrees / 15.0f;
×
599

600
    auto const current_range = ui->x_axis_samples->value();
×
601

602
    float rangeFactor;
×
603
    if (_zoom_scaling_mode == ZoomScalingMode::Adaptive) {
×
604
        // Adaptive scaling: range factor is proportional to current range width
605
        // This makes adjustments more sensitive when zoomed in (small range), less sensitive when zoomed out (large range)
606
        rangeFactor = static_cast<float>(current_range) * 0.1f;// 10% of current range width
×
607

608
        // Clamp range factor to reasonable bounds
609
        rangeFactor = std::max(1.0f, std::min(rangeFactor, static_cast<float>(_time_frame->getTotalFrameCount()) / 100.0f));
×
610
    } else {
611
        // Fixed scaling (original behavior)
612
        rangeFactor = static_cast<float>(_time_frame->getTotalFrameCount()) / 10000.0f;
×
613
    }
614

615
    // Calculate range delta
616
    // Wheel up (positive numSteps) should zoom IN (decrease range width)
617
    // Wheel down (negative numSteps) should zoom OUT (increase range width)
618
    auto const range_delta = static_cast<int64_t>(-numSteps * rangeFactor);
×
619

620
    // Apply range delta and get the actual achieved range
621
    ui->openGLWidget->changeRangeWidth(range_delta);
×
622

623
    // Get the actual range that was achieved (may be different due to clamping)
624
    auto x_axis = ui->openGLWidget->getXAxis();
×
625
    auto const actual_range = static_cast<int>(x_axis.getEnd() - x_axis.getStart());
×
626

627
    // Update spinbox with the actual achieved range (not the requested range)
628
    updateXAxisSamples(actual_range);
×
629
    _updateLabels();
×
630
}
631

632
void DataViewer_Widget::_updateLabels() {
19✔
633
    auto x_axis = ui->openGLWidget->getXAxis();
19✔
634
    ui->neg_x_label->setText(QString::number(x_axis.getStart()));
19✔
635
    ui->pos_x_label->setText(QString::number(x_axis.getEnd()));
19✔
636
}
19✔
637

638
void DataViewer_Widget::_handleColorChanged(std::string const & feature_key, std::string const & hex_color) {
×
639
    // Update the color in the OpenGL widget display options (tree widget color management will be added later)
640

641
    auto const type = _data_manager->getType(feature_key);
×
642

643
    if (type == DM_DataType::Analog) {
×
644
        auto config = ui->openGLWidget->getAnalogConfig(feature_key);
×
645
        if (config.has_value()) {
×
646
            config.value()->hex_color = hex_color;
×
647
        }
648

649
    } else if (type == DM_DataType::DigitalEvent) {
×
650
        auto config = ui->openGLWidget->getDigitalEventConfig(feature_key);
×
651
        if (config.has_value()) {
×
652
            config.value()->hex_color = hex_color;
×
653
        }
654

655
    } else if (type == DM_DataType::DigitalInterval) {
×
656
        auto config = ui->openGLWidget->getDigitalIntervalConfig(feature_key);
×
657
        if (config.has_value()) {
×
658
            config.value()->hex_color = hex_color;
×
659
        }
660
    }
661

662
    // Trigger a redraw
663
    ui->openGLWidget->updateCanvas();
×
664

665
    std::cout << "Color changed for " << feature_key << " to " << hex_color << std::endl;
×
666
}
×
667

668
void DataViewer_Widget::_updateCoordinateDisplay(float time_coordinate, float canvas_y, QString const & series_info) {
×
669
    // Convert time coordinate to actual time using the time frame
670
    int const time_index = static_cast<int>(std::round(time_coordinate));
×
671
    int const actual_time = _time_frame->getTimeAtIndex(TimeFrameIndex(time_index));
×
672

673
    // Get canvas size for debugging
674
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
675

676
    // Use fixed-width formatting to prevent label resizing
677
    // Reserve space for reasonable max values (time: 10 digits, index: 10 digits, Y: 8 chars, canvas: 5x5 digits)
678
    QString coordinate_text;
×
679
    if (series_info.isEmpty()) {
×
680
        coordinate_text = QString("Time: %1  Index: %2  Y: %3  Canvas: %4x%5")
×
681
                                  .arg(actual_time, 10)           // Right-aligned, width 10
×
682
                                  .arg(time_index, 10)             // Right-aligned, width 10
×
683
                                  .arg(canvas_y, 8, 'f', 1)       // Right-aligned, width 8, 1 decimal
×
UNCOV
684
                                  .arg(canvas_width, 5)            // Right-aligned, width 5
×
685
                                  .arg(canvas_height, 5);          // Right-aligned, width 5
×
686
    } else {
687
        // For series info, still use fixed-width for numeric values but allow series info to vary
688
        coordinate_text = QString("Time: %1  Index: %2  %3  Canvas: %4x%5")
×
689
                                  .arg(actual_time, 10)
×
690
                                  .arg(time_index, 10)
×
UNCOV
691
                                  .arg(series_info, -30)           // Left-aligned, min width 30
×
UNCOV
692
                                  .arg(canvas_width, 5)
×
693
                                  .arg(canvas_height, 5);
×
694
    }
695

UNCOV
696
    ui->coordinate_label->setText(coordinate_text);
×
UNCOV
697
}
×
698

699
std::optional<NewAnalogTimeSeriesDisplayOptions *> DataViewer_Widget::getAnalogConfig(std::string const & key) const {
46✔
700
    return ui->openGLWidget->getAnalogConfig(key);
46✔
701
}
702

703
std::optional<NewDigitalEventSeriesDisplayOptions *> DataViewer_Widget::getDigitalEventConfig(std::string const & key) const {
29✔
704
    return ui->openGLWidget->getDigitalEventConfig(key);
29✔
705
}
706

UNCOV
707
std::optional<NewDigitalIntervalSeriesDisplayOptions *> DataViewer_Widget::getDigitalIntervalConfig(std::string const & key) const {
×
708
    return ui->openGLWidget->getDigitalIntervalConfig(key);
×
709
}
710

UNCOV
711
void DataViewer_Widget::_handleThemeChanged(int theme_index) {
×
UNCOV
712
    PlotTheme theme = (theme_index == 0) ? PlotTheme::Dark : PlotTheme::Light;
×
713
    ui->openGLWidget->setPlotTheme(theme);
×
714

715
    // Update coordinate label styling based on theme
716
    if (theme == PlotTheme::Dark) {
×
UNCOV
717
        ui->coordinate_label->setStyleSheet("background-color: rgba(0, 0, 0, 50); color: white; padding: 2px;");
×
718
    } else {
719
        ui->coordinate_label->setStyleSheet("background-color: rgba(255, 255, 255, 50); color: black; padding: 2px;");
×
720
    }
721

722
    std::cout << "Theme changed to: " << (theme_index == 0 ? "Dark" : "Light") << std::endl;
×
723
}
×
724

UNCOV
725
void DataViewer_Widget::_handleGridLinesToggled(bool enabled) {
×
726
    ui->openGLWidget->setGridLinesEnabled(enabled);
×
727
}
×
728

UNCOV
729
void DataViewer_Widget::_handleGridSpacingChanged(int spacing) {
×
UNCOV
730
    ui->openGLWidget->setGridSpacing(spacing);
×
UNCOV
731
}
×
732

733
void DataViewer_Widget::_handleVerticalSpacingChanged(double spacing) {
29✔
734
    ui->openGLWidget->setVerticalSpacing(static_cast<float>(spacing));
29✔
735

736
    // Also update PlottingManager vertical scale
737
    if (_plotting_manager) {
29✔
738
        // Convert spacing to a scale factor relative to default (0.1f)
739
        float const scale_factor = static_cast<float>(spacing) / 0.1f;
29✔
740
        _plotting_manager->setGlobalVerticalScale(scale_factor);
29✔
741

742
        // Apply updated positions to all registered series
743
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
29✔
744
        for (auto const & key: analog_keys) {
81✔
745
            _applyPlottingManagerAllocation(key);
52✔
746
        }
747
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
29✔
748
        for (auto const & key: event_keys) {
53✔
749
            _applyPlottingManagerAllocation(key);
24✔
750
        }
751
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
29✔
752
        for (auto const & key: interval_keys) {
29✔
UNCOV
753
            _applyPlottingManagerAllocation(key);
×
754
        }
755

756
        // Trigger canvas update
757
        ui->openGLWidget->updateCanvas();
29✔
758
    }
29✔
759
}
29✔
760

761
void DataViewer_Widget::_plotSelectedFeatureWithoutUpdate(std::string const & key) {
5✔
762
    std::cout << "Attempting to plot feature (batch): " << key << std::endl;
5✔
763

764
    if (key.empty()) {
5✔
UNCOV
765
        std::cerr << "Error: empty key in _plotSelectedFeatureWithoutUpdate" << std::endl;
×
UNCOV
766
        return;
×
767
    }
768

769
    if (!_data_manager) {
5✔
UNCOV
770
        std::cerr << "Error: null data manager in _plotSelectedFeatureWithoutUpdate" << std::endl;
×
UNCOV
771
        return;
×
772
    }
773

774
    // Get color from model
775
    std::string color = _feature_tree_model->getFeatureColor(key);
5✔
776
    std::cout << "Using color: " << color << " for series: " << key << std::endl;
5✔
777

778
    auto data_type = _data_manager->getType(key);
5✔
779
    std::cout << "Feature type: " << convert_data_type_to_string(data_type) << std::endl;
5✔
780

781
    if (data_type == DM_DataType::Analog) {
5✔
782
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
5✔
783
        if (!series) {
5✔
UNCOV
784
            std::cerr << "Error: failed to get AnalogTimeSeries for key: " << key << std::endl;
×
UNCOV
785
            return;
×
786
        }
787

788
        auto time_key = _data_manager->getTimeKey(key);
5✔
789
        auto time_frame = _data_manager->getTime(time_key);
5✔
790
        if (!time_frame) {
5✔
UNCOV
791
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
UNCOV
792
            return;
×
793
        }
794

795
        // Register with plotting manager for later allocation (single call)
796
        _plotting_manager->addAnalogSeries(key, series, time_frame, color);
5✔
797
        ui->openGLWidget->addAnalogTimeSeries(key, series, time_frame, color);
5✔
798

799
    } else if (data_type == DM_DataType::DigitalEvent) {
5✔
800
        auto series = _data_manager->getData<DigitalEventSeries>(key);
×
UNCOV
801
        if (!series) {
×
UNCOV
802
            std::cerr << "Error: failed to get DigitalEventSeries for key: " << key << std::endl;
×
803
            return;
×
804
        }
805

806
        auto time_key = _data_manager->getTimeKey(key);
×
807
        auto time_frame = _data_manager->getTime(time_key);
×
UNCOV
808
        if (!time_frame) {
×
809
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
810
            return;
×
811
        }
812
        _plotting_manager->addDigitalEventSeries(key, series, time_frame, color);
×
813
        ui->openGLWidget->addDigitalEventSeries(key, series, time_frame, color);
×
814

815
    } else if (data_type == DM_DataType::DigitalInterval) {
×
816
        auto series = _data_manager->getData<DigitalIntervalSeries>(key);
×
UNCOV
817
        if (!series) {
×
UNCOV
818
            std::cerr << "Error: failed to get DigitalIntervalSeries for key: " << key << std::endl;
×
819
            return;
×
820
        }
821

822
        auto time_key = _data_manager->getTimeKey(key);
×
823
        auto time_frame = _data_manager->getTime(time_key);
×
UNCOV
824
        if (!time_frame) {
×
825
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
826
            return;
×
827
        }
828
        _plotting_manager->addDigitalIntervalSeries(key, series, time_frame, color);
×
829
        ui->openGLWidget->addDigitalIntervalSeries(key, series, time_frame, color);
×
830

UNCOV
831
    } else {
×
UNCOV
832
        std::cout << "Feature type not supported: " << convert_data_type_to_string(data_type) << std::endl;
×
UNCOV
833
        return;
×
834
    }
835

836
    // Note: No canvas update triggered - this is for batch operations
837
    std::cout << "Successfully added series to OpenGL widget (batch mode)" << std::endl;
5✔
838
}
5✔
839

840
void DataViewer_Widget::_removeSelectedFeatureWithoutUpdate(std::string const & key) {
×
841
    std::cout << "Attempting to remove feature (batch): " << key << std::endl;
×
842

UNCOV
843
    if (key.empty()) {
×
UNCOV
844
        std::cerr << "Error: empty key in _removeSelectedFeatureWithoutUpdate" << std::endl;
×
845
        return;
×
846
    }
847

UNCOV
848
    if (!_data_manager) {
×
UNCOV
849
        std::cerr << "Error: null data manager in _removeSelectedFeatureWithoutUpdate" << std::endl;
×
850
        return;
×
851
    }
852

853
    auto data_type = _data_manager->getType(key);
×
854

855
    if (data_type == DM_DataType::Analog) {
×
856
        ui->openGLWidget->removeAnalogTimeSeries(key);
×
857
    } else if (data_type == DM_DataType::DigitalEvent) {
×
UNCOV
858
        ui->openGLWidget->removeDigitalEventSeries(key);
×
859
    } else if (data_type == DM_DataType::DigitalInterval) {
×
860
        ui->openGLWidget->removeDigitalIntervalSeries(key);
×
861
    } else {
UNCOV
862
        std::cout << "Feature type not supported for removal: " << convert_data_type_to_string(data_type) << std::endl;
×
UNCOV
863
        return;
×
864
    }
865

866
    // Note: No canvas update triggered - this is for batch operations
867
    std::cout << "Successfully removed series from OpenGL widget (batch mode)" << std::endl;
×
868
}
869

UNCOV
870
void DataViewer_Widget::_calculateOptimalScaling(std::vector<std::string> const & group_keys) {
×
UNCOV
871
    if (group_keys.empty()) {
×
872
        return;
×
873
    }
874

875
    std::cout << "Calculating optimal scaling for " << group_keys.size() << " analog channels..." << std::endl;
×
876

877
    // Get current canvas dimensions
UNCOV
878
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
879
    std::cout << "Canvas size: " << canvas_width << "x" << canvas_height << " pixels" << std::endl;
×
880

881
    // Count total number of currently visible analog series (including the new group)
882
    int total_visible_analog_series = static_cast<int>(group_keys.size());
×
883

884
    // Add any other already visible analog series
UNCOV
885
    auto all_keys = _data_manager->getAllKeys();
×
886
    for (auto const & key: all_keys) {
×
887
        if (_data_manager->getType(key) == DM_DataType::Analog) {
×
888
            // Check if this key is already in our group (avoid double counting)
889
            bool in_group = std::find(group_keys.begin(), group_keys.end(), key) != group_keys.end();
×
890
            if (!in_group) {
×
891
                // Check if this series is currently visible
UNCOV
892
                auto config = ui->openGLWidget->getAnalogConfig(key);
×
UNCOV
893
                if (config.has_value() && config.value()->is_visible) {
×
UNCOV
894
                    total_visible_analog_series++;
×
895
                }
896
            }
897
        }
898
    }
899

900
    std::cout << "Total visible analog series (including new group): " << total_visible_analog_series << std::endl;
×
901

UNCOV
902
    if (total_visible_analog_series <= 0) {
×
UNCOV
903
        return;// No series to scale
×
904
    }
905

906
    // Calculate optimal vertical spacing
907
    // Leave some margin at top and bottom (10% each = 20% total)
UNCOV
908
    float const effective_height = static_cast<float>(canvas_height) * 0.8f;
×
UNCOV
909
    float const optimal_spacing = effective_height / static_cast<float>(total_visible_analog_series);
×
910

911
    // Convert to normalized coordinates (OpenGL widget uses normalized spacing)
912
    // Assuming the widget's view height is typically around 2.0 units in normalized coordinates
913
    float const normalized_spacing = (optimal_spacing / static_cast<float>(canvas_height)) * 2.0f;
×
914

915
    // Clamp to reasonable bounds
UNCOV
916
    float const min_spacing = 0.01f;
×
917
    float const max_spacing = 1.0f;
×
918
    float const final_spacing = std::clamp(normalized_spacing, min_spacing, max_spacing);
×
919

UNCOV
920
    std::cout << "Calculated spacing: " << optimal_spacing << " pixels -> "
×
921
              << final_spacing << " normalized units" << std::endl;
×
922

923
    // Calculate optimal global gain based on standard deviations
924
    std::vector<float> std_devs;
×
925
    std_devs.reserve(group_keys.size());
×
926

927
    for (auto const & key: group_keys) {
×
928
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
×
929
        if (series) {
×
UNCOV
930
            float const std_dev = calculate_std_dev_approximate(*series);
×
931
            std_devs.push_back(std_dev);
×
UNCOV
932
            std::cout << "Series " << key << " std dev: " << std_dev << std::endl;
×
933
        }
UNCOV
934
    }
×
935

936
    if (!std_devs.empty()) {
×
937
        // Use the median standard deviation as reference for scaling
UNCOV
938
        std::sort(std_devs.begin(), std_devs.end());
×
UNCOV
939
        float const median_std_dev = std_devs[std_devs.size() / 2];
×
940

941
        // Calculate optimal global scale
942
        // Target: each series should use about 60% of its allocated vertical space
UNCOV
943
        float const target_amplitude_fraction = 0.6f;
×
944
        float const target_amplitude_in_pixels = optimal_spacing * target_amplitude_fraction;
×
945

946
        // Convert to normalized coordinates (3 standard deviations should fit in target amplitude)
UNCOV
947
        float const target_amplitude_normalized = (target_amplitude_in_pixels / static_cast<float>(canvas_height)) * 2.0f;
×
948
        float const three_sigma_target = target_amplitude_normalized;
×
949

950
        // Calculate scale factor needed
951
        float const optimal_global_scale = three_sigma_target / (3.0f * median_std_dev);
×
952

953
        // Clamp to reasonable bounds
UNCOV
954
        float const min_scale = 0.1f;
×
955
        float const max_scale = 100.0f;
×
956
        float const final_scale = std::clamp(optimal_global_scale, min_scale, max_scale);
×
957

UNCOV
958
        std::cout << "Median std dev: " << median_std_dev
×
UNCOV
959
                  << ", target amplitude: " << target_amplitude_in_pixels << " pixels"
×
960
                  << ", optimal global scale: " << final_scale << std::endl;
×
961

962
        // Apply the calculated settings
963
        ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
×
964
        ui->global_zoom->setValue(static_cast<double>(final_scale));
×
965

UNCOV
966
        std::cout << "Applied auto-scaling: vertical spacing = " << final_spacing
×
UNCOV
967
                  << ", global scale = " << final_scale << std::endl;
×
968

969
    } else {
970
        // If we can't calculate standard deviations, just apply spacing
971
        ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
×
UNCOV
972
        std::cout << "Applied auto-spacing only: vertical spacing = " << final_spacing << std::endl;
×
973
    }
974
}
×
975

UNCOV
976
void DataViewer_Widget::_calculateOptimalEventSpacing(std::vector<std::string> const & group_keys) {
×
UNCOV
977
    if (group_keys.empty()) {
×
978
        return;
×
979
    }
980

981
    std::cout << "Calculating optimal event spacing for " << group_keys.size() << " digital event series..." << std::endl;
×
982

983
    // Get current canvas dimensions
UNCOV
984
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
985
    std::cout << "Canvas size: " << canvas_width << "x" << canvas_height << " pixels" << std::endl;
×
986

987
    // Count total number of currently visible digital event series (including the new group)
988
    int total_visible_event_series = static_cast<int>(group_keys.size());
×
989

990
    // Add any other already visible digital event series
UNCOV
991
    auto all_keys = _data_manager->getAllKeys();
×
992
    for (auto const & key: all_keys) {
×
993
        if (_data_manager->getType(key) == DM_DataType::DigitalEvent) {
×
994
            // Check if this key is already in our group (avoid double counting)
995
            bool const in_group = std::find(group_keys.begin(), group_keys.end(), key) != group_keys.end();
×
996
            if (!in_group) {
×
997
                // Check if this series is currently visible
UNCOV
998
                auto config = ui->openGLWidget->getDigitalEventConfig(key);
×
UNCOV
999
                if (config.has_value() && config.value()->is_visible) {
×
UNCOV
1000
                    total_visible_event_series++;
×
1001
                }
1002
            }
1003
        }
1004
    }
1005

1006
    std::cout << "Total visible digital event series (including new group): " << total_visible_event_series << std::endl;
×
1007

UNCOV
1008
    if (total_visible_event_series <= 0) {
×
UNCOV
1009
        return;// No series to scale
×
1010
    }
1011

1012
    // Calculate optimal vertical spacing
1013
    // Leave some margin at top and bottom (10% each = 20% total)
UNCOV
1014
    float const effective_height = static_cast<float>(canvas_height) * 0.8f;
×
UNCOV
1015
    float const optimal_spacing = effective_height / static_cast<float>(total_visible_event_series);
×
1016

1017
    // Convert to normalized coordinates (OpenGL widget uses normalized spacing)
1018
    // Assuming the widget's view height is typically around 2.0 units in normalized coordinates
1019
    float const normalized_spacing = (optimal_spacing / static_cast<float>(canvas_height)) * 2.0f;
×
1020

1021
    // Clamp to reasonable bounds
UNCOV
1022
    float const min_spacing = 0.01f;
×
UNCOV
1023
    float const max_spacing = 1.0f;
×
UNCOV
1024
    float const final_spacing = std::clamp(normalized_spacing, min_spacing, max_spacing);
×
1025

1026
    // Calculate optimal event height (keep events clearly within their lane)
1027
    // Use a conservative fraction of spacing so multiple stacked series remain visually distinct
1028
    float const optimal_event_height = std::min(final_spacing * 0.3f, 0.2f);
×
UNCOV
1029
    float const min_height = 0.01f;
×
1030
    float const max_height = 0.5f;
×
1031
    float const final_height = std::clamp(optimal_event_height, min_height, max_height);
×
1032

UNCOV
1033
    std::cout << "Calculated spacing: " << optimal_spacing << " pixels -> "
×
UNCOV
1034
              << final_spacing << " normalized units" << std::endl;
×
1035
    std::cout << "Calculated event height: " << final_height << " normalized units" << std::endl;
×
1036

1037
    // Apply the calculated settings to all event series in the group
1038
    for (auto const & key: group_keys) {
×
1039
        auto config = ui->openGLWidget->getDigitalEventConfig(key);
×
1040
        if (config.has_value()) {
×
UNCOV
1041
            config.value()->vertical_spacing = final_spacing;
×
UNCOV
1042
            config.value()->event_height = final_height;
×
UNCOV
1043
            config.value()->display_mode = EventDisplayMode::Stacked;// Ensure stacked mode
×
1044
        }
1045
    }
1046

UNCOV
1047
    std::cout << "Applied auto-calculated event spacing: spacing = " << final_spacing
×
UNCOV
1048
              << ", height = " << final_height << std::endl;
×
UNCOV
1049
}
×
1050

1051
void DataViewer_Widget::autoArrangeVerticalSpacing() {
29✔
1052
    std::cout << "DataViewer_Widget: Auto-arranging with plotting manager..." << std::endl;
29✔
1053

1054
    // Update dimensions first
1055
    _updatePlottingManagerDimensions();
29✔
1056

1057
    // Apply new allocations to all registered series
1058
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
29✔
1059
    auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
29✔
1060
    auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
29✔
1061

1062
    for (auto const & key: analog_keys) {
81✔
1063
        _applyPlottingManagerAllocation(key);
52✔
1064
    }
1065
    for (auto const & key: event_keys) {
53✔
1066
        _applyPlottingManagerAllocation(key);
24✔
1067
    }
1068
    for (auto const & key: interval_keys) {
29✔
UNCOV
1069
        _applyPlottingManagerAllocation(key);
×
1070
    }
1071

1072
    // Calculate and apply optimal scaling to fill the canvas
1073
    _autoFillCanvas();
29✔
1074

1075
    // Update OpenGL widget view bounds based on content height
1076
    _updateViewBounds();
29✔
1077

1078
    // Trigger canvas update to show new positions
1079
    ui->openGLWidget->updateCanvas();
29✔
1080

1081
    auto total_keys = analog_keys.size() + event_keys.size() + interval_keys.size();
29✔
1082
    std::cout << "DataViewer_Widget: Auto-arrange completed for " << total_keys << " series" << std::endl;
29✔
1083
}
58✔
1084

1085
void DataViewer_Widget::_updateViewBounds() {
29✔
1086
    if (!_plotting_manager) {
29✔
UNCOV
1087
        return;
×
1088
    }
1089

1090
    // PlottingManager uses normalized coordinates, so view bounds are typically -1 to +1
1091
    // For now, use standard bounds but this enables future enhancement
1092
    std::cout << "DataViewer_Widget: Using standard view bounds with PlottingManager" << std::endl;
29✔
1093
}
1094

1095
std::string DataViewer_Widget::_convertDataType(DM_DataType dm_type) const {
×
1096
    switch (dm_type) {
×
1097
        case DM_DataType::Analog:
×
1098
            return "Analog";
×
1099
        case DM_DataType::DigitalEvent:
×
1100
            return "DigitalEvent";
×
UNCOV
1101
        case DM_DataType::DigitalInterval:
×
UNCOV
1102
            return "DigitalInterval";
×
1103
        default:
×
1104
            // For unsupported types, default to Analog
1105
            // This should be rare in practice given our type filters
UNCOV
1106
            std::cerr << "Warning: Unsupported data type " << convert_data_type_to_string(dm_type)
×
UNCOV
1107
                      << " defaulting to Analog for plotting manager" << std::endl;
×
UNCOV
1108
            return "Analog";
×
1109
    }
1110
}
1111

1112
void DataViewer_Widget::_updatePlottingManagerDimensions() {
47✔
1113
    if (!_plotting_manager) {
47✔
UNCOV
1114
        return;
×
1115
    }
1116

1117
    // Get current canvas dimensions from OpenGL widget
1118
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
47✔
1119

1120
    // PlottingManager works in normalized device coordinates, so no specific dimension update needed
1121
    // But we could update viewport bounds if needed in the future
1122

1123
    std::cout << "DataViewer_Widget: Updated plotting manager dimensions: "
47✔
1124
              << canvas_width << "x" << canvas_height << " pixels" << std::endl;
47✔
1125
}
1126

1127
void DataViewer_Widget::_applyPlottingManagerAllocation(std::string const & series_key) {
243✔
1128
    if (!_plotting_manager) {
243✔
UNCOV
1129
        return;
×
1130
    }
1131

1132
    auto data_type = _data_manager->getType(series_key);
243✔
1133

1134
    std::cout << "DataViewer_Widget: Applying plotting manager allocation for '" << series_key << "'" << std::endl;
243✔
1135

1136
    // For now, use a basic implementation that will be enhanced when we update OpenGLWidget
1137
    // The main goal is to get the compilation working first
1138

1139
    // Apply positioning based on data type
1140
    if (data_type == DM_DataType::Analog) {
243✔
1141
        auto config = ui->openGLWidget->getAnalogConfig(series_key);
178✔
1142
        if (config.has_value()) {
178✔
1143
            float yc, yh;
178✔
1144
            if (_plotting_manager->getAnalogSeriesAllocationForKey(series_key, yc, yh)) {
178✔
1145
                config.value()->allocated_y_center = yc;
178✔
1146
                config.value()->allocated_height = yh;
178✔
1147
            }
1148
        }
1149

1150
    } else if (data_type == DM_DataType::DigitalEvent) {
65✔
1151
        auto config = ui->openGLWidget->getDigitalEventConfig(series_key);
65✔
1152
        if (config.has_value()) {
65✔
1153
            // Basic allocation - will be properly implemented when OpenGL widget is updated
1154
            std::cout << "  Applied basic allocation to event '" << series_key << "'" << std::endl;
65✔
1155
        }
1156

UNCOV
1157
    } else if (data_type == DM_DataType::DigitalInterval) {
×
1158
        auto config = ui->openGLWidget->getDigitalIntervalConfig(series_key);
×
UNCOV
1159
        if (config.has_value()) {
×
1160
            // Basic allocation - will be properly implemented when OpenGL widget is updated
UNCOV
1161
            std::cout << "  Applied basic allocation to interval '" << series_key << "'" << std::endl;
×
1162
        }
1163
    }
1164
}
1165

1166
// ===== Context menu and configuration handling =====
1167
void DataViewer_Widget::_showGroupContextMenu(std::string const & group_name, QPoint const & global_pos) {
×
1168
    QMenu menu;
×
UNCOV
1169
    QMenu * loadMenu = menu.addMenu("Load configuration");
×
1170
    QAction * loadSpikeSorter = loadMenu->addAction("spikesorter configuration");
×
1171
    QAction * clearConfig = menu.addAction("Clear configuration");
×
1172

1173
    connect(loadSpikeSorter, &QAction::triggered, this, [this, group_name]() {
×
1174
        _loadSpikeSorterConfigurationForGroup(QString::fromStdString(group_name));
×
1175
    });
×
UNCOV
1176
    connect(clearConfig, &QAction::triggered, this, [this, group_name]() {
×
1177
        _clearConfigurationForGroup(QString::fromStdString(group_name));
×
1178
    });
×
1179

1180
    menu.exec(global_pos);
×
UNCOV
1181
}
×
1182

1183
void DataViewer_Widget::_loadSpikeSorterConfigurationForGroup(QString const & group_name) {
×
1184
    // For now, use a test constant string or file dialog; here we open a file dialog
1185
    QString path = QFileDialog::getOpenFileName(this, QString("Load spikesorter configuration for %1").arg(group_name), QString(), "Text Files (*.txt *.cfg *.conf);;All Files (*)");
×
1186
    if (path.isEmpty()) return;
×
1187
    QFile file(path);
×
1188
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return;
×
1189
    QByteArray data = file.readAll();
×
UNCOV
1190
    auto positions = _parseSpikeSorterConfig(data.toStdString());
×
UNCOV
1191
    if (positions.empty()) return;
×
1192
    _plotting_manager->loadAnalogSpikeSorterConfiguration(group_name.toStdString(), positions);
×
1193

1194
    // Re-apply allocation to visible analog keys and update
UNCOV
1195
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
×
1196
    for (auto const & key : analog_keys) {
×
1197
        _applyPlottingManagerAllocation(key);
×
1198
    }
1199
    ui->openGLWidget->updateCanvas();
×
1200
}
×
1201

1202
void DataViewer_Widget::_clearConfigurationForGroup(QString const & group_name) {
×
1203
    _plotting_manager->clearAnalogGroupConfiguration(group_name.toStdString());
×
UNCOV
1204
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
×
1205
    for (auto const & key : analog_keys) {
×
1206
        _applyPlottingManagerAllocation(key);
×
1207
    }
UNCOV
1208
    ui->openGLWidget->updateCanvas();
×
UNCOV
1209
}
×
1210

1211
std::vector<PlottingManager::AnalogGroupChannelPosition> DataViewer_Widget::_parseSpikeSorterConfig(std::string const & text) {
1✔
1212
    std::vector<PlottingManager::AnalogGroupChannelPosition> out;
1✔
1213
    std::istringstream ss(text);
1✔
1214
    std::string line;
1✔
1215
    bool first = true;
1✔
1216
    while (std::getline(ss, line)) {
6✔
1217
        if (line.empty()) continue;
5✔
1218
        if (first) { first = false; continue; } // skip header row (electrode name)
5✔
1219
        std::istringstream ls(line);
4✔
1220
        int row = 0; int ch = 0; float x = 0.0f; float y = 0.0f;
4✔
1221
        if (!(ls >> row >> ch >> x >> y)) continue;
4✔
1222
        // SpikeSorter is 1-based; convert to 0-based for our program
1223
        if (ch > 0) ch -= 1;
4✔
1224
        PlottingManager::AnalogGroupChannelPosition p; p.channel_id = ch; p.x = x; p.y = y;
4✔
1225
        out.push_back(p);
4✔
1226
    }
4✔
1227
    return out;
2✔
1228
}
1✔
1229

1230
void DataViewer_Widget::_loadSpikeSorterConfigurationFromText(QString const & group_name, QString const & text) {
1✔
1231
    auto positions = _parseSpikeSorterConfig(text.toStdString());
1✔
1232
    if (positions.empty()) {
1✔
UNCOV
1233
        std::cout << "No positions found in spike sorter configuration" << std::endl;
×
UNCOV
1234
        return;
×
1235
    }
1236
    _plotting_manager->loadAnalogSpikeSorterConfiguration(group_name.toStdString(), positions);
1✔
1237
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
1✔
1238
    for (auto const & key : analog_keys) {
5✔
1239
        _applyPlottingManagerAllocation(key);
4✔
1240
    }
1241
    ui->openGLWidget->updateCanvas();
1✔
1242
}
1✔
1243

1244
void DataViewer_Widget::_autoFillCanvas() {
29✔
1245
    std::cout << "DataViewer_Widget: Auto-filling canvas with PlottingManager..." << std::endl;
29✔
1246

1247
    if (!_plotting_manager) {
29✔
UNCOV
1248
        std::cout << "No plotting manager available" << std::endl;
×
UNCOV
1249
        return;
×
1250
    }
1251

1252
    // Get current canvas dimensions
1253
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
29✔
1254
    std::cout << "Canvas size: " << canvas_width << "x" << canvas_height << " pixels" << std::endl;
29✔
1255

1256
    // Count visible series using PlottingManager
1257
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
29✔
1258
    auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
29✔
1259
    auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
29✔
1260

1261
    int visible_analog_count = static_cast<int>(analog_keys.size());
29✔
1262
    int visible_event_count = static_cast<int>(event_keys.size());
29✔
1263
    int visible_interval_count = static_cast<int>(interval_keys.size());
29✔
1264

1265
    int total_visible = visible_analog_count + visible_event_count + visible_interval_count;
29✔
1266
    std::cout << "Visible series: " << visible_analog_count << " analog, "
29✔
1267
              << visible_event_count << " events, " << visible_interval_count
29✔
1268
              << " intervals (total: " << total_visible << ")" << std::endl;
29✔
1269

1270
    if (total_visible == 0) {
29✔
UNCOV
1271
        std::cout << "No visible series to auto-scale" << std::endl;
×
UNCOV
1272
        return;
×
1273
    }
1274

1275
    // Calculate optimal vertical spacing to fill canvas
1276
    // Use 90% of canvas height, leaving 5% margin at top and bottom
1277
    float const usable_height = static_cast<float>(canvas_height) * 0.9f;
29✔
1278
    float const optimal_spacing_pixels = usable_height / static_cast<float>(total_visible);
29✔
1279

1280
    // Convert to normalized coordinates (assuming 2.0 total normalized height)
1281
    float const optimal_spacing_normalized = (optimal_spacing_pixels / static_cast<float>(canvas_height)) * 2.0f;
29✔
1282

1283
    // Clamp to reasonable bounds
1284
    float const min_spacing = 0.02f;
29✔
1285
    float const max_spacing = 1.5f;
29✔
1286
    float const final_spacing = std::clamp(optimal_spacing_normalized, min_spacing, max_spacing);
29✔
1287

1288
    std::cout << "Calculated optimal spacing: " << optimal_spacing_pixels << " pixels -> "
29✔
1289
              << final_spacing << " normalized units" << std::endl;
29✔
1290

1291
    // Apply the calculated vertical spacing
1292
    ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
29✔
1293

1294
    // Calculate and apply optimal event heights for digital event series
1295
    if (visible_event_count > 0) {
29✔
1296
        // Calculate event height conservatively to avoid near full-lane rendering
1297
        // Use 30% of the spacing and cap at 0.2 to keep consistent scale across zooms
1298
        float const optimal_event_height = std::min(final_spacing * 0.3f, 0.2f);
11✔
1299

1300
        std::cout << "Calculated optimal event height: " << optimal_event_height << " normalized units" << std::endl;
11✔
1301

1302
        // Apply optimal height to all visible digital event series
1303
        for (auto const & key: event_keys) {
35✔
1304
            auto config = ui->openGLWidget->getDigitalEventConfig(key);
24✔
1305
            if (config.has_value() && config.value()->is_visible) {
24✔
1306
                config.value()->event_height = optimal_event_height;
24✔
1307
                config.value()->display_mode = EventDisplayMode::Stacked;// Ensure stacked mode
24✔
1308
                std::cout << "  Applied event height " << optimal_event_height
24✔
1309
                          << " to series '" << key << "'" << std::endl;
24✔
1310
            }
1311
        }
1312
    }
1313

1314
    // Calculate and apply optimal interval heights for digital interval series
1315
    if (visible_interval_count > 0) {
29✔
1316
        // Calculate optimal interval height to fill most of the allocated space
1317
        // Use 80% of the spacing to leave some visual separation between intervals
UNCOV
1318
        float const optimal_interval_height = final_spacing * 0.8f;
×
1319

1320
        std::cout << "Calculated optimal interval height: " << optimal_interval_height << " normalized units" << std::endl;
×
1321

1322
        // Apply optimal height to all visible digital interval series
1323
        for (auto const & key: interval_keys) {
×
1324
            auto config = ui->openGLWidget->getDigitalIntervalConfig(key);
×
1325
            if (config.has_value() && config.value()->is_visible) {
×
UNCOV
1326
                config.value()->interval_height = optimal_interval_height;
×
UNCOV
1327
                std::cout << "  Applied interval height " << optimal_interval_height
×
UNCOV
1328
                          << " to series '" << key << "'" << std::endl;
×
1329
            }
1330
        }
1331
    }
1332

1333
    // Calculate optimal global scale for analog series to use their allocated space effectively
1334
    if (visible_analog_count > 0) {
29✔
1335
        // Sample a few analog series to estimate appropriate scaling
1336
        std::vector<float> sample_std_devs;
22✔
1337
        sample_std_devs.reserve(std::min(5, visible_analog_count));// Sample up to 5 series
22✔
1338

1339
        int sampled = 0;
22✔
1340
        for (auto const & key: analog_keys) {
74✔
1341
            if (sampled >= 5) break;
52✔
1342

1343
            auto config = ui->openGLWidget->getAnalogConfig(key);
52✔
1344
            if (config.has_value() && config.value()->is_visible) {
52✔
1345
                auto series = _data_manager->getData<AnalogTimeSeries>(key);
52✔
1346
                if (series) {
52✔
1347
                    float std_dev = calculate_std_dev_approximate(*series);
52✔
1348
                    if (std_dev > 0.0f) {
52✔
1349
                        sample_std_devs.push_back(std_dev);
52✔
1350
                        sampled++;
52✔
1351
                    }
1352
                }
1353
            }
52✔
1354
        }
1355

1356
        if (!sample_std_devs.empty()) {
22✔
1357
            // Use median standard deviation for scaling calculation
1358
            std::sort(sample_std_devs.begin(), sample_std_devs.end());
22✔
1359
            float median_std_dev = sample_std_devs[sample_std_devs.size() / 2];
22✔
1360

1361
            // Calculate scale so that ±3 standard deviations use ~60% of allocated space
1362
            float const target_amplitude_fraction = 0.6f;
22✔
1363
            float const target_amplitude_pixels = optimal_spacing_pixels * target_amplitude_fraction;
22✔
1364
            float const target_amplitude_normalized = (target_amplitude_pixels / static_cast<float>(canvas_height)) * 2.0f;
22✔
1365

1366
            // For ±3σ coverage
1367
            float const three_sigma_coverage = target_amplitude_normalized;
22✔
1368
            float const optimal_global_scale = three_sigma_coverage / (6.0f * median_std_dev);
22✔
1369

1370
            // Clamp to reasonable bounds
1371
            float const min_scale = 0.01f;
22✔
1372
            float const max_scale = 100.0f;
22✔
1373
            float const final_scale = std::clamp(optimal_global_scale, min_scale, max_scale);
22✔
1374

1375
            std::cout << "Calculated optimal global scale: median_std_dev=" << median_std_dev
22✔
1376
                      << ", target_amplitude=" << target_amplitude_pixels << " pixels"
22✔
1377
                      << ", final_scale=" << final_scale << std::endl;
22✔
1378

1379
            // Apply the calculated global scale
1380
            ui->global_zoom->setValue(static_cast<double>(final_scale));
22✔
1381
        }
1382
    }
22✔
1383

1384
    std::cout << "Auto-fill canvas completed" << std::endl;
29✔
1385
}
29✔
1386

1387
void DataViewer_Widget::cleanupDeletedData() {
15✔
1388
    if (!_data_manager) {
15✔
UNCOV
1389
        return;
×
1390
    }
1391

1392
    // Collect keys that no longer exist in DataManager
1393
    std::vector<std::string> keys_to_cleanup;
15✔
1394

1395
    if (_plotting_manager) {
15✔
1396
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
15✔
1397
        for (auto const & key: analog_keys) {
18✔
1398
            if (!_data_manager->getData<AnalogTimeSeries>(key)) {
3✔
UNCOV
1399
                keys_to_cleanup.push_back(key);
×
1400
            }
1401
        }
1402
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
15✔
1403
        for (auto const & key: event_keys) {
15✔
UNCOV
1404
            if (!_data_manager->getData<DigitalEventSeries>(key)) {
×
UNCOV
1405
                keys_to_cleanup.push_back(key);
×
1406
            }
1407
        }
1408
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
15✔
1409
        for (auto const & key: interval_keys) {
15✔
UNCOV
1410
            if (!_data_manager->getData<DigitalIntervalSeries>(key)) {
×
UNCOV
1411
                keys_to_cleanup.push_back(key);
×
1412
            }
1413
        }
1414
    }
15✔
1415

1416
    if (keys_to_cleanup.empty()) {
15✔
1417
        return;
15✔
1418
    }
1419

1420
    // De-duplicate keys in case the same key appears in multiple lists
UNCOV
1421
    std::sort(keys_to_cleanup.begin(), keys_to_cleanup.end());
×
1422
    keys_to_cleanup.erase(std::unique(keys_to_cleanup.begin(), keys_to_cleanup.end()), keys_to_cleanup.end());
×
1423

1424
    // Post cleanup to OpenGLWidget's thread safely
1425
    QPointer<OpenGLWidget> glw = ui ? ui->openGLWidget : nullptr;
×
1426
    if (glw) {
×
1427
        QMetaObject::invokeMethod(glw, [glw, keys = keys_to_cleanup]() {
×
1428
            if (!glw) return;
×
1429
            for (auto const & key : keys) {
×
UNCOV
1430
                glw->removeAnalogTimeSeries(key);
×
UNCOV
1431
                glw->removeDigitalEventSeries(key);
×
UNCOV
1432
                glw->removeDigitalIntervalSeries(key);
×
1433
            } }, Qt::QueuedConnection);
1434
    }
1435

1436
    // Remove from PlottingManager defensively (all types) on our thread
1437
    if (_plotting_manager) {
×
1438
        for (auto const & key: keys_to_cleanup) {
×
UNCOV
1439
            (void) _plotting_manager->removeAnalogSeries(key);
×
UNCOV
1440
            (void) _plotting_manager->removeDigitalEventSeries(key);
×
UNCOV
1441
            (void) _plotting_manager->removeDigitalIntervalSeries(key);
×
1442
        }
1443
    }
1444

1445
    // Re-arrange remaining data
1446
    autoArrangeVerticalSpacing();
×
1447
}
15✔
1448

UNCOV
1449
void DataViewer_Widget::_hidePropertiesPanel() {
×
1450
    // Save current splitter sizes before hiding
1451
    _saved_splitter_sizes = ui->main_splitter->sizes();
×
1452
    
1453
    // Collapse the properties panel to 0 width
1454
    ui->main_splitter->setSizes({0, ui->main_splitter->sizes()[1]});
×
1455
    
1456
    // Hide the properties panel and show the reveal button
1457
    ui->properties_container->hide();
×
UNCOV
1458
    ui->show_properties_button->show();
×
1459
    
UNCOV
1460
    _properties_panel_collapsed = true;
×
1461
    
1462
    std::cout << "Properties panel hidden" << std::endl;
×
1463
    
1464
    // Trigger a canvas update to adjust to new size
1465
    ui->openGLWidget->update();
×
UNCOV
1466
}
×
1467

UNCOV
1468
void DataViewer_Widget::_showPropertiesPanel() {
×
1469
    // Show the properties panel
1470
    ui->properties_container->show();
×
1471
    
1472
    // Restore saved splitter sizes
UNCOV
1473
    if (!_saved_splitter_sizes.isEmpty()) {
×
1474
        ui->main_splitter->setSizes(_saved_splitter_sizes);
×
1475
    } else {
1476
        // Default sizes if no saved sizes (320px for properties, rest for plot)
UNCOV
1477
        ui->main_splitter->setSizes({320, 1000});
×
1478
    }
1479
    
1480
    // Hide the reveal button
UNCOV
1481
    ui->show_properties_button->hide();
×
1482
    
UNCOV
1483
    _properties_panel_collapsed = false;
×
1484
    
1485
    std::cout << "Properties panel shown" << std::endl;
×
1486
    
1487
    // Trigger a canvas update to adjust to new size
UNCOV
1488
    ui->openGLWidget->update();
×
UNCOV
1489
}
×
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