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

paulmthompson / WhiskerToolbox / 18685379784

21 Oct 2025 01:25PM UTC coverage: 72.522% (+0.1%) from 72.391%
18685379784

push

github

paulmthompson
fix failing tests

18 of 40 new or added lines in 1 file covered. (45.0%)

1765 existing lines in 32 files now uncovered.

53998 of 74457 relevant lines covered (72.52%)

46177.73 hits per line

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

57.97
/src/WhiskerToolbox/TableDesignerWidget/TableDesignerWidget.cpp
1
#include "TableDesignerWidget.hpp"
2
#include "ui_TableDesignerWidget.h"
3

4
#include "Collapsible_Widget/Section.hpp"
5
#include "DataManager/AnalogTimeSeries/Analog_Time_Series.hpp"
6
#include "DataManager/DataManager.hpp"
7
#include "DataManager/DigitalTimeSeries/Digital_Event_Series.hpp"
8
#include "DataManager/DigitalTimeSeries/Digital_Interval_Series.hpp"
9
#include "DataManager/Lines/Line_Data.hpp"
10
#include "DataManager/Points/Point_Data.hpp"
11
#include "DataManager/utils/TableView/ComputerRegistry.hpp"
12
#include "DataManager/utils/TableView/TableEvents.hpp"
13
#include "DataManager/utils/TableView/TableRegistry.hpp"
14
#include "DataManager/utils/TableView/adapters/DataManagerExtension.h"
15
#include "DataManager/utils/TableView/computers/EventInIntervalComputer.h"
16
#include "DataManager/utils/TableView/computers/IntervalReductionComputer.h"
17
#include "DataManager/utils/TableView/core/TableViewBuilder.h"
18
#include "DataManager/utils/TableView/interfaces/IColumnComputer.h"
19
#include "DataManager/utils/TableView/interfaces/IRowSelector.h"
20
#include "DataManager/utils/TableView/transforms/PCATransform.hpp"
21
#include "Entity/EntityGroupManager.hpp"
22
#include "TableExportWidget.hpp"
23
#include "TableInfoWidget.hpp"
24
#include "TableJSONWidget.hpp"
25
#include "TableTransformWidget.hpp"
26
#include "TableViewerWidget/TableViewerWidget.hpp"
27

28

29
#include <QCheckBox>
30
#include <QComboBox>
31
#include <QDebug>
32
#include <QFileDialog>
33
#include <QGroupBox>
34
#include <QHBoxLayout>
35
#include <QInputDialog>
36
#include <QLabel>
37
#include <QLineEdit>
38
#include <QMessageBox>
39
#include <QPushButton>
40
#include <QRegularExpression>
41
#include <QSpinBox>
42
#include <QTableView>
43
#include <QTreeWidget>
44
#include <QTreeWidgetItem>
45
#include <QVBoxLayout>
46

47
#include <QFutureWatcher>
48
#include <QJsonArray>
49
#include <QJsonDocument>
50
#include <QJsonObject>
51
#include <QJsonParseError>
52
#include <QTimer>
53
#include <QtConcurrent>
54

55
#include <algorithm>
56
#include <fstream>
57
#include <iomanip>
58
#include <limits>
59
#include <regex>
60
#include <tuple>
61
#include <typeindex>
62
#include <vector>
63

64
TableDesignerWidget::TableDesignerWidget(std::shared_ptr<DataManager> data_manager, QWidget * parent)
18✔
65
    : QWidget(parent),
66
      ui(new Ui::TableDesignerWidget),
36✔
67
      _data_manager(std::move(data_manager)) {
54✔
68

69
    ui->setupUi(this);
18✔
70

71
    // Configure combo boxes for better scrolling with many items
72
    // Combo box scrolling is now handled by the global stylesheet
73

74
    _parameter_widget = nullptr;
18✔
75
    _parameter_layout = nullptr;
18✔
76

77
    // Initialize table viewer widget for preview
78
    _table_viewer = new TableViewerWidget(this);
18✔
79

80
    // Add the table viewer widget to the preview layout
81
    ui->preview_layout->addWidget(_table_viewer);
18✔
82

83
    _preview_debounce_timer = new QTimer(this);
18✔
84
    _preview_debounce_timer->setSingleShot(true);
18✔
85
    _preview_debounce_timer->setInterval(150);
18✔
86
    connect(_preview_debounce_timer, &QTimer::timeout, this, &TableDesignerWidget::rebuildPreviewNow);
18✔
87

88
    _table_info_widget = new TableInfoWidget(this);
18✔
89
    _table_info_section = new Section(this, "Table Information");
18✔
90
    _table_info_section->setContentLayout(*new QVBoxLayout());
18✔
91
    _table_info_section->layout()->addWidget(_table_info_widget);
18✔
92
    _table_info_section->autoSetContentLayout();
18✔
93
    ui->main_layout->insertWidget(1, _table_info_section);
18✔
94

95
    // Hook save from table info widget
96
    connect(_table_info_widget, &TableInfoWidget::saveClicked, this, &TableDesignerWidget::onSaveTableInfo);
18✔
97

98
    // Connect table viewer signals for better integration
99
    connect(_table_viewer, &TableViewerWidget::rowScrolled, this, [this](size_t row_index) {
18✔
100
        // Optional: Could emit a signal or update status when user scrolls preview
101
        // For now, just ensure the table viewer is working as expected
102
        Q_UNUSED(row_index)
UNCOV
103
    });
×
104

105
    connectSignals();
18✔
106

107

108
    // Initialize UI to a clean state, then populate controls
109
    clearUI();
18✔
110
    refreshTableCombo();
18✔
111
    refreshRowDataSourceCombo();
18✔
112
    refreshComputersTree();
18✔
113

114
    // Insert Transform section
115
    _table_transform_widget = new TableTransformWidget(this);
18✔
116
    _table_transform_section = new Section(this, "Transforms");
18✔
117
    _table_transform_section->setContentLayout(*new QVBoxLayout());
18✔
118
    _table_transform_section->layout()->addWidget(_table_transform_widget);
18✔
119
    _table_transform_section->autoSetContentLayout();
18✔
120
    // Place after build_group (after preview)
121
    ui->main_layout->insertWidget(ui->main_layout->indexOf(ui->build_group) + 1, _table_transform_section);
18✔
122
    connect(_table_transform_widget, &TableTransformWidget::applyTransformClicked,
54✔
123
            this, &TableDesignerWidget::onApplyTransform);
36✔
124

125
    // Insert Export section
126
    _table_export_widget = new TableExportWidget(this);
18✔
127
    _table_export_section = new Section(this, "Export");
18✔
128
    _table_export_section->setContentLayout(*new QVBoxLayout());
18✔
129
    _table_export_section->layout()->addWidget(_table_export_widget);
18✔
130
    _table_export_section->autoSetContentLayout();
18✔
131
    ui->main_layout->insertWidget(ui->main_layout->indexOf(ui->build_group) + 2, _table_export_section);
18✔
132
    connect(_table_export_widget, &TableExportWidget::exportClicked,
54✔
133
            this, &TableDesignerWidget::onExportCsv);
36✔
134

135
    // Insert JSON section
136
    _table_json_widget = new TableJSONWidget(this);
18✔
137
    _table_json_section = new Section(this, "Table JSON Template");
18✔
138
    _table_json_section->setContentLayout(*new QVBoxLayout());
18✔
139
    _table_json_section->layout()->addWidget(_table_json_widget);
18✔
140
    _table_json_section->autoSetContentLayout();
18✔
141
    ui->main_layout->insertWidget(ui->main_layout->indexOf(ui->build_group) + 3, _table_json_section);
18✔
142
    connect(_table_json_widget, &TableJSONWidget::updateRequested, this, [this](QString const & jsonText) {
18✔
143
        applyJsonTemplateToUI(jsonText);
7✔
144
    });
7✔
145

146
    // Add observer to automatically refresh dropdowns when DataManager changes
147
    if (_data_manager) {
18✔
148
        _data_manager->addObserver([this]() {
18✔
149
            refreshAllDataSources();
4✔
150
        });
4✔
151
    }
152

153
    qDebug() << "TableDesignerWidget initialized with TableViewerWidget for efficient pagination";
18✔
154
}
18✔
155

156
TableDesignerWidget::~TableDesignerWidget() {
18✔
157
    delete ui;
18✔
158
}
18✔
159

160
void TableDesignerWidget::refreshAllDataSources() {
4✔
161
    qDebug() << "Manually refreshing all data sources...";
4✔
162
    refreshRowDataSourceCombo();
4✔
163
    refreshComputersTree();
4✔
164

165
    // If we have a selected table, refresh its info
166
    if (!_current_table_id.isEmpty()) {
4✔
UNCOV
167
        loadTableInfo(_current_table_id);
×
168
    }
169
}
4✔
170

171

172
void TableDesignerWidget::connectSignals() {
18✔
173
    // Table selection signals
174
    connect(ui->table_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
54✔
175
            this, &TableDesignerWidget::onTableSelectionChanged);
36✔
176
    connect(ui->new_table_btn, &QPushButton::clicked,
54✔
177
            this, &TableDesignerWidget::onCreateNewTable);
36✔
178
    connect(ui->delete_table_btn, &QPushButton::clicked,
54✔
179
            this, &TableDesignerWidget::onDeleteTable);
36✔
180

181
    // Table info signals are connected via TableInfoWidget
182

183
    // Row source signals
184
    connect(ui->row_data_source_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
54✔
185
            this, &TableDesignerWidget::onRowDataSourceChanged);
36✔
186
    connect(ui->capture_range_spinbox, QOverload<int>::of(&QSpinBox::valueChanged),
54✔
187
            this, &TableDesignerWidget::onCaptureRangeChanged);
36✔
188
    connect(ui->interval_beginning_radio, &QRadioButton::toggled,
54✔
189
            this, &TableDesignerWidget::onIntervalSettingChanged);
36✔
190
    connect(ui->interval_end_radio, &QRadioButton::toggled,
54✔
191
            this, &TableDesignerWidget::onIntervalSettingChanged);
36✔
192
    connect(ui->interval_itself_radio, &QRadioButton::toggled,
54✔
193
            this, &TableDesignerWidget::onIntervalSettingChanged);
36✔
194

195
    // Column design signals (tree-based)
196
    connect(ui->computers_tree, &QTreeWidget::itemChanged,
54✔
197
            this, &TableDesignerWidget::onComputersTreeItemChanged);
36✔
198
    connect(ui->computers_tree, &QTreeWidget::itemChanged,
54✔
199
            this, &TableDesignerWidget::onComputersTreeItemEdited);
36✔
200
    connect(ui->group_mode_toggle_btn, &QPushButton::toggled,
54✔
201
            this, &TableDesignerWidget::onGroupModeToggled);
36✔
202

203
    // Build signals
204
    connect(ui->build_table_btn, &QPushButton::clicked,
54✔
205
            this, &TableDesignerWidget::onBuildTable);
36✔
206
    // Transform apply handled via TableTransformWidget
207
    // Export handled via TableExportWidget
208

209
    // Subscribe to DataManager table observer
210
    if (_data_manager) {
18✔
211
        auto token = _data_manager->addTableObserver([this](TableEvent const & ev) {
36✔
212
            switch (ev.type) {
30✔
213
                case TableEventType::Created:
13✔
214
                    this->onTableManagerTableCreated(QString::fromStdString(ev.tableId));
13✔
215
                    break;
13✔
216
                case TableEventType::Removed:
×
UNCOV
217
                    this->onTableManagerTableRemoved(QString::fromStdString(ev.tableId));
×
UNCOV
218
                    break;
×
219
                case TableEventType::InfoUpdated:
13✔
220
                    this->onTableManagerTableInfoUpdated(QString::fromStdString(ev.tableId));
13✔
221
                    break;
13✔
222
                case TableEventType::DataChanged:
4✔
223
                    // No direct UI change needed here for designer list
224
                    break;
4✔
225
            }
226
        });
48✔
227
        (void) token;// Optionally store and remove on dtor
228
    }
229
}
18✔
230

231
void TableDesignerWidget::onTableSelectionChanged() {
45✔
232
    int current_index = ui->table_combo->currentIndex();
45✔
233
    if (current_index < 0) {
45✔
234
        clearUI();
13✔
235
        return;
13✔
236
    }
237

238
    QString table_id = ui->table_combo->itemData(current_index).toString();
32✔
239
    if (table_id.isEmpty()) {
32✔
240
        clearUI();
18✔
241
        return;
18✔
242
    }
243

244
    _current_table_id = table_id;
14✔
245
    loadTableInfo(table_id);
14✔
246

247
    // Enable/disable controls
248
    ui->delete_table_btn->setEnabled(true);
14✔
249
    // Table info section is controlled separately
250
    ui->build_table_btn->setEnabled(true);
14✔
251
    if (auto gb = this->findChild<QGroupBox *>("row_source_group")) gb->setEnabled(true);
28✔
252
    if (auto gb = this->findChild<QGroupBox *>("column_design_group")) gb->setEnabled(true);
28✔
253
    // Enable save info within TableInfoWidget
254
    if (_table_info_section) _table_info_section->setEnabled(true);
14✔
255

256
    updateBuildStatus("Table selected: " + table_id);
14✔
257

258
    qDebug() << "Selected table:" << table_id;
14✔
259
}
32✔
260

261
void TableDesignerWidget::onCreateNewTable() {
×
UNCOV
262
    bool ok;
×
263
    QString name = QInputDialog::getText(this, "New Table", "Enter table name:", QLineEdit::Normal, "New Table", &ok);
×
264

UNCOV
265
    if (!ok || name.isEmpty()) {
×
UNCOV
266
        return;
×
267
    }
268

269
    auto * registry = _data_manager->getTableRegistry();
×
UNCOV
270
    if (!registry) { return; }
×
271
    auto table_id = registry->generateUniqueTableId("Table");
×
272

UNCOV
273
    if (registry->createTable(table_id, name.toStdString())) {
×
274
        // The combo will be refreshed by the signal handler
275
        // Set the new table as selected
276
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
277
            if (ui->table_combo->itemData(i).toString().toStdString() == table_id) {
×
UNCOV
278
                ui->table_combo->setCurrentIndex(i);
×
UNCOV
279
                break;
×
280
            }
281
        }
282
    } else {
283
        QMessageBox::warning(this, "Error", "Failed to create table with ID: " + QString::fromStdString(table_id));
×
284
    }
285
}
×
286

287
void TableDesignerWidget::onDeleteTable() {
×
UNCOV
288
    if (_current_table_id.isEmpty()) {
×
UNCOV
289
        return;
×
290
    }
291

292
    auto reply = QMessageBox::question(this, "Delete Table",
×
UNCOV
293
                                       QString("Are you sure you want to delete table '%1'?").arg(_current_table_id),
×
294
                                       QMessageBox::Yes | QMessageBox::No);
×
295

296
    if (reply == QMessageBox::Yes) {
×
UNCOV
297
        auto * registry = _data_manager->getTableRegistry();
×
298
        if (registry && registry->removeTable(_current_table_id.toStdString())) {
×
299
            // The combo will be refreshed by the signal handler
300
            clearUI();
×
301
        } else {
UNCOV
302
            QMessageBox::warning(this, "Error", "Failed to delete table: " + _current_table_id);
×
303
        }
304
    }
305
}
306

307
void TableDesignerWidget::onRowDataSourceChanged() {
48✔
308
    QString selected = ui->row_data_source_combo->currentText();
48✔
309
    if (selected.isEmpty()) {
48✔
310
        ui->row_info_label->setText("No row source selected");
17✔
311
        return;
17✔
312
    }
313

314
    // Save the row source selection to the current table
315
    // Only save if we have a current table and we're not loading table info
316
    if (!_current_table_id.isEmpty() && _data_manager) {
31✔
317
        if (auto * reg = _data_manager->getTableRegistry()) {
9✔
318
            reg->updateTableRowSource(_current_table_id.toStdString(), selected.toStdString());
9✔
319
        }
320
    }
321

322
    // Update the info label
323
    updateRowInfoLabel(selected);
31✔
324

325
    // Update interval settings visibility
326
    updateIntervalSettingsVisibility();
31✔
327

328
    // Refresh computers tree since available computers depend on row selector type
329
    refreshComputersTree();
31✔
330

331
    qDebug() << "Row data source changed to:" << selected;
31✔
332
    triggerPreviewDebounced();
31✔
333
}
48✔
334

335
void TableDesignerWidget::onCaptureRangeChanged() {
×
336
    // Update the info label to reflect the new capture range
337
    QString selected = ui->row_data_source_combo->currentText();
×
UNCOV
338
    if (!selected.isEmpty()) {
×
339
        updateRowInfoLabel(selected);
×
340
    }
UNCOV
341
    triggerPreviewDebounced();
×
UNCOV
342
}
×
343

344
void TableDesignerWidget::onIntervalSettingChanged() {
2✔
345
    // Update the info label to reflect the new interval setting
346
    QString selected = ui->row_data_source_combo->currentText();
2✔
347
    if (!selected.isEmpty()) {
2✔
348
        updateRowInfoLabel(selected);
2✔
349
    }
350

351
    // Update capture range visibility based on interval setting
352
    updateIntervalSettingsVisibility();
2✔
353
    triggerPreviewDebounced();
2✔
354
}
4✔
355

356

357
void TableDesignerWidget::onBuildTable() {
×
358
    if (_current_table_id.isEmpty()) {
×
UNCOV
359
        updateBuildStatus("No table selected", true);
×
UNCOV
360
        return;
×
361
    }
362

363
    QString row_source = ui->row_data_source_combo->currentText();
×
364
    if (row_source.isEmpty()) {
×
UNCOV
365
        updateBuildStatus("No row data source selected", true);
×
UNCOV
366
        return;
×
367
    }
368

369
    // Get enabled column infos from the tree
370
    auto column_infos = getEnabledColumnInfos();
×
371
    if (column_infos.empty()) {
×
UNCOV
372
        updateBuildStatus("No computers enabled. Check boxes in the tree to enable computers.", true);
×
UNCOV
373
        return;
×
374
    }
375

376
    try {
377
        // Create the row selector
378
        auto row_selector = createRowSelector(row_source);
×
379
        if (!row_selector) {
×
UNCOV
380
            updateBuildStatus("Failed to create row selector", true);
×
UNCOV
381
            return;
×
382
        }
383

384
        // Get the data manager extension
385
        auto * reg = _data_manager->getTableRegistry();
×
386
        auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
×
387
        if (!data_manager_extension) {
×
UNCOV
388
            updateBuildStatus("DataManager extension not available", true);
×
UNCOV
389
            return;
×
390
        }
391

392
        // Create the TableViewBuilder
UNCOV
393
        TableViewBuilder builder(data_manager_extension);
×
UNCOV
394
        builder.setRowSelector(std::move(row_selector));
×
395

396
        // Add all enabled columns from the tree
397
        bool all_columns_valid = true;
×
398
        for (auto const & column_info: column_infos) {
×
399
            if (!reg->addColumnToBuilder(builder, column_info)) {
×
400
                updateBuildStatus(QString("Failed to create column: %1").arg(QString::fromStdString(column_info.name)), true);
×
UNCOV
401
                all_columns_valid = false;
×
UNCOV
402
                break;
×
403
            }
404
        }
405

UNCOV
406
        if (!all_columns_valid) {
×
UNCOV
407
            return;
×
408
        }
409

410
        // Build the table
UNCOV
411
        auto table_view = builder.build();
×
412

413
        // Store the built table in the TableManager and update table info with current columns
414
        if (reg) {
×
415
            // Update table info with current column configuration
416
            auto table_info = reg->getTableInfo(_current_table_id.toStdString());
×
UNCOV
417
            table_info.columns = column_infos;// Store the current enabled columns
×
UNCOV
418
            reg->updateTableInfo(_current_table_id.toStdString(),
×
419
                                 table_info.name, table_info.description);
420

421
            // Store the built table
422
            if (reg->storeBuiltTable(_current_table_id.toStdString(), std::make_unique<TableView>(std::move(table_view)))) {
×
UNCOV
423
                updateBuildStatus(QString("Table built successfully with %1 columns!").arg(column_infos.size()));
×
424
                qDebug() << "Successfully built table:" << _current_table_id << "with" << column_infos.size() << "columns";
×
425
                // Populate JSON widget with the current configuration
426
                setJsonTemplateFromCurrentState();
×
427
            } else {
428
                updateBuildStatus("Failed to store built table", true);
×
429
            }
UNCOV
430
        } else {
×
UNCOV
431
            updateBuildStatus("Registry unavailable", true);
×
432
        }
433

434
    } catch (std::exception const & e) {
×
435
        updateBuildStatus(QString("Error building table: %1").arg(e.what()), true);
×
436
        qDebug() << "Exception during table building:" << e.what();
×
UNCOV
437
    }
×
UNCOV
438
}
×
439

440
bool TableDesignerWidget::buildTableFromTree() {
4✔
441
    // This is essentially the same as onBuildTable but returns success status
442
    if (_current_table_id.isEmpty()) {
4✔
UNCOV
443
        updateBuildStatus("No table selected", true);
×
UNCOV
444
        return false;
×
445
    }
446

447
    QString row_source = ui->row_data_source_combo->currentText();
4✔
448
    if (row_source.isEmpty()) {
4✔
UNCOV
449
        updateBuildStatus("No row data source selected", true);
×
UNCOV
450
        return false;
×
451
    }
452

453
    // Get enabled column infos from the tree
454
    auto column_infos = getEnabledColumnInfos();
4✔
455
    if (column_infos.empty()) {
4✔
UNCOV
456
        updateBuildStatus("No computers enabled. Check boxes in the tree to enable computers.", true);
×
UNCOV
457
        return false;
×
458
    }
459

460
    try {
461
        // Create the row selector
462
        auto row_selector = createRowSelector(row_source);
4✔
463
        if (!row_selector) {
4✔
UNCOV
464
            updateBuildStatus("Failed to create row selector", true);
×
UNCOV
465
            return false;
×
466
        }
467

468
        // Get the data manager extension
469
        auto * reg = _data_manager->getTableRegistry();
4✔
470
        auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
4✔
471
        if (!data_manager_extension) {
4✔
UNCOV
472
            updateBuildStatus("DataManager extension not available", true);
×
UNCOV
473
            return false;
×
474
        }
475

476
        // Create the TableViewBuilder
477
        TableViewBuilder builder(data_manager_extension);
4✔
478
        builder.setRowSelector(std::move(row_selector));
4✔
479

480
        // Add all enabled columns from the tree
481
        bool all_columns_valid = true;
4✔
482
        for (auto const & column_info: column_infos) {
10✔
483
            if (!reg->addColumnToBuilder(builder, column_info)) {
6✔
484
                updateBuildStatus(QString("Failed to create column: %1").arg(QString::fromStdString(column_info.name)), true);
×
UNCOV
485
                all_columns_valid = false;
×
UNCOV
486
                break;
×
487
            }
488
        }
489

490
        if (!all_columns_valid) {
4✔
UNCOV
491
            return false;
×
492
        }
493

494
        // Build the table
495
        auto table_view = builder.build();
4✔
496

497
        // Store the built table in the TableManager and update table info with current columns
498
        if (reg) {
4✔
499
            // Update table info with current column configuration
500
            auto table_info = reg->getTableInfo(_current_table_id.toStdString());
4✔
501
            table_info.columns = column_infos;// Store the current enabled columns
4✔
502
            reg->updateTableInfo(_current_table_id.toStdString(),
4✔
503
                                 table_info.name,
504
                                 table_info.description);
505

506
            // Store the built table
507
            if (reg->storeBuiltTable(_current_table_id.toStdString(), std::make_unique<TableView>(std::move(table_view)))) {
4✔
508
                updateBuildStatus(QString("Table built successfully with %1 columns!").arg(column_infos.size()));
4✔
509
                qDebug() << "Successfully built table:" << _current_table_id << "with" << column_infos.size() << "columns";
4✔
510
                // Populate JSON widget with the current configuration as well
511
                setJsonTemplateFromCurrentState();
4✔
512
                return true;
4✔
513
            } else {
UNCOV
514
                updateBuildStatus("Failed to store built table", true);
×
UNCOV
515
                return false;
×
516
            }
517
        } else {
4✔
UNCOV
518
            updateBuildStatus("Registry unavailable", true);
×
UNCOV
519
            return false;
×
520
        }
521

522
    } catch (std::exception const & e) {
4✔
523
        updateBuildStatus(QString("Error building table: %1").arg(e.what()), true);
×
524
        qDebug() << "Exception during table building:" << e.what();
×
UNCOV
525
        return false;
×
UNCOV
526
    }
×
527
}
4✔
528

529
void TableDesignerWidget::onApplyTransform() {
×
530
    if (_current_table_id.isEmpty() || !_data_manager) {
×
UNCOV
531
        updateBuildStatus("No base table selected", true);
×
UNCOV
532
        return;
×
533
    }
534

535
    // Fetch the built base table
536
    auto * reg = _data_manager->getTableRegistry();
×
537
    if (!reg) {
×
UNCOV
538
        updateBuildStatus("Registry unavailable", true);
×
539
        return;
×
540
    }
541
    auto base_view = reg->getBuiltTable(_current_table_id.toStdString());
×
542
    if (!base_view) {
×
UNCOV
543
        updateBuildStatus("Build the base table first", true);
×
UNCOV
544
        return;
×
545
    }
546

547
    // Currently only PCA option is exposed
548
    QString transform = _table_transform_widget ? _table_transform_widget->getTransformType() : QString();
×
549
    if (transform != "PCA") {
×
UNCOV
550
        updateBuildStatus("Unsupported transform", true);
×
UNCOV
551
        return;
×
552
    }
553

554
    // Configure PCA
555
    PCAConfig cfg;
×
556
    cfg.center = _table_transform_widget && _table_transform_widget->isCenterEnabled();
×
557
    cfg.standardize = _table_transform_widget && _table_transform_widget->isStandardizeEnabled();
×
558
    if (_table_transform_widget) {
×
UNCOV
559
        for (auto const & s: _table_transform_widget->getIncludeColumns()) cfg.include.push_back(s);
×
UNCOV
560
        for (auto const & s: _table_transform_widget->getExcludeColumns()) cfg.exclude.push_back(s);
×
561
    }
562

563
    try {
UNCOV
564
        PCATransform pca(cfg);
×
UNCOV
565
        TableView derived = pca.apply(*base_view);
×
566

567
        // Determine output id/name
568
        QString out_name = _table_transform_widget ? _table_transform_widget->getOutputName().trimmed() : QString();
×
569
        if (out_name.isEmpty()) {
×
570
            QString base = _table_info_widget ? _table_info_widget->getName() : QString();
×
UNCOV
571
            out_name = base.isEmpty() ? QString("(PCA)") : QString("%1 (PCA)").arg(base);
×
572
        }
×
573

574
        std::string out_id = reg->generateUniqueTableId((_current_table_id + "_pca").toStdString());
×
UNCOV
575
        if (!reg->createTable(out_id, out_name.toStdString())) {
×
576
            reg->updateTableInfo(out_id, out_name.toStdString(), "");
×
577
        }
578
        if (reg->storeBuiltTable(out_id, std::make_unique<TableView>(std::move(derived)))) {
×
UNCOV
579
            updateBuildStatus(QString("Created transformed table: %1").arg(out_name));
×
580
            refreshTableCombo();
×
581
        } else {
582
            updateBuildStatus("Failed to store transformed table", true);
×
583
        }
584
    } catch (std::exception const & e) {
×
585
        updateBuildStatus(QString("Transform failed: %1").arg(e.what()), true);
×
UNCOV
586
    }
×
587
}
×
588

589
std::vector<std::string> TableDesignerWidget::parseCommaSeparatedList(QString const & text) const {
×
590
    std::vector<std::string> out;
×
591
    for (QString s: text.split(",", Qt::SkipEmptyParts)) {
×
592
        s = s.trimmed();
×
593
        if (!s.isEmpty()) out.push_back(s.toStdString());
×
594
    }
×
UNCOV
595
    return out;
×
596
}
×
597

598
void TableDesignerWidget::onExportCsv() {
×
599
    if (_current_table_id.isEmpty() || !_data_manager) {
×
UNCOV
600
        updateBuildStatus("No table selected", true);
×
UNCOV
601
        return;
×
602
    }
603

604
    auto * reg = _data_manager->getTableRegistry();
×
605
    if (!reg) {
×
UNCOV
606
        updateBuildStatus("Registry unavailable", true);
×
607
        return;
×
608
    }
609
    auto view = reg->getBuiltTable(_current_table_id.toStdString());
×
610
    if (!view) {
×
UNCOV
611
        updateBuildStatus("Build the table first", true);
×
UNCOV
612
        return;
×
613
    }
614

615
    // CSV options from export widget
UNCOV
616
    QString delimiter = _table_export_widget ? _table_export_widget->getDelimiterText() : "Comma";
×
UNCOV
617
    QString lineEnding = _table_export_widget ? _table_export_widget->getLineEndingText() : "LF (\\n)";
×
618
    int precision = _table_export_widget ? _table_export_widget->getPrecision() : 3;
×
619
    bool includeHeader = _table_export_widget && _table_export_widget->isHeaderIncluded();
×
620
    bool exportByGroup = _table_export_widget && _table_export_widget->isExportByGroup();
×
621

UNCOV
622
    std::string delim = ",";
×
623
    if (delimiter == "Space") delim = " ";
×
624
    else if (delimiter == "Tab")
×
625
        delim = "\t";
×
626
    std::string eol = "\n";
×
627
    if (lineEnding.startsWith("CRLF")) eol = "\r\n";
×
628

UNCOV
629
    if (exportByGroup) {
×
630
        // Export separate files by group
631
        QString directory = promptSaveDirectoryForGroupExport();
×
632
        if (directory.isEmpty()) return;
×
633
        
634
        // Extract base name from current table ID
UNCOV
635
        QString base_name = _current_table_id;
×
636
        
637
        try {
638
            int files_exported = exportTableByGroups(view.get(), directory, base_name, delim, eol, precision, includeHeader);
×
639
            
640
            if (files_exported > 0) {
×
641
                updateBuildStatus(QString("Exported %1 CSV files to: %2").arg(files_exported).arg(directory));
×
642
            } else {
643
                // Error message already set by exportTableByGroups
644
                // updateBuildStatus("No groups found or export failed", true);
645
            }
646
        } catch (std::exception const & e) {
×
647
            updateBuildStatus(QString("Export by group failed: %1").arg(e.what()), true);
×
648
        }
×
649
    } else {
×
650
        // Export single file
UNCOV
651
        QString filename = promptSaveCsvFilename();
×
UNCOV
652
        if (filename.isEmpty()) return;
×
653
        if (!filename.endsWith(".csv", Qt::CaseInsensitive)) filename += ".csv";
×
654
        
UNCOV
655
        if (exportTableToSingleCsv(view.get(), filename, delim, eol, precision, includeHeader)) {
×
UNCOV
656
            updateBuildStatus(QString("Exported CSV: %1").arg(filename));
×
657
        } else {
658
            updateBuildStatus("Export failed", true);
×
659
        }
660
    }
×
UNCOV
661
}
×
662

UNCOV
663
QString TableDesignerWidget::promptSaveCsvFilename() const {
×
664
    return QFileDialog::getSaveFileName(const_cast<TableDesignerWidget *>(this), "Export Table to CSV", QString(), "CSV Files (*.csv)");
×
665
}
666

UNCOV
667
QString TableDesignerWidget::promptSaveDirectoryForGroupExport() const {
×
668
    return QFileDialog::getExistingDirectory(const_cast<TableDesignerWidget *>(this), "Select Directory for Group CSV Export");
×
669
}
670

UNCOV
671
bool TableDesignerWidget::exportTableToSingleCsv(TableView * view, QString const & filename,
×
672
                                                 std::string const & delim, std::string const & eol,
673
                                                 int precision, bool includeHeader) {
674
    try {
675
        std::ofstream file(filename.toStdString());
×
UNCOV
676
        if (!file.is_open()) {
×
677
            return false;
×
678
        }
679
        file << std::fixed << std::setprecision(precision);
×
680

681
        auto names = view->getColumnNames();
×
UNCOV
682
        if (includeHeader) {
×
UNCOV
683
            for (size_t i = 0; i < names.size(); ++i) {
×
UNCOV
684
                if (i > 0) file << delim;
×
UNCOV
685
                file << names[i];
×
686
            }
687
            file << eol;
×
688
        }
689
        
UNCOV
690
        size_t rows = view->getRowCount();
×
UNCOV
691
        for (size_t r = 0; r < rows; ++r) {
×
692
            for (size_t c = 0; c < names.size(); ++c) {
×
693
                if (c > 0) file << delim;
×
694

695
                // Use efficient visitColumnData approach instead of try/catch
696
                try {
UNCOV
697
                    view->visitColumnData(names[c], [&file, r, precision, this](auto const & vec) {
×
698
                        using VecT = std::decay_t<decltype(vec)>;
699
                        using ElemT = typename VecT::value_type;
700

701
                        if (r >= vec.size()) {
×
702
                            if constexpr (std::is_same_v<ElemT, double>) file << "NaN";
×
703
                            else if constexpr (std::is_same_v<ElemT, float>)
UNCOV
704
                                file << "NaN";
×
705
                            else if constexpr (std::is_same_v<ElemT, int>)
706
                                file << "NaN";
×
707
                            else if constexpr (std::is_same_v<ElemT, int64_t>)
UNCOV
708
                                file << "NaN";
×
709
                            else if constexpr (std::is_same_v<ElemT, bool>)
710
                                file << "false";
×
711
                            else
712
                                file << "N/A";
×
713
                            return;
×
714
                        }
715

716
                        if constexpr (std::is_same_v<ElemT, double>) {
717
                            file << std::fixed << std::setprecision(precision) << vec[r];
×
718
                        } else if constexpr (std::is_same_v<ElemT, float>) {
719
                            file << std::fixed << std::setprecision(precision) << vec[r];
×
720
                        } else if constexpr (std::is_same_v<ElemT, int>) {
721
                            file << vec[r];
×
722
                        } else if constexpr (std::is_same_v<ElemT, int64_t>) {
723
                            file << static_cast<int64_t>(vec[r]);
×
724
                        } else if constexpr (std::is_same_v<ElemT, bool>) {
725
                            file << (vec[r] ? 1 : 0);
×
726
                        } else if constexpr (
727
                                std::is_same_v<ElemT, std::vector<double>> ||
728
                                std::is_same_v<ElemT, std::vector<int>> ||
729
                                std::is_same_v<ElemT, std::vector<float>>) {
730
                            // Format vector data as comma-separated values
UNCOV
731
                            formatVectorForCsv(file, vec[r], precision);
×
732
                        } else {
733
                            file << "?";
×
734
                        }
735
                    });
736
                } catch (...) {
×
737
                    file << "Error";
×
738
                }
×
739
            }
UNCOV
740
            file << eol;
×
741
        }
742
        file.close();
×
UNCOV
743
        return true;
×
744
    } catch (std::exception const & e) {
×
UNCOV
745
        qWarning() << "Export failed:" << e.what();
×
746
        return false;
×
747
    }
×
748
}
749

UNCOV
750
int TableDesignerWidget::exportTableByGroups(TableView * view, QString const & directory,
×
751
                                            QString const & base_name,
752
                                            std::string const & delim, std::string const & eol,
753
                                            int precision, bool includeHeader) {
UNCOV
754
    if (!view || !_data_manager) {
×
755
        return 0;
×
756
    }
757

758
    // Get the entity group manager
UNCOV
759
    auto * group_manager = _data_manager->getEntityGroupManager();
×
UNCOV
760
    if (!group_manager) {
×
UNCOV
761
        qWarning() << "EntityGroupManager not available";
×
762
        return 0;
×
763
    }
764

765
    // Get all groups
766
    auto group_descriptors = group_manager->getAllGroupDescriptors();
×
UNCOV
767
    if (group_descriptors.empty()) {
×
768
        qWarning() << "No entity groups defined";
×
769
        updateBuildStatus("Cannot export by group: No entity groups defined", true);
×
UNCOV
770
        return 0;
×
771
    }
772

773
    // Materialize all columns first - this ensures entity IDs are computed and stored
774
    try {
UNCOV
775
        view->materializeAll();
×
UNCOV
776
    } catch (std::exception const & e) {
×
UNCOV
777
        qWarning() << "Failed to materialize table:" << e.what();
×
UNCOV
778
        updateBuildStatus(QString("Cannot export by group: Failed to materialize table: %1").arg(e.what()), true);
×
UNCOV
779
        return 0;
×
UNCOV
780
    }
×
781

782
    // Check if table has entity information
UNCOV
783
    if (!view->hasEntityColumn()) {
×
UNCOV
784
        qWarning() << "Table does not have entity information";
×
UNCOV
785
        updateBuildStatus("Cannot export by group: Table does not have entity information", true);
×
UNCOV
786
        return 0;
×
787
    }
788

789
    // Get all entity IDs for all rows
UNCOV
790
    auto all_entity_ids = view->getEntityIds();
×
UNCOV
791
    if (all_entity_ids.empty() || all_entity_ids.size() != view->getRowCount()) {
×
UNCOV
792
        qWarning() << "Entity IDs not available or mismatch with row count";
×
UNCOV
793
        updateBuildStatus("Cannot export by group: Entity IDs incomplete or unavailable", true);
×
UNCOV
794
        return 0;
×
795
    }
796

797
    int files_exported = 0;
×
798

799
    // For each group, find matching rows and export
UNCOV
800
    for (auto const & group_desc : group_descriptors) {
×
801
        // Get entities in this group
UNCOV
802
        auto group_entities = group_manager->getEntitiesInGroup(group_desc.id);
×
UNCOV
803
        if (group_entities.empty()) {
×
UNCOV
804
            continue;
×
805
        }
806

807
        // Convert to unordered_set for fast lookup
UNCOV
808
        std::unordered_set<EntityId> group_entity_set(group_entities.begin(), group_entities.end());
×
809

810
        // Find rows that have any entity in this group
811
        std::vector<size_t> matching_rows;
×
812
        for (size_t r = 0; r < all_entity_ids.size(); ++r) {
×
813
            // Check if any of the row's entities are in this group
UNCOV
814
            bool has_match = false;
×
UNCOV
815
            for (auto const & entity_id : all_entity_ids[r]) {
×
UNCOV
816
                if (group_entity_set.count(entity_id) > 0) {
×
UNCOV
817
                    has_match = true;
×
UNCOV
818
                    break;
×
819
                }
820
            }
UNCOV
821
            if (has_match) {
×
UNCOV
822
                matching_rows.push_back(r);
×
823
            }
824
        }
825

826
        // Skip if no matching rows
827
        if (matching_rows.empty()) {
×
UNCOV
828
            continue;
×
829
        }
830

831
        // Create filename for this group
UNCOV
832
        QString group_name = QString::fromStdString(group_desc.name);
×
833
        // Sanitize group name for filename (replace invalid characters)
UNCOV
834
        group_name = group_name.replace(QRegularExpression("[^a-zA-Z0-9_\\-]"), "_");
×
UNCOV
835
        QString filename = QString("%1/%2_%3.csv").arg(directory).arg(base_name).arg(group_name);
×
836

837
        // Export this group to file
838
        try {
UNCOV
839
            std::ofstream file(filename.toStdString());
×
UNCOV
840
            if (!file.is_open()) {
×
UNCOV
841
                qWarning() << "Could not open file:" << filename;
×
UNCOV
842
                continue;
×
843
            }
UNCOV
844
            file << std::fixed << std::setprecision(precision);
×
845

UNCOV
846
            auto names = view->getColumnNames();
×
UNCOV
847
            if (includeHeader) {
×
UNCOV
848
                for (size_t i = 0; i < names.size(); ++i) {
×
UNCOV
849
                    if (i > 0) file << delim;
×
UNCOV
850
                    file << names[i];
×
851
                }
UNCOV
852
                file << eol;
×
853
            }
854

855
            // Write only matching rows
UNCOV
856
            for (size_t r : matching_rows) {
×
UNCOV
857
                for (size_t c = 0; c < names.size(); ++c) {
×
UNCOV
858
                    if (c > 0) file << delim;
×
859

860
                    try {
UNCOV
861
                        view->visitColumnData(names[c], [&file, r, precision, this](auto const & vec) {
×
862
                            using VecT = std::decay_t<decltype(vec)>;
863
                            using ElemT = typename VecT::value_type;
864

UNCOV
865
                            if (r >= vec.size()) {
×
UNCOV
866
                                if constexpr (std::is_same_v<ElemT, double>) file << "NaN";
×
867
                                else if constexpr (std::is_same_v<ElemT, float>)
UNCOV
868
                                    file << "NaN";
×
869
                                else if constexpr (std::is_same_v<ElemT, int>)
UNCOV
870
                                    file << "NaN";
×
871
                                else if constexpr (std::is_same_v<ElemT, int64_t>)
UNCOV
872
                                    file << "NaN";
×
873
                                else if constexpr (std::is_same_v<ElemT, bool>)
UNCOV
874
                                    file << "false";
×
875
                                else
UNCOV
876
                                    file << "N/A";
×
UNCOV
877
                                return;
×
878
                            }
879

880
                            if constexpr (std::is_same_v<ElemT, double>) {
UNCOV
881
                                file << std::fixed << std::setprecision(precision) << vec[r];
×
882
                            } else if constexpr (std::is_same_v<ElemT, float>) {
UNCOV
883
                                file << std::fixed << std::setprecision(precision) << vec[r];
×
884
                            } else if constexpr (std::is_same_v<ElemT, int>) {
UNCOV
885
                                file << vec[r];
×
886
                            } else if constexpr (std::is_same_v<ElemT, int64_t>) {
UNCOV
887
                                file << static_cast<int64_t>(vec[r]);
×
888
                            } else if constexpr (std::is_same_v<ElemT, bool>) {
UNCOV
889
                                file << (vec[r] ? 1 : 0);
×
890
                            } else if constexpr (
891
                                    std::is_same_v<ElemT, std::vector<double>> ||
892
                                    std::is_same_v<ElemT, std::vector<int>> ||
893
                                    std::is_same_v<ElemT, std::vector<float>>) {
UNCOV
894
                                formatVectorForCsv(file, vec[r], precision);
×
895
                            } else {
UNCOV
896
                                file << "?";
×
897
                            }
898
                        });
UNCOV
899
                    } catch (...) {
×
UNCOV
900
                        file << "Error";
×
UNCOV
901
                    }
×
902
                }
UNCOV
903
                file << eol;
×
904
            }
905
            
UNCOV
906
            file.close();
×
UNCOV
907
            files_exported++;
×
908
            
UNCOV
909
        } catch (std::exception const & e) {
×
910
            qWarning() << "Failed to export group" << group_name << ":" << e.what();
×
UNCOV
911
        }
×
UNCOV
912
    }
×
913

UNCOV
914
    return files_exported;
×
UNCOV
915
}
×
916

917
template<typename T>
UNCOV
918
void TableDesignerWidget::formatVectorForCsv(std::ofstream & file, std::vector<T> const & values, int precision) {
×
UNCOV
919
    if (values.empty()) {
×
920
        file << "[]";
×
921
        return;
×
922
    }
923

UNCOV
924
    file << "[";
×
UNCOV
925
    for (size_t i = 0; i < values.size(); ++i) {
×
926
        if constexpr (std::is_same_v<T, double> || std::is_same_v<T, float>) {
927
            file << std::fixed << std::setprecision(precision) << values[i];
×
928
        } else {
UNCOV
929
            file << values[i];
×
930
        }
UNCOV
931
        if (i + 1 < values.size()) file << ",";
×
932
    }
933
    file << "]";
×
934
}
935

UNCOV
936
void TableDesignerWidget::onSaveTableInfo() {
×
UNCOV
937
    if (_current_table_id.isEmpty()) {
×
UNCOV
938
        return;
×
939
    }
940

UNCOV
941
    QString name = _table_info_widget ? _table_info_widget->getName() : QString();
×
UNCOV
942
    QString description = _table_info_widget ? _table_info_widget->getDescription() : QString();
×
943

UNCOV
944
    if (name.isEmpty()) {
×
UNCOV
945
        QMessageBox::warning(this, "Error", "Table name cannot be empty");
×
UNCOV
946
        return;
×
947
    }
948

UNCOV
949
    if (auto * reg = _data_manager->getTableRegistry(); reg && reg->updateTableInfo(_current_table_id.toStdString(), name.toStdString(), description.toStdString())) {
×
UNCOV
950
        updateBuildStatus("Table information saved");
×
951
        // Refresh the combo to show updated name
UNCOV
952
        refreshTableCombo();
×
953
        // Restore selection
UNCOV
954
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
UNCOV
955
            if (ui->table_combo->itemData(i).toString() == _current_table_id) {
×
UNCOV
956
                ui->table_combo->setCurrentIndex(i);
×
UNCOV
957
                break;
×
958
            }
959
        }
960
    } else {
UNCOV
961
        QMessageBox::warning(this, "Error", "Failed to save table information");
×
962
    }
UNCOV
963
}
×
964

965
void TableDesignerWidget::onTableManagerTableCreated(QString const & table_id) {
13✔
966
    refreshTableCombo();
13✔
967
    qDebug() << "Table created signal received:" << table_id;
13✔
968
}
13✔
969

UNCOV
970
void TableDesignerWidget::onTableManagerTableRemoved(QString const & table_id) {
×
UNCOV
971
    refreshTableCombo();
×
UNCOV
972
    if (_current_table_id == table_id) {
×
UNCOV
973
        _current_table_id.clear();
×
UNCOV
974
        clearUI();
×
975
    }
UNCOV
976
    qDebug() << "Table removed signal received:" << table_id;
×
UNCOV
977
}
×
978

979
void TableDesignerWidget::onTableManagerTableInfoUpdated(QString const & table_id) {
13✔
980
    if (_current_table_id == table_id && !_loading_column_configuration) {
13✔
981
        loadTableInfo(table_id);
13✔
982
    }
983
    qDebug() << "Table info updated signal received:" << table_id;
13✔
984
}
13✔
985

986
void TableDesignerWidget::refreshTableCombo() {
31✔
987
    ui->table_combo->clear();
31✔
988

989
    auto * reg = _data_manager->getTableRegistry();
31✔
990
    auto table_infos = reg ? reg->getAllTableInfo() : std::vector<TableInfo>{};
31✔
991
    for (auto const & info: table_infos) {
47✔
992
        ui->table_combo->addItem(QString::fromStdString(info.name), QString::fromStdString(info.id));
16✔
993
    }
994

995
    if (ui->table_combo->count() == 0) {
31✔
996
        ui->table_combo->addItem("(No tables available)", "");
18✔
997
    }
998
}
62✔
999

1000
void TableDesignerWidget::refreshRowDataSourceCombo() {
22✔
1001
    ui->row_data_source_combo->clear();
22✔
1002

1003
    if (!_data_manager) {
22✔
UNCOV
1004
        qDebug() << "refreshRowDataSourceCombo: No table manager";
×
UNCOV
1005
        return;
×
1006
    }
1007

1008
    auto data_sources = getAvailableDataSources();
22✔
1009
    qDebug() << "refreshRowDataSourceCombo: Found" << data_sources.size() << "data sources:" << data_sources;
22✔
1010

1011
    for (QString const & source: data_sources) {
248✔
1012
        // Only include valid row sources in this combo
1013
        if (source.startsWith("TimeFrame: ") || source.startsWith("Events: ") || source.startsWith("Intervals: ")) {
226✔
1014
            ui->row_data_source_combo->addItem(source);
160✔
1015
        }
1016
    }
1017

1018
    if (ui->row_data_source_combo->count() == 0) {
22✔
UNCOV
1019
        ui->row_data_source_combo->addItem("(No data sources available)");
×
UNCOV
1020
        qDebug() << "refreshRowDataSourceCombo: No data sources available";
×
1021
    }
1022
}
22✔
1023

1024

1025
void TableDesignerWidget::loadTableInfo(QString const & table_id) {
27✔
1026
    if (table_id.isEmpty() || !_data_manager) {
27✔
UNCOV
1027
        clearUI();
×
UNCOV
1028
        return;
×
1029
    }
1030

1031
    auto * reg = _data_manager->getTableRegistry();
27✔
1032
    auto info = reg ? reg->getTableInfo(table_id.toStdString()) : TableInfo{};
27✔
1033
    if (info.id.empty()) {
27✔
UNCOV
1034
        clearUI();
×
UNCOV
1035
        return;
×
1036
    }
1037

1038
    // Load table information
1039
    if (_table_info_widget) {
27✔
1040
        _table_info_widget->setName(QString::fromStdString(info.name));
27✔
1041
        _table_info_widget->setDescription(QString::fromStdString(info.description));
27✔
1042
    }
1043

1044
    // Load row source if available
1045
    if (!info.rowSourceName.empty()) {
27✔
1046
        int row_index = ui->row_data_source_combo->findText(QString::fromStdString(info.rowSourceName));
14✔
1047
        if (row_index >= 0) {
14✔
1048
            // Block signals to prevent circular dependency when loading table info
1049
            ui->row_data_source_combo->blockSignals(true);
14✔
1050
            ui->row_data_source_combo->setCurrentIndex(row_index);
14✔
1051
            ui->row_data_source_combo->blockSignals(false);
14✔
1052

1053
            // Manually update the info label without triggering the signal handler
1054
            updateRowInfoLabel(QString::fromStdString(info.rowSourceName));
14✔
1055

1056
            // Update interval settings visibility
1057
            updateIntervalSettingsVisibility();
14✔
1058

1059
            // Since signals were blocked, this will ensure the tree is refreshed
1060
            // when the computers tree is populated later in this function
1061
        }
1062
    }
1063

1064
    // Clear old column list (deprecated)
1065
    // The computers tree will be populated based on available data sources
1066
    refreshComputersTree();
27✔
1067

1068
    updateBuildStatus(QString("Loaded table: %1").arg(QString::fromStdString(info.name)));
27✔
1069
    triggerPreviewDebounced();
27✔
1070
}
27✔
1071

1072
void TableDesignerWidget::clearUI() {
49✔
1073
    _current_table_id.clear();
49✔
1074

1075
    // Clear table info
1076
    if (_table_info_widget) {
49✔
1077
        _table_info_widget->setName("");
49✔
1078
        _table_info_widget->setDescription("");
49✔
1079
    }
1080

1081
    // Clear row source
1082
    ui->row_data_source_combo->setCurrentIndex(-1);
49✔
1083
    ui->row_info_label->setText("No row source selected");
49✔
1084

1085
    // Reset capture range and interval settings
1086
    setCaptureRange(30000);// Default value
49✔
1087
    if (ui->interval_beginning_radio) {
49✔
1088
        ui->interval_beginning_radio->setChecked(true);
49✔
1089
    }
1090
    if (ui->interval_itself_radio) {
49✔
1091
        ui->interval_itself_radio->setChecked(false);
49✔
1092
    }
1093
    if (ui->interval_settings_group) {
49✔
1094
        ui->interval_settings_group->setVisible(false);
49✔
1095
    }
1096

1097
    // Clear computers tree
1098
    if (ui->computers_tree) {
49✔
1099
        ui->computers_tree->clear();
49✔
1100
    }
1101

1102
    // Disable controls
1103
    ui->delete_table_btn->setEnabled(false);
49✔
1104
    // Table info section is controlled separately
1105
    ui->build_table_btn->setEnabled(false);
49✔
1106
    if (auto gb = this->findChild<QGroupBox *>("row_source_group")) gb->setEnabled(false);
98✔
1107
    if (auto gb = this->findChild<QGroupBox *>("column_design_group")) gb->setEnabled(false);
98✔
1108
    if (_table_info_section) _table_info_section->setEnabled(false);
49✔
1109

1110
    updateBuildStatus("No table selected");
49✔
1111
    if (_table_viewer) _table_viewer->clearTable();
49✔
1112
}
49✔
1113

1114
void TableDesignerWidget::updateBuildStatus(QString const & message, bool is_error) {
94✔
1115
    ui->build_status_label->setText(message);
94✔
1116

1117
    if (is_error) {
94✔
UNCOV
1118
        ui->build_status_label->setStyleSheet("QLabel { color: red; font-weight: bold; }");
×
1119
    } else {
1120
        ui->build_status_label->setStyleSheet("QLabel { color: green; }");
94✔
1121
    }
1122
}
94✔
1123

1124
QStringList TableDesignerWidget::getAvailableDataSources() const {
106✔
1125
    QStringList sources;
106✔
1126

1127
    if (!_data_manager) {
106✔
UNCOV
1128
        qDebug() << "getAvailableDataSources: No table manager";
×
UNCOV
1129
        return sources;
×
1130
    }
1131

1132
    auto * reg = _data_manager->getTableRegistry();
106✔
1133
    auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
106✔
1134
    if (!data_manager_extension) {
106✔
UNCOV
1135
        qDebug() << "getAvailableDataSources: No data manager extension";
×
UNCOV
1136
        return sources;
×
1137
    }
1138

1139
    if (!_data_manager) {
106✔
UNCOV
1140
        qDebug() << "getAvailableDataSources: No data manager";
×
UNCOV
1141
        return sources;
×
1142
    }
1143

1144
    // Add TimeFrame keys as potential row sources
1145
    // TimeFrames can define intervals for analysis
1146
    auto timeframe_keys = _data_manager->getTimeFrameKeys();
106✔
1147
    qDebug() << "getAvailableDataSources: TimeFrame keys:" << timeframe_keys.size();
106✔
1148
    for (auto const & key: timeframe_keys) {
539✔
1149
        QString source = QString("TimeFrame: %1").arg(QString::fromStdString(key.str()));
433✔
1150
        sources << source;
433✔
1151
        qDebug() << "  Added TimeFrame:" << source;
433✔
1152
    }
433✔
1153

1154
    // Add DigitalEventSeries keys as potential row sources
1155
    // Events can be used to define analysis windows or timestamps
1156
    auto event_keys = _data_manager->getKeys<DigitalEventSeries>();
106✔
1157
    qDebug() << "getAvailableDataSources: Event keys:" << event_keys.size();
106✔
1158
    for (auto const & key: event_keys) {
321✔
1159
        QString source = QString("Events: %1").arg(QString::fromStdString(key));
215✔
1160
        sources << source;
215✔
1161
        qDebug() << "  Added Events:" << source;
215✔
1162
    }
215✔
1163

1164
    // Add DigitalIntervalSeries keys as potential row sources
1165
    // Intervals directly define analysis windows
1166
    auto interval_keys = _data_manager->getKeys<DigitalIntervalSeries>();
106✔
1167
    qDebug() << "getAvailableDataSources: Interval keys:" << interval_keys.size();
106✔
1168
    for (auto const & key: interval_keys) {
227✔
1169
        QString source = QString("Intervals: %1").arg(QString::fromStdString(key));
121✔
1170
        sources << source;
121✔
1171
        qDebug() << "  Added Intervals:" << source;
121✔
1172
    }
121✔
1173

1174
    // Add AnalogTimeSeries keys as data sources (for computers; not row selectors)
1175
    auto analog_keys = _data_manager->getKeys<AnalogTimeSeries>();
106✔
1176
    qDebug() << "getAvailableDataSources: Analog keys:" << analog_keys.size();
106✔
1177
    for (auto const & key: analog_keys) {
318✔
1178
        QString source = QString("analog:%1").arg(QString::fromStdString(key));
212✔
1179
        sources << source;
212✔
1180
        qDebug() << "  Added Analog:" << source;
212✔
1181
    }
212✔
1182

1183
    // Add LineData keys as data sources (for computers; not row selectors)
1184
    auto line_keys = _data_manager->getKeys<LineData>();
106✔
1185
    qDebug() << "getAvailableDataSources: Line keys:" << line_keys.size();
106✔
1186
    for (auto const & key: line_keys) {
212✔
1187
        QString source = QString("lines:%1").arg(QString::fromStdString(key));
106✔
1188
        sources << source;
106✔
1189
        qDebug() << "  Added Lines:" << source;
106✔
1190
    }
106✔
1191

1192
    qDebug() << "getAvailableDataSources: Total sources found:" << sources.size();
106✔
1193

1194
    return sources;
106✔
1195
}
106✔
1196

1197
std::pair<std::optional<DataSourceVariant>, RowSelectorType>
1198
TableDesignerWidget::createDataSourceVariant(QString const & data_source_string,
861✔
1199
                                             std::shared_ptr<DataManagerExtension> data_manager_extension) const {
1200
    std::optional<DataSourceVariant> result;
861✔
1201
    RowSelectorType row_selector_type = RowSelectorType::IntervalBased;
861✔
1202

1203
    if (data_source_string.startsWith("TimeFrame: ")) {
861✔
1204
        // TimeFrames are used with TimestampSelector for rows; no concrete data source needed
1205
        row_selector_type = RowSelectorType::Timestamp;
343✔
1206

1207
    } else if (data_source_string.startsWith("Events: ")) {
518✔
1208
        QString source_name = data_source_string.mid(8);// Remove "Events: " prefix
170✔
1209
        // Event-based computers in the registry operate with interval rows
1210
        row_selector_type = RowSelectorType::IntervalBased;
170✔
1211

1212
        if (auto event_source = data_manager_extension->getEventSource(source_name.toStdString())) {
170✔
1213
            result = event_source;
170✔
1214
        }
170✔
1215

1216
    } else if (data_source_string.startsWith("Intervals: ")) {
518✔
1217
        QString source_name = data_source_string.mid(11);// Remove "Intervals: " prefix
96✔
1218
        row_selector_type = RowSelectorType::IntervalBased;
96✔
1219

1220
        if (auto interval_source = data_manager_extension->getIntervalSource(source_name.toStdString())) {
96✔
1221
            result = interval_source;
96✔
1222
        }
96✔
1223
    } else if (data_source_string.startsWith("analog:")) {
348✔
1224
        QString source_name = data_source_string.mid(7);// Remove "analog:" prefix
168✔
1225
        row_selector_type = RowSelectorType::IntervalBased;
168✔
1226

1227
        if (auto analog_source = data_manager_extension->getAnalogSource(source_name.toStdString())) {
168✔
1228
            result = analog_source;
168✔
1229
        }
168✔
1230
    } else if (data_source_string.startsWith("lines:")) {
252✔
1231
        QString source_name = data_source_string.mid(6);// Remove "lines:" prefix
84✔
1232
        row_selector_type = RowSelectorType::Timestamp; // LineData computers work with timestamp row selectors
84✔
1233

1234
        if (auto line_source = data_manager_extension->getLineSource(source_name.toStdString())) {
84✔
1235
            result = line_source;
84✔
1236
        }
84✔
1237
    }
84✔
1238

1239
    return {result, row_selector_type};
1,722✔
1240
}
861✔
1241

1242
void TableDesignerWidget::updateRowInfoLabel(QString const & selected_source) {
47✔
1243
    if (selected_source.isEmpty()) {
47✔
UNCOV
1244
        ui->row_info_label->setText("No row source selected");
×
UNCOV
1245
        return;
×
1246
    }
1247

1248
    // Parse the selected source to get type and name
1249
    QString source_type;
47✔
1250
    QString source_name;
47✔
1251

1252
    if (selected_source.startsWith("TimeFrame: ")) {
47✔
1253
        source_type = "TimeFrame";
25✔
1254
        source_name = selected_source.mid(11);// Remove "TimeFrame: " prefix
25✔
1255
    } else if (selected_source.startsWith("Events: ")) {
22✔
1256
        source_type = "Events";
×
1257
        source_name = selected_source.mid(8);// Remove "Events: " prefix
×
1258
    } else if (selected_source.startsWith("Intervals: ")) {
22✔
1259
        source_type = "Intervals";
22✔
1260
        source_name = selected_source.mid(11);// Remove "Intervals: " prefix
22✔
1261
    }
1262

1263
    // Get additional information about the selected source
1264
    QString info_text = QString("Selected: %1 (%2)").arg(source_name, source_type);
47✔
1265

1266
    if (!_data_manager) {
47✔
UNCOV
1267
        ui->row_info_label->setText(info_text);
×
UNCOV
1268
        return;
×
1269
    }
1270

1271
    auto * reg3 = _data_manager->getTableRegistry();
47✔
1272
    auto data_manager_extension = reg3 ? reg3->getDataManagerExtension() : nullptr;
47✔
1273
    if (!data_manager_extension) {
47✔
UNCOV
1274
        ui->row_info_label->setText(info_text);
×
UNCOV
1275
        return;
×
1276
    }
1277

1278
    auto const source_name_str = source_name.toStdString();
47✔
1279

1280
    // Add specific information based on source type
1281
    if (source_type == "TimeFrame") {
47✔
1282
        auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
25✔
1283
        if (timeframe) {
25✔
1284
            info_text += QString(" - %1 time points").arg(timeframe->getTotalFrameCount());
25✔
1285
        }
1286
    } else if (source_type == "Events") {
47✔
UNCOV
1287
        auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
UNCOV
1288
        if (event_series) {
×
UNCOV
1289
            auto events = event_series->getEventSeries();
×
UNCOV
1290
            info_text += QString(" - %1 events").arg(events.size());
×
UNCOV
1291
        }
×
1292
    } else if (source_type == "Intervals") {
22✔
1293
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
22✔
1294
        if (interval_series) {
22✔
1295
            auto intervals = interval_series->getDigitalIntervalSeries();
22✔
1296
            info_text += QString(" - %1 intervals").arg(intervals.size());
22✔
1297

1298
            // Add capture range and interval setting information
1299
            if (isIntervalItselfSelected()) {
22✔
1300
                info_text += QString("\nUsing intervals as-is (no capture range)");
2✔
1301
            } else {
1302
                int capture_range = getCaptureRange();
20✔
1303
                QString interval_point = isIntervalBeginningSelected() ? "beginning" : "end";
20✔
1304
                info_text += QString("\nCapture range: ±%1 samples around %2 of intervals").arg(capture_range).arg(interval_point);
20✔
1305
            }
20✔
1306
        }
22✔
1307
    }
22✔
1308

1309
    ui->row_info_label->setText(info_text);
47✔
1310
}
47✔
1311

1312
std::unique_ptr<IRowSelector> TableDesignerWidget::createRowSelector(QString const & row_source) {
24✔
1313
    // Parse the row source to get type and name
1314
    QString source_type;
24✔
1315
    QString source_name;
24✔
1316

1317
    if (row_source.startsWith("TimeFrame: ")) {
24✔
1318
        source_type = "TimeFrame";
4✔
1319
        source_name = row_source.mid(11);// Remove "TimeFrame: " prefix
4✔
1320
    } else if (row_source.startsWith("Events: ")) {
20✔
UNCOV
1321
        source_type = "Events";
×
UNCOV
1322
        source_name = row_source.mid(8);// Remove "Events: " prefix
×
1323
    } else if (row_source.startsWith("Intervals: ")) {
20✔
1324
        source_type = "Intervals";
20✔
1325
        source_name = row_source.mid(11);// Remove "Intervals: " prefix
20✔
1326
    } else {
UNCOV
1327
        qDebug() << "Unknown row source format:" << row_source;
×
UNCOV
1328
        return nullptr;
×
1329
    }
1330

1331
    auto const source_name_str = source_name.toStdString();
24✔
1332

1333
    try {
1334
        if (source_type == "TimeFrame") {
24✔
1335
            // Create IntervalSelector using TimeFrame
1336
            auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
4✔
1337
            if (!timeframe) {
4✔
1338
                qDebug() << "TimeFrame not found:" << source_name;
×
1339
                return nullptr;
×
1340
            }
1341

1342
            // Use timestamps to select all rows
1343
            std::vector<TimeFrameIndex> timestamps;
4✔
1344
            for (int64_t i = 0; i < timeframe->getTotalFrameCount(); ++i) {
408✔
1345
                timestamps.push_back(TimeFrameIndex(i));
404✔
1346
            }
1347
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe);
4✔
1348

1349
        } else if (source_type == "Events") {
24✔
1350
            // Create TimestampSelector using DigitalEventSeries
UNCOV
1351
            auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
UNCOV
1352
            if (!event_series) {
×
UNCOV
1353
                qDebug() << "DigitalEventSeries not found:" << source_name;
×
UNCOV
1354
                return nullptr;
×
1355
            }
1356

UNCOV
1357
            auto events = event_series->getEventSeries();
×
UNCOV
1358
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
×
UNCOV
1359
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
×
UNCOV
1360
            if (!timeframe_obj) {
×
UNCOV
1361
                qDebug() << "TimeFrame not found for events:" << timeframe_key.str();
×
UNCOV
1362
                return nullptr;
×
1363
            }
1364

1365
            // Convert events to TimeFrameIndex
UNCOV
1366
            std::vector<TimeFrameIndex> timestamps;
×
UNCOV
1367
            for (auto const & event: events) {
×
UNCOV
1368
                timestamps.push_back(TimeFrameIndex(static_cast<int64_t>(event)));
×
1369
            }
1370

UNCOV
1371
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe_obj);
×
1372

1373
        } else if (source_type == "Intervals") {
20✔
1374
            // Create IntervalSelector using DigitalIntervalSeries with capture range
1375
            auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
20✔
1376
            if (!interval_series) {
20✔
UNCOV
1377
                qDebug() << "DigitalIntervalSeries not found:" << source_name;
×
UNCOV
1378
                return nullptr;
×
1379
            }
1380

1381
            auto intervals = interval_series->getDigitalIntervalSeries();
20✔
1382
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
20✔
1383
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
20✔
1384
            if (!timeframe_obj) {
20✔
UNCOV
1385
                qDebug() << "TimeFrame not found for intervals:" << timeframe_key.str();
×
UNCOV
1386
                return nullptr;
×
1387
            }
1388

1389
            // Get capture range and interval setting
1390
            int capture_range = getCaptureRange();
20✔
1391
            bool use_beginning = isIntervalBeginningSelected();
20✔
1392
            bool use_interval_itself = isIntervalItselfSelected();
20✔
1393

1394
            // Create intervals based on the selected option
1395
            std::vector<TimeFrameInterval> tf_intervals;
20✔
1396
            for (auto const & interval: intervals) {
99✔
1397
                if (use_interval_itself) {
79✔
1398
                    // Use the interval as-is
1399
                    tf_intervals.emplace_back(TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
3✔
1400
                } else {
1401
                    // Determine the reference point (beginning or end of interval)
1402
                    int64_t reference_point;
1403
                    if (use_beginning) {
76✔
1404
                        reference_point = interval.start;
76✔
1405
                    } else {
1406
                        reference_point = interval.end;
×
1407
                    }
1408

1409
                    // Create a new interval around the reference point
1410
                    int64_t start_point = reference_point - capture_range;
76✔
1411
                    int64_t end_point = reference_point + capture_range;
76✔
1412

1413
                    // Ensure bounds are within the timeframe
1414
                    start_point = std::max(start_point, int64_t(0));
76✔
1415
                    end_point = std::min(end_point, static_cast<int64_t>(timeframe_obj->getTotalFrameCount() - 1));
76✔
1416

1417
                    tf_intervals.emplace_back(TimeFrameIndex(start_point), TimeFrameIndex(end_point));
76✔
1418
                }
1419
            }
1420

1421
            return std::make_unique<IntervalSelector>(std::move(tf_intervals), timeframe_obj);
20✔
1422
        }
20✔
1423

UNCOV
1424
    } catch (std::exception const & e) {
×
UNCOV
1425
        qDebug() << "Exception creating row selector:" << e.what();
×
UNCOV
1426
        return nullptr;
×
UNCOV
1427
    }
×
1428

UNCOV
1429
    qDebug() << "Unsupported row source type:" << source_type;
×
1430
    return nullptr;
×
1431
}
24✔
1432

UNCOV
1433
bool TableDesignerWidget::addColumnToBuilder(TableViewBuilder & builder, ColumnInfo const & column_info) {
×
1434
    // Use the simplified TableRegistry method that handles all the type checking internally
UNCOV
1435
    auto * reg = _data_manager->getTableRegistry();
×
1436
    if (!reg) {
×
1437
        qDebug() << "TableRegistry not available";
×
UNCOV
1438
        return false;
×
1439
    }
1440

UNCOV
1441
    bool success = reg->addColumnToBuilder(builder, column_info);
×
UNCOV
1442
    if (!success) {
×
UNCOV
1443
        qDebug() << "Failed to add column to builder:" << QString::fromStdString(column_info.name);
×
1444
    }
1445

UNCOV
1446
    return success;
×
1447
}
1448

1449
void TableDesignerWidget::updateIntervalSettingsVisibility() {
47✔
1450
    if (!ui->interval_settings_group) {
47✔
UNCOV
1451
        return;
×
1452
    }
1453

1454
    QString selected_key = ui->row_data_source_combo->currentText();
47✔
1455
    if (selected_key.isEmpty()) {
47✔
UNCOV
1456
        ui->interval_settings_group->setVisible(false);
×
UNCOV
1457
        if (ui->capture_range_spinbox) {
×
UNCOV
1458
            ui->capture_range_spinbox->setEnabled(false);
×
1459
        }
1460
        return;
×
1461
    }
1462

1463
    if (!_data_manager) {
47✔
UNCOV
1464
        ui->interval_settings_group->setVisible(false);
×
1465
        if (ui->capture_range_spinbox) {
×
1466
            ui->capture_range_spinbox->setEnabled(false);
×
1467
        }
UNCOV
1468
        return;
×
1469
    }
1470

1471
    // Check if the selected source is an interval series
1472
    if (selected_key.startsWith("Intervals: ")) {
47✔
1473
        ui->interval_settings_group->setVisible(true);
22✔
1474

1475
        // Enable/disable capture range based on interval setting
1476
        if (ui->capture_range_spinbox) {
22✔
1477
            bool use_interval_itself = isIntervalItselfSelected();
22✔
1478
            ui->capture_range_spinbox->setEnabled(!use_interval_itself);
22✔
1479
        }
1480
    } else {
1481
        ui->interval_settings_group->setVisible(false);
25✔
1482
        if (ui->capture_range_spinbox) {
25✔
1483
            ui->capture_range_spinbox->setEnabled(false);
25✔
1484
        }
1485
    }
1486
}
47✔
1487

1488
int TableDesignerWidget::getCaptureRange() const {
40✔
1489
    if (ui->capture_range_spinbox) {
40✔
1490
        return ui->capture_range_spinbox->value();
40✔
1491
    }
1492
    return 30000;// Default value
×
1493
}
1494

1495
void TableDesignerWidget::setCaptureRange(int value) {
49✔
1496
    if (ui->capture_range_spinbox) {
49✔
1497
        ui->capture_range_spinbox->blockSignals(true);
49✔
1498
        ui->capture_range_spinbox->setValue(value);
49✔
1499
        ui->capture_range_spinbox->blockSignals(false);
49✔
1500
    }
1501
}
49✔
1502

1503
bool TableDesignerWidget::isIntervalBeginningSelected() const {
40✔
1504
    if (ui->interval_beginning_radio) {
40✔
1505
        return ui->interval_beginning_radio->isChecked();
40✔
1506
    }
UNCOV
1507
    return true;// Default to beginning
×
1508
}
1509

1510
bool TableDesignerWidget::isIntervalItselfSelected() const {
64✔
1511
    if (ui->interval_itself_radio) {
64✔
1512
        return ui->interval_itself_radio->isChecked();
64✔
1513
    }
UNCOV
1514
    return false;// Default to not selected
×
1515
}
1516

1517
void TableDesignerWidget::triggerPreviewDebounced() {
163✔
1518
    if (_preview_debounce_timer) _preview_debounce_timer->start();
163✔
1519
    // Also trigger an immediate rebuild to support non-interactive/test contexts
1520
    rebuildPreviewNow();
163✔
1521
}
163✔
1522

1523
void TableDesignerWidget::rebuildPreviewNow() {
163✔
1524
    if (!_data_manager || !_table_viewer) return;
163✔
1525
    if (_current_table_id.isEmpty()) {
163✔
1526
        _table_viewer->clearTable();
73✔
1527
        return;
73✔
1528
    }
1529

1530
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
90✔
1531
    if (row_source.isEmpty()) {
90✔
1532
        _table_viewer->clearTable();
24✔
1533
        return;
24✔
1534
    }
1535

1536
    // Get enabled column infos from the computers tree
1537
    auto column_infos = getEnabledColumnInfos();
66✔
1538
    if (column_infos.empty()) {
66✔
1539
        _table_viewer->clearTable();
46✔
1540
        return;
46✔
1541
    }
1542

1543
    // Create row selector for the entire dataset
1544
    auto selector = createRowSelector(row_source);
20✔
1545
    if (!selector) {
20✔
UNCOV
1546
        _table_viewer->clearTable();
×
UNCOV
1547
        return;
×
1548
    }
1549

1550
    // Apply any saved column order for this table id
1551
    auto desiredOrder = _table_column_order.value(_current_table_id);
20✔
1552
    if (!desiredOrder.isEmpty()) {
20✔
1553
        std::vector<ColumnInfo> reordered;
12✔
1554
        reordered.reserve(column_infos.size());
12✔
1555
        for (auto const & name: desiredOrder) {
39✔
1556
            auto it = std::find_if(column_infos.begin(), column_infos.end(), [&](ColumnInfo const & ci) { return QString::fromStdString(ci.name) == name; });
61✔
1557
            if (it != column_infos.end()) {
27✔
1558
                reordered.push_back(*it);
15✔
1559
            }
1560
        }
1561
        for (auto const & ci: column_infos) {
33✔
1562
            if (std::find_if(reordered.begin(), reordered.end(), [&](ColumnInfo const & x) { return x.name == ci.name; }) == reordered.end()) {
48✔
1563
                reordered.push_back(ci);
6✔
1564
            }
1565
        }
1566
        column_infos = std::move(reordered);
12✔
1567
    }
12✔
1568

1569
    // Set up the table viewer with pagination
1570
    _table_viewer->setTableConfiguration(
100✔
1571
            std::move(selector),
20✔
1572
            std::move(column_infos),
20✔
1573
            _data_manager,
20✔
1574
            QString("Preview: %1").arg(_current_table_id),
40✔
1575
            row_source);
1576

1577
    // Capture the current visual order from the viewer
1578
    QStringList currentOrder;
20✔
1579
    if (_table_viewer) {
20✔
1580
        auto * tv = _table_viewer->findChild<QTableView *>();
20✔
1581
        if (tv && tv->model()) {
20✔
1582
            auto * header = tv->horizontalHeader();
20✔
1583
            int cols = tv->model()->columnCount();
20✔
1584
            for (int v = 0; header && v < cols; ++v) {
64✔
1585
                int logical = header->logicalIndex(v);
44✔
1586
                auto name = tv->model()->headerData(logical, Qt::Horizontal, Qt::DisplayRole).toString();
44✔
1587
                currentOrder.push_back(name);
44✔
1588
            }
44✔
1589
        }
1590
    }
1591
    if (!currentOrder.isEmpty()) {
20✔
1592
        _table_column_order[_current_table_id] = currentOrder;
20✔
1593
    }
1594
}
136✔
1595

1596
void TableDesignerWidget::refreshComputersTree() {
84✔
1597
    if (!_data_manager) return;
84✔
1598

1599
    _updating_computers_tree = true;
84✔
1600

1601
    // Preserve previous checkbox states and custom column names
1602
    std::map<std::string, std::pair<Qt::CheckState, QString>> previous_states;
84✔
1603
    if (ui->computers_tree && ui->computers_tree->topLevelItemCount() > 0) {
84✔
1604
        for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
598✔
1605
            auto * top_level_item = ui->computers_tree->topLevelItem(i);
545✔
1606
            // Handle both grouped and individual modes
1607
            for (int j = 0; j < top_level_item->childCount(); ++j) {
2,303✔
1608
                auto * child_item = top_level_item->child(j);
1,758✔
1609
                if (child_item->childCount() > 0) {
1,758✔
1610
                    // This is a computer item under a group/data source
1611
                    for (int k = 0; k < child_item->childCount(); ++k) {
×
UNCOV
1612
                        auto * computer_item = child_item->child(k);
×
UNCOV
1613
                        QString ds = computer_item->data(0, Qt::UserRole).toString();
×
1614
                        QString cn = computer_item->data(1, Qt::UserRole).toString();
×
1615
                        std::string key = (ds + "||" + cn).toStdString();
×
UNCOV
1616
                        previous_states[key] = {computer_item->checkState(1), computer_item->text(2)};
×
UNCOV
1617
                    }
×
1618
                } else {
1619
                    // This is a computer item directly under data source (individual mode)
1620
                    QString ds = child_item->data(0, Qt::UserRole).toString();
1,758✔
1621
                    QString cn = child_item->data(1, Qt::UserRole).toString();
1,758✔
1622
                    std::string key = (ds + "||" + cn).toStdString();
1,758✔
1623
                    previous_states[key] = {child_item->checkState(1), child_item->text(2)};
1,758✔
1624
                }
1,758✔
1625
            }
1626
        }
1627
    }
1628

1629
    ui->computers_tree->clear();
84✔
1630
    ui->computers_tree->setHeaderLabels({"Data Source / Computer", "Enabled", "Column Name", "Parameters"});
504✔
1631

1632
    // Clean up parameter widgets
1633
    _computer_parameter_widgets.clear();
84✔
1634
    _parameter_controls.clear();
84✔
1635

1636
    auto * registry = _data_manager->getTableRegistry();
84✔
1637
    if (!registry) {
84✔
UNCOV
1638
        _updating_computers_tree = false;
×
UNCOV
1639
        return;
×
1640
    }
1641

1642
    auto data_manager_extension = registry->getDataManagerExtension();
84✔
1643
    if (!data_manager_extension) {
84✔
UNCOV
1644
        _updating_computers_tree = false;
×
UNCOV
1645
        return;
×
1646
    }
1647

1648
    auto & computer_registry = registry->getComputerRegistry();
84✔
1649

1650
    // Get available data sources
1651
    auto data_sources = getAvailableDataSources();
84✔
1652

1653
    if (_group_mode) {
84✔
1654
        // Group similar data sources together
1655
        std::map<std::string, QStringList> groups;
84✔
1656

1657
        // First pass: group data sources by their extracted group name
1658
        for (QString const & data_source: data_sources) {
945✔
1659
            std::string group_name = extractGroupName(data_source);
861✔
1660
            groups[group_name].append(data_source);
861✔
1661
        }
861✔
1662

1663
        // Second pass: create tree structure
1664
        for (auto const & [group_name, group_members]: groups) {
945✔
1665
            if (group_members.size() > 1) {
861✔
1666
                // Create group item
UNCOV
1667
                auto * group_item = new QTreeWidgetItem(ui->computers_tree);
×
UNCOV
1668
                group_item->setText(0, QString::fromStdString(group_name) + " (Group)");
×
UNCOV
1669
                group_item->setFlags(Qt::ItemIsEnabled);
×
UNCOV
1670
                group_item->setExpanded(false);// Start collapsed
×
1671

1672
                // Get computers available for this group (use first member to determine available computers)
UNCOV
1673
                auto [first_variant, row_selector_type] = createDataSourceVariant(group_members.first(), data_manager_extension);
×
UNCOV
1674
                if (!first_variant.has_value()) {
×
UNCOV
1675
                    continue;
×
1676
                }
1677

UNCOV
1678
                auto available_computers = computer_registry.getAvailableComputers(row_selector_type, first_variant.value());
×
1679

1680
                // Add computers as children of the group
UNCOV
1681
                for (auto const & computer_info: available_computers) {
×
UNCOV
1682
                    auto * computer_item = new QTreeWidgetItem(group_item);
×
UNCOV
1683
                    computer_item->setText(0, QString::fromStdString(computer_info.name));
×
UNCOV
1684
                    computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
×
UNCOV
1685
                    computer_item->setCheckState(1, Qt::Unchecked);
×
1686

1687
                    // Generate default column name using group name
UNCOV
1688
                    QString default_name = QString("%1_%2").arg(QString::fromStdString(group_name), QString::fromStdString(computer_info.name));
×
UNCOV
1689
                    computer_item->setText(2, default_name);
×
UNCOV
1690
                    computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
×
1691

1692
                    // Store group members and computer name for later use
UNCOV
1693
                    computer_item->setData(0, Qt::UserRole, group_members.join("||"));// Store all group members
×
UNCOV
1694
                    computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
×
UNCOV
1695
                    computer_item->setData(2, Qt::UserRole, true);// Mark as group computer
×
1696

1697
                    // Create parameter widget if computer has parameters
UNCOV
1698
                    if (!computer_info.parameterDescriptors.empty()) {
×
UNCOV
1699
                        auto * param_widget = createParameterWidget(QString::fromStdString(computer_info.name),
×
UNCOV
1700
                                                                    computer_info.parameterDescriptors);
×
UNCOV
1701
                        if (param_widget) {
×
UNCOV
1702
                            ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
×
UNCOV
1703
                            _computer_parameter_widgets[computer_item] = param_widget;
×
1704
                        }
1705
                    }
1706

1707
                    // Restore previous state if present (use first member for key)
UNCOV
1708
                    std::string prev_key = (group_members.first() + "||" + QString::fromStdString(computer_info.name)).toStdString();
×
UNCOV
1709
                    auto it_prev = previous_states.find(prev_key);
×
UNCOV
1710
                    if (it_prev != previous_states.end()) {
×
UNCOV
1711
                        computer_item->setCheckState(1, it_prev->second.first);
×
UNCOV
1712
                        if (!it_prev->second.second.isEmpty()) {
×
UNCOV
1713
                            computer_item->setText(2, it_prev->second.second);
×
1714
                        }
1715
                    }
UNCOV
1716
                }
×
UNCOV
1717
            } else {
×
1718
                // Single item - create as individual data source
1719
                QString const & data_source = group_members.first();
861✔
1720
                auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
861✔
1721
                data_source_item->setText(0, data_source);
861✔
1722
                data_source_item->setFlags(Qt::ItemIsEnabled);
861✔
1723
                data_source_item->setExpanded(false);
861✔
1724

1725
                auto [data_source_variant, row_selector_type] = createDataSourceVariant(data_source, data_manager_extension);
861✔
1726
                if (!data_source_variant.has_value()) {
861✔
1727
                    continue;
343✔
1728
                }
1729

1730
                auto available_computers = computer_registry.getAvailableComputers(row_selector_type, data_source_variant.value());
518✔
1731

1732
                for (auto const & computer_info: available_computers) {
3,296✔
1733
                    auto * computer_item = new QTreeWidgetItem(data_source_item);
2,778✔
1734
                    computer_item->setText(0, QString::fromStdString(computer_info.name));
2,778✔
1735
                    computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
2,778✔
1736
                    computer_item->setCheckState(1, Qt::Unchecked);
2,778✔
1737

1738
                    QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
2,778✔
1739
                    computer_item->setText(2, default_name);
2,778✔
1740
                    computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
2,778✔
1741

1742
                    computer_item->setData(0, Qt::UserRole, data_source);
2,778✔
1743
                    computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
2,778✔
1744
                    computer_item->setData(2, Qt::UserRole, false);// Mark as individual computer
2,778✔
1745

1746
                    // Create parameter widget if computer has parameters
1747
                    if (!computer_info.parameterDescriptors.empty()) {
2,778✔
1748
                        auto * param_widget = createParameterWidget(QString::fromStdString(computer_info.name),
254✔
1749
                                                                    computer_info.parameterDescriptors);
254✔
1750
                        if (param_widget) {
254✔
1751
                            ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
254✔
1752
                            _computer_parameter_widgets[computer_item] = param_widget;
254✔
1753
                        }
1754
                    }
1755

1756
                    std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
2,778✔
1757
                    auto it_prev = previous_states.find(prev_key);
2,778✔
1758
                    if (it_prev != previous_states.end()) {
2,778✔
1759
                        computer_item->setCheckState(1, it_prev->second.first);
1,755✔
1760
                        if (!it_prev->second.second.isEmpty()) {
1,755✔
1761
                            computer_item->setText(2, it_prev->second.second);
1,755✔
1762
                        }
1763
                    }
1764
                }
2,778✔
1765
            }
861✔
1766
        }
1767
    } else {
84✔
1768
        // Individual mode - create tree structure: Data Source -> Computers (original behavior)
UNCOV
1769
        for (QString const & data_source: data_sources) {
×
UNCOV
1770
            auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
×
UNCOV
1771
            data_source_item->setText(0, data_source);
×
1772
            data_source_item->setFlags(Qt::ItemIsEnabled);
×
UNCOV
1773
            data_source_item->setExpanded(false);// Start collapsed
×
1774

1775
            // Convert data source string to DataSourceVariant and determine RowSelectorType
UNCOV
1776
            auto [data_source_variant, row_selector_type] = createDataSourceVariant(data_source, data_manager_extension);
×
1777

UNCOV
1778
            if (!data_source_variant.has_value()) {
×
UNCOV
1779
                qDebug() << "Failed to create data source variant for:" << data_source;
×
UNCOV
1780
                continue;
×
1781
            }
1782

1783
            // Get available computers for this specific data source and row selector combination
UNCOV
1784
            auto available_computers = computer_registry.getAvailableComputers(row_selector_type, data_source_variant.value());
×
1785

1786
            // Add compatible computers as children
UNCOV
1787
            for (auto const & computer_info: available_computers) {
×
UNCOV
1788
                auto * computer_item = new QTreeWidgetItem(data_source_item);
×
UNCOV
1789
                computer_item->setText(0, QString::fromStdString(computer_info.name));
×
UNCOV
1790
                computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
×
UNCOV
1791
                computer_item->setCheckState(1, Qt::Unchecked);
×
1792

1793
                // Generate default column name
1794
                QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
×
UNCOV
1795
                computer_item->setText(2, default_name);
×
UNCOV
1796
                computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
×
1797

1798
                // Store data source and computer name for later use
UNCOV
1799
                computer_item->setData(0, Qt::UserRole, data_source);
×
UNCOV
1800
                computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
×
UNCOV
1801
                computer_item->setData(2, Qt::UserRole, false);// Mark as individual computer
×
1802

1803
                // Create parameter widget if computer has parameters
UNCOV
1804
                if (!computer_info.parameterDescriptors.empty()) {
×
UNCOV
1805
                    auto * param_widget = createParameterWidget(QString::fromStdString(computer_info.name),
×
UNCOV
1806
                                                                computer_info.parameterDescriptors);
×
UNCOV
1807
                    if (param_widget) {
×
UNCOV
1808
                        ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
×
UNCOV
1809
                        _computer_parameter_widgets[computer_item] = param_widget;
×
1810
                    }
1811
                }
1812

1813
                // Restore previous state if present
UNCOV
1814
                std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
×
UNCOV
1815
                auto it_prev = previous_states.find(prev_key);
×
UNCOV
1816
                if (it_prev != previous_states.end()) {
×
UNCOV
1817
                    computer_item->setCheckState(1, it_prev->second.first);
×
UNCOV
1818
                    if (!it_prev->second.second.isEmpty()) {
×
UNCOV
1819
                        computer_item->setText(2, it_prev->second.second);
×
1820
                    }
1821
                }
1822
            }
×
1823
        }
×
1824
    }
1825

1826
    // Resize columns to content
1827
    ui->computers_tree->resizeColumnToContents(0);
84✔
1828
    ui->computers_tree->resizeColumnToContents(1);
84✔
1829
    ui->computers_tree->resizeColumnToContents(2);
84✔
1830
    ui->computers_tree->resizeColumnToContents(3);
84✔
1831

1832
    _updating_computers_tree = false;
84✔
1833

1834
    // Update preview after refresh
1835
    triggerPreviewDebounced();
84✔
1836
}
168✔
1837

1838
void TableDesignerWidget::setJsonTemplateFromCurrentState() {
4✔
1839
    if (!_table_json_widget) return;
4✔
1840
    // Build a minimal JSON template representing current UI state
1841
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
4✔
1842
    auto columns = getEnabledColumnInfos();
4✔
1843
    if (row_source.isEmpty() && columns.empty()) {
4✔
UNCOV
1844
        _table_json_widget->setJsonText("{}");
×
UNCOV
1845
        return;
×
1846
    }
1847

1848
    QString row_type;
4✔
1849
    QString row_source_name;
4✔
1850
    if (row_source.startsWith("TimeFrame: ")) {
4✔
1851
        row_type = "timestamp";
1✔
1852
        row_source_name = row_source.mid(11);
1✔
1853
    } else if (row_source.startsWith("Events: ")) {
3✔
UNCOV
1854
        row_type = "timestamp";
×
1855
        row_source_name = row_source.mid(8);
×
1856
    } else if (row_source.startsWith("Intervals: ")) {
3✔
1857
        row_type = "interval";
3✔
1858
        row_source_name = row_source.mid(11);
3✔
1859
    }
1860

1861
    QStringList column_entries;
4✔
1862
    for (auto const & c: columns) {
10✔
1863
        // Strip any internal prefixes for JSON to keep schema user-friendly
1864
        QString ds = QString::fromStdString(c.dataSourceName);
6✔
1865
        if (ds.startsWith("events:")) ds = ds.mid(7);
6✔
1866
        else if (ds.startsWith("intervals:"))
4✔
1867
            ds = ds.mid(10);
3✔
1868
        else if (ds.startsWith("analog:"))
1✔
UNCOV
1869
            ds = ds.mid(7);
×
1870

1871
        QString entry = QString(
18✔
1872
                                "{\n  \"name\": \"%1\",\n  \"description\": \"%2\",\n  \"data_source\": \"%3\",\n  \"computer\": \"%4\"%5\n}")
1873
                                .arg(QString::fromStdString(c.name))
24✔
1874
                                .arg(QString::fromStdString(c.description))
24✔
1875
                                .arg(ds)
24✔
1876
                                .arg(QString::fromStdString(c.computerName))
24✔
1877
                                .arg(c.parameters.empty() ? QString() : QString(",\n  \"parameters\": {}"));
12✔
1878
        column_entries << entry;
6✔
1879
    }
6✔
1880

1881
    QString table_name = _table_info_widget ? _table_info_widget->getName() : _current_table_id;
4✔
1882
    QString json = QString(
12✔
1883
                           "{\n  \"tables\": [\n    {\n      \"table_id\": \"%1\",\n      \"name\": \"%2\",\n      \"row_selector\": { \"type\": \"%3\", \"source\": \"%4\" },\n      \"columns\": [\n%5\n      ]\n    }\n  ]\n}")
1884
                           .arg(_current_table_id)
16✔
1885
                           .arg(table_name)
16✔
1886
                           .arg(row_type)
16✔
1887
                           .arg(row_source_name)
16✔
1888
                           .arg(column_entries.join(",\n"));
8✔
1889

1890
    _table_json_widget->setJsonText(json);
4✔
1891
}
4✔
1892

1893
void TableDesignerWidget::applyJsonTemplateToUI(QString const & jsonText) {
7✔
1894
    // Very light-weight parser using Qt to extract essential fields.
1895
    // Assumes a schema similar to tests under computers *.test.cpp.
1896
    QJsonParseError err;
7✔
1897
    QByteArray bytes = jsonText.toUtf8();
7✔
1898
    QJsonDocument doc = QJsonDocument::fromJson(bytes, &err);
7✔
1899
    if (err.error != QJsonParseError::NoError || !doc.isObject()) {
7✔
1900
        // Compute line/column from byte offset if possible
1901
        int64_t offset = static_cast<int64_t>(err.offset);
1✔
1902
        int line = 1;
1✔
1903
        int col = 1;
1✔
1904
        // Avoid operator[] ambiguity on some compilers by using qsizetype and at()
1905
        qsizetype len = std::min<qsizetype>(bytes.size(), static_cast<qsizetype>(offset));
1✔
1906
        for (qsizetype i = 0; i < len; ++i) {
149✔
1907
            char ch = bytes.at(i);
148✔
1908
            if (ch == '\n') {
148✔
1909
                ++line;
6✔
1910
                col = 1;
6✔
1911
            } else {
1912
                ++col;
142✔
1913
            }
1914
        }
1915
        QString detail = err.error != QJsonParseError::NoError
1✔
1916
                                 ? QString("%1 (line %2, column %3)").arg(err.errorString()).arg(line).arg(col)
3✔
1917
                                 : QString("JSON root must be an object");
2✔
1918
        auto * box = new QMessageBox(this);
1✔
1919
        box->setIcon(QMessageBox::Critical);
1✔
1920
        box->setWindowTitle("Invalid JSON");
1✔
1921
        box->setText(QString("JSON format is invalid: %1").arg(detail));
1✔
1922
        box->setAttribute(Qt::WA_DeleteOnClose);
1✔
1923
        box->show();
1✔
1924
        return;
1✔
1925
    }
1✔
1926
    auto obj = doc.object();
6✔
1927
    if (!obj.contains("tables") || !obj["tables"].isArray()) {
6✔
UNCOV
1928
        auto * box = new QMessageBox(this);
×
1929
        box->setIcon(QMessageBox::Critical);
×
UNCOV
1930
        box->setWindowTitle("Invalid JSON");
×
1931
        box->setText("Missing required key: tables (array)");
×
UNCOV
1932
        box->setAttribute(Qt::WA_DeleteOnClose);
×
1933
        box->show();
×
UNCOV
1934
        return;
×
1935
    }
1936
    auto tables = obj["tables"].toArray();
6✔
1937
    if (tables.isEmpty() || !tables[0].isObject()) return;
6✔
1938
    auto table = tables[0].toObject();
6✔
1939

1940
    // Row selector
1941
    QStringList errors;
6✔
1942
    QString rs_type;
6✔
1943
    QString rs_source;
6✔
1944
    if (table.contains("row_selector") && table["row_selector"].isObject()) {
6✔
1945
        auto rs = table["row_selector"].toObject();
5✔
1946
        rs_type = rs.value("type").toString();
5✔
1947
        rs_source = rs.value("source").toString();
5✔
1948
        if (rs_type.isEmpty() || rs_source.isEmpty()) {
5✔
UNCOV
1949
            errors << "Missing required keys in row_selector: 'type' and/or 'source'";
×
1950
        } else {
1951
            // Validate existence
1952
            bool source_ok = false;
5✔
1953
            if (rs_type == "interval") {
5✔
1954
                source_ok = (_data_manager && _data_manager->getData<DigitalIntervalSeries>(rs_source.toStdString()) != nullptr);
5✔
UNCOV
1955
            } else if (rs_type == "timestamp") {
×
1956
                source_ok = (_data_manager && (_data_manager->getTime(TimeKey(rs_source.toStdString())) != nullptr ||
×
UNCOV
1957
                                               _data_manager->getData<DigitalEventSeries>(rs_source.toStdString()) != nullptr));
×
1958
            } else {
1959
                errors << QString("Unsupported row_selector type: %1").arg(rs_type);
×
1960
            }
1961
            if (!source_ok) {
5✔
1962
                errors << QString("Row selector data key not found in DataManager: %1").arg(rs_source);
1✔
1963
            } else {
1964
                // Apply selection to UI
1965
                QString entry;
4✔
1966
                if (rs_type == "interval") {
4✔
1967
                    entry = QString("Intervals: %1").arg(rs_source);
4✔
1968
                } else if (rs_type == "timestamp") {
×
1969
                    // Prefer TimeFrame, fallback to Events
UNCOV
1970
                    entry = QString("TimeFrame: %1").arg(rs_source);
×
UNCOV
1971
                    int idx_tf = ui->row_data_source_combo->findText(entry);
×
UNCOV
1972
                    if (idx_tf < 0) entry = QString("Events: %1").arg(rs_source);
×
1973
                }
1974
                int idx = ui->row_data_source_combo->findText(entry);
4✔
1975
                if (idx >= 0) {
4✔
1976
                    ui->row_data_source_combo->setCurrentIndex(idx);
4✔
1977
                    // Ensure computers tree reflects this row selector before enabling columns
1978
                    refreshComputersTree();
4✔
1979
                } else {
1980
                    errors << QString("Row selector entry not available in UI: %1").arg(entry);
×
1981
                }
1982
            }
4✔
1983
        }
1984
    } else {
5✔
1985
        errors << "Missing required key: row_selector (object)";
1✔
1986
    }
1987

1988
    // Columns: enable matching computers and set column names
1989
    if (table.contains("columns") && table["columns"].isArray()) {
6✔
1990
        auto cols = table["columns"].toArray();
6✔
1991
        auto * tree = ui->computers_tree;
6✔
1992
        // Avoid recursive preview rebuilds while we toggle many items
1993
        bool prevBlocked = tree->blockSignals(true);
6✔
1994
        for (auto const & cval: cols) {
12✔
1995
            if (!cval.isObject()) continue;
6✔
1996
            auto cobj = cval.toObject();
6✔
1997
            QString data_source = cobj.value("data_source").toString();
6✔
1998
            QString computer = cobj.value("computer").toString();
6✔
1999
            QString name = cobj.value("name").toString();
6✔
2000
            if (data_source.isEmpty() || computer.isEmpty() || name.isEmpty()) {
6✔
UNCOV
2001
                errors << "Missing required keys in column: 'name', 'data_source', and 'computer'";
×
UNCOV
2002
                continue;
×
2003
            }
2004
            // Validate data source existence
2005
            bool has_ds = (_data_manager && (_data_manager->getData<DigitalEventSeries>(data_source.toStdString()) != nullptr ||
19✔
2006
                                             _data_manager->getData<DigitalIntervalSeries>(data_source.toStdString()) != nullptr ||
7✔
2007
                                             _data_manager->getData<AnalogTimeSeries>(data_source.toStdString()) != nullptr));
13✔
2008
            if (!has_ds) {
6✔
2009
                errors << QString("Data key not found in DataManager: %1").arg(data_source);
1✔
2010
            }
2011
            // Validate computer exists
2012
            bool computer_exists = false;
6✔
2013
            if (_data_manager) {
6✔
2014
                if (auto * reg = _data_manager->getTableRegistry()) {
6✔
2015
                    auto & cr = reg->getComputerRegistry();
6✔
2016
                    computer_exists = cr.findComputerInfo(computer.toStdString());
6✔
2017
                }
2018
            }
2019
            if (!computer_exists) {
6✔
2020
                errors << QString("Requested computer does not exist: %1").arg(computer);
2✔
2021
            }
2022
            // Validate compatibility (heuristic)
2023
            bool type_event = false, type_interval = false, type_analog = false;
6✔
2024
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
66✔
2025
                auto * ds_item = tree->topLevelItem(i);
60✔
2026
                QString ds_text = ds_item->text(0);
60✔
2027
                if (ds_text.contains(data_source)) {
60✔
2028
                    if (ds_text.startsWith("Events: ")) type_event = true;
5✔
UNCOV
2029
                    else if (ds_text.startsWith("Intervals: "))
×
UNCOV
2030
                        type_interval = true;
×
2031
                    else if (ds_text.startsWith("analog:"))
×
UNCOV
2032
                        type_analog = true;
×
2033
                }
2034
            }
60✔
2035
            QString ds_repr;
6✔
2036
            if (type_event) ds_repr = QString("Events: %1").arg(data_source);
6✔
2037
            else if (type_interval)
1✔
UNCOV
2038
                ds_repr = QString("Intervals: %1").arg(data_source);
×
2039
            else if (type_analog)
1✔
UNCOV
2040
                ds_repr = QString("analog:%1").arg(data_source);
×
2041
            if (!ds_repr.isEmpty() && !isComputerCompatibleWithDataSource(computer.toStdString(), ds_repr)) {
6✔
2042
                errors << QString("Computer '%1' is not valid for data source type requested (%2)").arg(computer, ds_repr);
2✔
2043
            }
2044

2045
            // Find matching tree item with strict preference
2046
            QString exact_events = QString("Events: %1").arg(data_source);
6✔
2047
            QString exact_intervals = QString("Intervals: %1").arg(data_source);
6✔
2048
            QString exact_analog = QString("analog:%1").arg(data_source);
6✔
2049
            QTreeWidgetItem * matched_ds = nullptr;
6✔
2050
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
31✔
2051
                auto * ds_item = tree->topLevelItem(i);
30✔
2052
                QString t = ds_item->text(0);
30✔
2053
                if (t == exact_events || t == exact_intervals || t == exact_analog) {
30✔
2054
                    matched_ds = ds_item;
5✔
2055
                    break;
5✔
2056
                }
2057
            }
30✔
2058
            if (!matched_ds) {
6✔
2059
                for (int i = 0; i < tree->topLevelItemCount(); ++i) {
11✔
2060
                    auto * ds_item = tree->topLevelItem(i);
10✔
2061
                    QString t = ds_item->text(0);
10✔
2062
                    if (t.contains(data_source) || t.endsWith(data_source)) {
10✔
UNCOV
2063
                        matched_ds = ds_item;
×
UNCOV
2064
                        break;
×
2065
                    }
2066
                }
10✔
2067
            }
2068
            if (matched_ds) {
6✔
2069
                for (int j = 0; j < matched_ds->childCount(); ++j) {
20✔
2070
                    auto * comp_item = matched_ds->child(j);
15✔
2071
                    QString comp_text = comp_item->text(0).trimmed();
15✔
2072
                    if (comp_text == computer || comp_text.contains(computer)) {
15✔
2073
                        comp_item->setCheckState(1, Qt::Checked);
3✔
2074
                        if (!name.isEmpty()) comp_item->setText(2, name);
3✔
2075
                    }
2076
                }
15✔
2077
            } else {
2078
                errors << QString("Data source not found in tree: %1").arg(data_source);
1✔
2079
            }
2080
        }
6✔
2081
        tree->blockSignals(prevBlocked);
6✔
2082
        if (!errors.isEmpty()) {
6✔
2083
            auto * box = new QMessageBox(this);
4✔
2084
            box->setIcon(QMessageBox::Critical);
4✔
2085
            box->setWindowTitle("Invalid Table JSON");
4✔
2086
            box->setText(errors.join("\n"));
4✔
2087
            box->setAttribute(Qt::WA_DeleteOnClose);
4✔
2088
            box->show();
4✔
2089
            return;
4✔
2090
        }
2091
        triggerPreviewDebounced();
2✔
2092
    }
6✔
2093
}
36✔
2094

2095
void TableDesignerWidget::onComputersTreeItemChanged() {
23,972✔
2096
    if (_updating_computers_tree) return;
23,972✔
2097

2098
    // Trigger preview update when checkbox states change
2099
    triggerPreviewDebounced();
16✔
2100
}
2101

2102
void TableDesignerWidget::onComputersTreeItemEdited(QTreeWidgetItem * item, int column) {
23,972✔
2103
    if (_updating_computers_tree) return;
23,972✔
2104

2105
    // Only respond to column name edits (column 2)
2106
    if (column == 2) {
16✔
2107
        // Column name was edited, trigger preview update
2108
        triggerPreviewDebounced();
1✔
2109
    }
2110
}
2111

2112
std::vector<ColumnInfo> TableDesignerWidget::getEnabledColumnInfos() const {
80✔
2113
    std::vector<ColumnInfo> column_infos;
80✔
2114

2115
    if (!ui->computers_tree) return column_infos;
80✔
2116

2117
    // Iterate through all data source items
2118
    for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
901✔
2119
        auto * data_source_item = ui->computers_tree->topLevelItem(i);
821✔
2120

2121
        // Iterate through computer items under each data source
2122
        for (int j = 0; j < data_source_item->childCount(); ++j) {
3,479✔
2123
            auto * computer_item = data_source_item->child(j);
2,658✔
2124

2125
            // Check if this computer is enabled
2126
            if (computer_item->checkState(1) == Qt::Checked) {
2,658✔
2127
                QString data_source = computer_item->data(0, Qt::UserRole).toString();
50✔
2128
                QString computer_name = computer_item->data(1, Qt::UserRole).toString();
50✔
2129
                QString column_name = computer_item->text(2);
50✔
2130
                bool is_group_computer = computer_item->data(2, Qt::UserRole).toBool();
50✔
2131

2132
                // Get parameter values for this computer
2133
                std::map<std::string, std::string> parameters = getParameterValues(computer_name);
50✔
2134

2135
                if (is_group_computer) {
50✔
2136
                    // This is a group computer - create columns for all members
UNCOV
2137
                    QStringList group_members = data_source.split("||");
×
2138

UNCOV
2139
                    for (QString const & member: group_members) {
×
2140
                        // Generate individual column name (e.g., "spike_1_Mean", "spike_2_Mean")
UNCOV
2141
                        QString individual_column_name = generateDefaultColumnName(member, computer_name);
×
2142

2143
                        // Create ColumnInfo for each group member
UNCOV
2144
                        QString source_key = member;
×
UNCOV
2145
                        if (source_key.startsWith("Events: ")) {
×
UNCOV
2146
                            source_key = QString("events:%1").arg(source_key.mid(8));
×
UNCOV
2147
                        } else if (source_key.startsWith("Intervals: ")) {
×
UNCOV
2148
                            source_key = QString("intervals:%1").arg(source_key.mid(11));
×
UNCOV
2149
                        } else if (source_key.startsWith("analog:")) {
×
UNCOV
2150
                            source_key = source_key;// already prefixed
×
UNCOV
2151
                        } else if (source_key.startsWith("lines:")) {
×
UNCOV
2152
                            source_key = source_key;// already prefixed
×
UNCOV
2153
                        } else if (source_key.startsWith("TimeFrame: ")) {
×
2154
                            // TimeFrame used only for row selector; columns require concrete sources
UNCOV
2155
                            source_key = source_key.mid(11);
×
2156
                        }
2157

UNCOV
2158
                        ColumnInfo info(individual_column_name.toStdString(),
×
UNCOV
2159
                                        QString("Column from %1 using %2 (group applied)").arg(member, computer_name).toStdString(),
×
UNCOV
2160
                                        source_key.toStdString(),
×
UNCOV
2161
                                        computer_name.toStdString());
×
2162

2163
                        // Set parameters
UNCOV
2164
                        info.parameters = parameters;
×
2165

2166
                        // Set output type based on computer info
UNCOV
2167
                        if (auto * registry = _data_manager->getTableRegistry()) {
×
UNCOV
2168
                            auto & computer_registry = registry->getComputerRegistry();
×
UNCOV
2169
                            auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
×
UNCOV
2170
                            if (computer_info) {
×
UNCOV
2171
                                info.outputType = computer_info->outputType;
×
UNCOV
2172
                                info.outputTypeName = computer_info->outputTypeName;
×
UNCOV
2173
                                info.isVectorType = computer_info->isVectorType;
×
UNCOV
2174
                                if (info.isVectorType) {
×
UNCOV
2175
                                    info.elementType = computer_info->elementType;
×
UNCOV
2176
                                    info.elementTypeName = computer_info->elementTypeName;
×
2177
                                }
2178
                            }
2179
                        }
2180

UNCOV
2181
                        column_infos.push_back(std::move(info));
×
UNCOV
2182
                    }
×
UNCOV
2183
                } else {
×
2184
                    // Individual computer - original behavior
2185
                    if (column_name.isEmpty()) {
50✔
2186
                        // Clean the data source name before generating column name
UNCOV
2187
                        QString clean_data_source = data_source;
×
UNCOV
2188
                        if (clean_data_source.startsWith("lines:")) {
×
UNCOV
2189
                            clean_data_source = clean_data_source.mid(6);// Remove "lines:" prefix
×
2190
                        }
UNCOV
2191
                        column_name = generateDefaultColumnName(clean_data_source, computer_name);
×
UNCOV
2192
                    }
×
2193

2194
                    // Create ColumnInfo (use raw key without UI prefixes)
2195
                    QString source_key = data_source;
50✔
2196
                    if (source_key.startsWith("Events: ")) {
50✔
2197
                        source_key = QString("events:%1").arg(source_key.mid(8));
21✔
2198
                    } else if (source_key.startsWith("Intervals: ")) {
29✔
2199
                        source_key = QString("intervals:%1").arg(source_key.mid(11));
23✔
2200
                    } else if (source_key.startsWith("analog:")) {
6✔
2201
                        source_key = source_key;// already prefixed
×
2202
                    } else if (source_key.startsWith("lines:")) {
6✔
2203
                        source_key = source_key;// already prefixed
6✔
2204
                    } else if (source_key.startsWith("TimeFrame: ")) {
×
2205
                        // TimeFrame used only for row selector; columns require concrete sources
UNCOV
2206
                        source_key = source_key.mid(11);
×
2207
                    }
2208

2209
                    ColumnInfo info(column_name.toStdString(),
150✔
2210
                                    QString("Column from %1 using %2").arg(data_source, computer_name).toStdString(),
100✔
2211
                                    source_key.toStdString(),
100✔
2212
                                    computer_name.toStdString());
250✔
2213

2214
                    // Set parameters
2215
                    info.parameters = parameters;
50✔
2216

2217
                    // Set output type based on computer info
2218
                    if (auto * registry = _data_manager->getTableRegistry()) {
50✔
2219
                        auto & computer_registry = registry->getComputerRegistry();
50✔
2220
                        auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
50✔
2221
                        if (computer_info) {
50✔
2222
                            info.outputType = computer_info->outputType;
50✔
2223
                            info.outputTypeName = computer_info->outputTypeName;
50✔
2224
                            info.isVectorType = computer_info->isVectorType;
50✔
2225
                            if (info.isVectorType) {
50✔
2226
                                info.elementType = computer_info->elementType;
6✔
2227
                                info.elementTypeName = computer_info->elementTypeName;
6✔
2228
                            }
2229
                        }
2230
                    }
2231

2232
                    column_infos.push_back(std::move(info));
50✔
2233
                }
50✔
2234
            }
50✔
2235
        }
2236
    }
2237

2238
    return column_infos;
80✔
UNCOV
2239
}
×
2240

2241
bool TableDesignerWidget::isComputerCompatibleWithDataSource(std::string const & computer_name, QString const & data_source) const {
5✔
2242
    if (!_data_manager) return false;
5✔
2243

2244
    auto * registry = _data_manager->getTableRegistry();
5✔
2245
    if (!registry) return false;
5✔
2246

2247
    auto & computer_registry = registry->getComputerRegistry();
5✔
2248
    auto computer_info = computer_registry.findComputerInfo(computer_name);
5✔
2249
    if (!computer_info) return false;
5✔
2250

2251
    // Basic compatibility check based on data source type and common computer patterns
2252
    if (data_source.startsWith("Events: ")) {
3✔
2253
        // Event-based computers typically have "Event" in their name
2254
        return computer_name.find("Event") != std::string::npos;
3✔
UNCOV
2255
    } else if (data_source.startsWith("Intervals: ")) {
×
2256
        // Interval-based computers typically work with intervals or events
UNCOV
2257
        return computer_name.find("Event") != std::string::npos ||
×
UNCOV
2258
               computer_name.find("Interval") != std::string::npos;
×
UNCOV
2259
    } else if (data_source.startsWith("analog:")) {
×
2260
        // Analog-based computers typically have "Analog" in their name
UNCOV
2261
        return computer_name.find("Analog") != std::string::npos;
×
UNCOV
2262
    } else if (data_source.startsWith("TimeFrame: ")) {
×
2263
        // TimeFrame-based computers - generally most computers can work with timestamps
UNCOV
2264
        return computer_name.find("Timestamp") != std::string::npos ||
×
UNCOV
2265
               computer_name.find("Time") != std::string::npos;
×
2266
    }
2267

2268
    // Default: assume compatibility for unrecognized patterns
UNCOV
2269
    return true;
×
2270
}
2271

2272
QString TableDesignerWidget::generateDefaultColumnName(QString const & data_source, QString const & computer_name) const {
2,778✔
2273
    QString source_name = data_source;
2,778✔
2274

2275
    // Extract the actual name from prefixed data sources
2276
    if (source_name.startsWith("Events: ")) {
2,778✔
2277
        source_name = source_name.mid(8);
510✔
2278
    } else if (source_name.startsWith("Intervals: ")) {
2,268✔
2279
        source_name = source_name.mid(11);
672✔
2280
    } else if (source_name.startsWith("analog:")) {
1,596✔
2281
        source_name = source_name.mid(7);
1,344✔
2282
    } else if (source_name.startsWith("lines:")) {
252✔
2283
        source_name = source_name.mid(6);
252✔
UNCOV
2284
    } else if (source_name.startsWith("TimeFrame: ")) {
×
UNCOV
2285
        source_name = source_name.mid(11);
×
2286
    }
2287

2288
    // Create a concise name
2289
    return QString("%1_%2").arg(source_name, computer_name);
8,334✔
2290
}
2,778✔
2291

2292
std::string TableDesignerWidget::extractGroupName(QString const & data_source) const {
861✔
2293
    QString source_name = data_source;
861✔
2294

2295
    // Extract the actual name from prefixed data sources
2296
    if (source_name.startsWith("Events: ")) {
861✔
2297
        source_name = source_name.mid(8);
170✔
2298
    } else if (source_name.startsWith("Intervals: ")) {
691✔
2299
        source_name = source_name.mid(11);
96✔
2300
    } else if (source_name.startsWith("analog:")) {
595✔
2301
        source_name = source_name.mid(7);
168✔
2302
    } else if (source_name.startsWith("lines:")) {
427✔
2303
        source_name = source_name.mid(6);
84✔
2304
    } else if (source_name.startsWith("TimeFrame: ")) {
343✔
2305
        source_name = source_name.mid(11);
343✔
2306
    }
2307

2308
    // Use the same grouping pattern as Feature_Tree_Widget
2309
    std::regex const pattern{_grouping_pattern};
861✔
2310
    std::smatch matches{};
861✔
2311
    std::string key = source_name.toStdString();
861✔
2312

2313
    if (std::regex_search(key, matches, pattern) && matches.size() > 1) {
861✔
UNCOV
2314
        return matches[1].str();
×
2315
    }
2316

2317
    return key;// Return the key itself if no match
861✔
2318
}
861✔
2319

UNCOV
2320
void TableDesignerWidget::onGroupModeToggled(bool enabled) {
×
UNCOV
2321
    _group_mode = enabled;
×
2322

2323
    // Update button text to reflect current mode
UNCOV
2324
    if (enabled) {
×
UNCOV
2325
        ui->group_mode_toggle_btn->setText("Group Mode");
×
UNCOV
2326
        ui->computers_info_label->setText("Select computers by checking the boxes. Similar data will be grouped and transformed together.");
×
2327
    } else {
UNCOV
2328
        ui->group_mode_toggle_btn->setText("Individual Mode");
×
UNCOV
2329
        ui->computers_info_label->setText("Select computers by checking the boxes. Each data source will be handled individually.");
×
2330
    }
2331

2332
    // Refresh the tree to apply the new grouping mode
UNCOV
2333
    refreshComputersTree();
×
UNCOV
2334
}
×
2335

2336
QWidget * TableDesignerWidget::createParameterWidget(QString const & computer_name,
254✔
2337
                                                     std::vector<std::unique_ptr<IParameterDescriptor>> const & parameter_descriptors) {
2338
    if (parameter_descriptors.empty()) {
254✔
UNCOV
2339
        return nullptr;
×
2340
    }
2341

2342
    auto * widget = new QWidget();
254✔
2343
    auto * layout = new QHBoxLayout(widget);
254✔
2344
    layout->setContentsMargins(2, 2, 2, 2);
254✔
2345
    layout->setSpacing(4);
254✔
2346

2347
    for (auto const & param_desc: parameter_descriptors) {
508✔
2348
        QString param_name = QString::fromStdString(param_desc->getName());
254✔
2349
        QString param_key = computer_name + "::" + param_name;
254✔
2350

2351
        // Add parameter label
2352
        auto * label = new QLabel(QString::fromStdString(param_desc->getName()) + ":");
254✔
2353
        label->setToolTip(QString::fromStdString(param_desc->getDescription()));
254✔
2354
        layout->addWidget(label);
254✔
2355

2356
        if (param_desc->getUIHint() == "enum") {
254✔
2357
            // Create combo box for enum parameters
2358
            auto * combo = new QComboBox();
170✔
2359
            combo->setObjectName(param_key);// Store parameter key for retrieval
170✔
2360

2361
            auto ui_props = param_desc->getUIProperties();
170✔
2362
            QString options_str = QString::fromStdString(ui_props["options"]);
510✔
2363
            QString default_value = QString::fromStdString(ui_props["default"]);
510✔
2364

2365
            QStringList options = options_str.split(',', Qt::SkipEmptyParts);
170✔
2366
            combo->addItems(options);
170✔
2367

2368
            // Set default value
2369
            int default_index = combo->findText(default_value);
170✔
2370
            if (default_index >= 0) {
170✔
2371
                combo->setCurrentIndex(default_index);
170✔
2372
            }
2373

2374
            combo->setToolTip(QString::fromStdString(param_desc->getDescription()));
170✔
2375
            layout->addWidget(combo);
170✔
2376

2377
            // Store the widget for parameter retrieval
2378
            _parameter_controls[param_key.toStdString()] = combo;
170✔
2379

2380
        } else if (param_desc->getUIHint() == "number") {
254✔
2381
            // Create spin box for numeric parameters
2382
            auto * spinbox = new QSpinBox();
84✔
2383
            spinbox->setObjectName(param_key);
84✔
2384

2385
            auto ui_props = param_desc->getUIProperties();
84✔
2386
            QString default_str = QString::fromStdString(ui_props["default"]);
252✔
2387
            QString min_str = QString::fromStdString(ui_props["min"]);
252✔
2388
            QString max_str = QString::fromStdString(ui_props["max"]);
252✔
2389

2390
            if (!min_str.isEmpty()) spinbox->setMinimum(min_str.toInt());
84✔
2391
            if (!max_str.isEmpty()) spinbox->setMaximum(max_str.toInt());
84✔
2392
            if (!default_str.isEmpty()) spinbox->setValue(default_str.toInt());
84✔
2393

2394
            spinbox->setToolTip(QString::fromStdString(param_desc->getDescription()));
84✔
2395
            layout->addWidget(spinbox);
84✔
2396

2397
            _parameter_controls[param_key.toStdString()] = spinbox;
84✔
2398

2399
        } else {
84✔
2400
            // Default to text input
UNCOV
2401
            auto * lineedit = new QLineEdit();
×
UNCOV
2402
            lineedit->setObjectName(param_key);
×
2403

UNCOV
2404
            auto ui_props = param_desc->getUIProperties();
×
UNCOV
2405
            QString default_value = QString::fromStdString(ui_props["default"]);
×
UNCOV
2406
            lineedit->setText(default_value);
×
2407

UNCOV
2408
            lineedit->setToolTip(QString::fromStdString(param_desc->getDescription()));
×
UNCOV
2409
            layout->addWidget(lineedit);
×
2410

UNCOV
2411
            _parameter_controls[param_key.toStdString()] = lineedit;
×
UNCOV
2412
        }
×
2413
    }
254✔
2414

2415
    return widget;
254✔
2416
}
2417

2418
std::map<std::string, std::string> TableDesignerWidget::getParameterValues(QString const & computer_name) const {
50✔
2419
    std::map<std::string, std::string> parameters;
50✔
2420

2421
    // Look for parameter controls with this computer name prefix
2422
    QString prefix = computer_name + "::";
50✔
2423

2424
    for (auto const & [key, widget]: _parameter_controls) {
150✔
2425
        QString key_str = QString::fromStdString(key);
100✔
2426
        if (key_str.startsWith(prefix)) {
100✔
2427
            QString param_name = key_str.mid(prefix.length());
6✔
2428

2429
            if (auto * combo = qobject_cast<QComboBox *>(widget)) {
6✔
UNCOV
2430
                parameters[param_name.toStdString()] = combo->currentText().toStdString();
×
2431
            } else if (auto * spinbox = qobject_cast<QSpinBox *>(widget)) {
6✔
2432
                parameters[param_name.toStdString()] = QString::number(spinbox->value()).toStdString();
6✔
UNCOV
2433
            } else if (auto * lineedit = qobject_cast<QLineEdit *>(widget)) {
×
UNCOV
2434
                parameters[param_name.toStdString()] = lineedit->text().toStdString();
×
2435
            }
2436
        }
6✔
2437
    }
100✔
2438

2439
    return parameters;
50✔
2440
}
50✔
2441

2442
// Explicit template instantiations for formatVectorForCsv
2443
template void TableDesignerWidget::formatVectorForCsv<double>(std::ofstream & file, std::vector<double> const & values, int precision);
2444
template void TableDesignerWidget::formatVectorForCsv<int>(std::ofstream & file, std::vector<int> const & values, int precision);
2445
template void TableDesignerWidget::formatVectorForCsv<float>(std::ofstream & file, std::vector<float> const & values, int precision);
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