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

paulmthompson / WhiskerToolbox / 18840223125

27 Oct 2025 12:01PM UTC coverage: 73.058% (+0.2%) from 72.822%
18840223125

push

github

paulmthompson
fix failing tests from table designer redesign

69 of 74 new or added lines in 2 files covered. (93.24%)

669 existing lines in 10 files now uncovered.

56029 of 76691 relevant lines covered (73.06%)

45039.63 hits per line

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

58.06
/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)
23✔
65
    : QWidget(parent),
66
      ui(new Ui::TableDesignerWidget),
46✔
67
      _data_manager(std::move(data_manager)) {
69✔
68

69
    ui->setupUi(this);
23✔
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;
23✔
75
    _parameter_layout = nullptr;
23✔
76

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

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

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

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

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

98
    // Connect table viewer signals for better integration
99
    connect(_table_viewer, &TableViewerWidget::rowScrolled, this, [this](size_t row_index) {
23✔
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)
103
    });
×
104

105
    connectSignals();
23✔
106

107

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

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

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

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

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

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

156
TableDesignerWidget::~TableDesignerWidget() {
23✔
157
    delete ui;
23✔
158
}
23✔
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✔
167
        loadTableInfo(_current_table_id);
×
168
    }
169
}
4✔
170

171

172
void TableDesignerWidget::connectSignals() {
23✔
173
    // Table selection signals
174
    connect(ui->table_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
69✔
175
            this, &TableDesignerWidget::onTableSelectionChanged);
46✔
176
    connect(ui->new_table_btn, &QPushButton::clicked,
69✔
177
            this, &TableDesignerWidget::onCreateNewTable);
46✔
178
    connect(ui->delete_table_btn, &QPushButton::clicked,
69✔
179
            this, &TableDesignerWidget::onDeleteTable);
46✔
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),
69✔
185
            this, &TableDesignerWidget::onRowDataSourceChanged);
46✔
186
    connect(ui->capture_range_spinbox, QOverload<int>::of(&QSpinBox::valueChanged),
69✔
187
            this, &TableDesignerWidget::onCaptureRangeChanged);
46✔
188
    connect(ui->interval_beginning_radio, &QRadioButton::toggled,
69✔
189
            this, &TableDesignerWidget::onIntervalSettingChanged);
46✔
190
    connect(ui->interval_end_radio, &QRadioButton::toggled,
69✔
191
            this, &TableDesignerWidget::onIntervalSettingChanged);
46✔
192
    connect(ui->interval_itself_radio, &QRadioButton::toggled,
69✔
193
            this, &TableDesignerWidget::onIntervalSettingChanged);
46✔
194

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

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

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

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

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

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

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

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

258
    qDebug() << "Selected table:" << table_id;
19✔
259
}
42✔
260

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

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

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

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) {
×
278
                ui->table_combo->setCurrentIndex(i);
×
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() {
×
288
    if (_current_table_id.isEmpty()) {
×
289
        return;
×
290
    }
291

292
    auto reply = QMessageBox::question(this, "Delete Table",
×
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) {
×
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 {
302
            QMessageBox::warning(this, "Error", "Failed to delete table: " + _current_table_id);
×
303
        }
304
    }
305
}
306

307
void TableDesignerWidget::onRowDataSourceChanged() {
73✔
308
    QString selected = ui->row_data_source_combo->currentText();
73✔
309
    if (selected.isEmpty()) {
73✔
310
        ui->row_info_label->setText("No row source selected");
22✔
311
        return;
22✔
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) {
51✔
317
        if (auto * reg = _data_manager->getTableRegistry()) {
15✔
318
            reg->updateTableRowSource(_current_table_id.toStdString(), selected.toStdString());
15✔
319
        }
320
    }
321

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

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

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

331
    qDebug() << "Row data source changed to:" << selected;
51✔
332
    triggerPreviewDebounced();
51✔
333
}
73✔
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();
×
338
    if (!selected.isEmpty()) {
×
339
        updateRowInfoLabel(selected);
×
340
    }
341
    triggerPreviewDebounced();
×
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()) {
×
359
        updateBuildStatus("No table selected", true);
×
360
        return;
×
361
    }
362

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

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

376
    try {
377
        // Create the row selector
378
        auto row_selector = createRowSelector(row_source);
×
379
        if (!row_selector) {
×
380
            updateBuildStatus("Failed to create row selector", true);
×
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) {
×
388
            updateBuildStatus("DataManager extension not available", true);
×
389
            return;
×
390
        }
391

392
        // Create the TableViewBuilder
393
        TableViewBuilder builder(data_manager_extension);
×
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);
×
401
                all_columns_valid = false;
×
402
                break;
×
403
            }
404
        }
405

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

410
        // Build the table
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());
×
417
            table_info.columns = column_infos;// Store the current enabled columns
×
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)))) {
×
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
            }
430
        } else {
×
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();
×
437
    }
×
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✔
443
        updateBuildStatus("No table selected", true);
×
444
        return false;
×
445
    }
446

447
    QString row_source = ui->row_data_source_combo->currentText();
4✔
448
    if (row_source.isEmpty()) {
4✔
449
        updateBuildStatus("No row data source selected", true);
×
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✔
456
        updateBuildStatus("No computers enabled. Check boxes in the tree to enable computers.", true);
×
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✔
464
            updateBuildStatus("Failed to create row selector", true);
×
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✔
472
            updateBuildStatus("DataManager extension not available", true);
×
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);
×
485
                all_columns_valid = false;
×
486
                break;
×
487
            }
488
        }
489

490
        if (!all_columns_valid) {
4✔
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 {
514
                updateBuildStatus("Failed to store built table", true);
×
515
                return false;
×
516
            }
517
        } else {
4✔
518
            updateBuildStatus("Registry unavailable", true);
×
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();
×
525
        return false;
×
526
    }
×
527
}
4✔
528

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

535
    // Fetch the built base table
536
    auto * reg = _data_manager->getTableRegistry();
×
537
    if (!reg) {
×
538
        updateBuildStatus("Registry unavailable", true);
×
539
        return;
×
540
    }
541
    auto base_view = reg->getBuiltTable(_current_table_id.toStdString());
×
542
    if (!base_view) {
×
543
        updateBuildStatus("Build the base table first", true);
×
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") {
×
550
        updateBuildStatus("Unsupported transform", true);
×
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) {
×
559
        for (auto const & s: _table_transform_widget->getIncludeColumns()) cfg.include.push_back(s);
×
560
        for (auto const & s: _table_transform_widget->getExcludeColumns()) cfg.exclude.push_back(s);
×
561
    }
562

563
    try {
564
        PCATransform pca(cfg);
×
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();
×
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());
×
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)))) {
×
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);
×
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
    }
×
595
    return out;
×
596
}
×
597

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

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

615
    // CSV options from export widget
616
    QString delimiter = _table_export_widget ? _table_export_widget->getDelimiterText() : "Comma";
×
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

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

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
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
651
        QString filename = promptSaveCsvFilename();
×
652
        if (filename.isEmpty()) return;
×
653
        if (!filename.endsWith(".csv", Qt::CaseInsensitive)) filename += ".csv";
×
654
        
655
        if (exportTableToSingleCsv(view.get(), filename, delim, eol, precision, includeHeader)) {
×
656
            updateBuildStatus(QString("Exported CSV: %1").arg(filename));
×
657
        } else {
658
            updateBuildStatus("Export failed", true);
×
659
        }
660
    }
×
661
}
×
662

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

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

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());
×
676
        if (!file.is_open()) {
×
677
            return false;
×
678
        }
679
        file << std::fixed << std::setprecision(precision);
×
680

681
        auto names = view->getColumnNames();
×
682
        if (includeHeader) {
×
683
            for (size_t i = 0; i < names.size(); ++i) {
×
684
                if (i > 0) file << delim;
×
685
                file << names[i];
×
686
            }
687
            file << eol;
×
688
        }
689
        
690
        size_t rows = view->getRowCount();
×
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 {
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>)
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>)
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
731
                            formatVectorForCsv(file, vec[r], precision);
×
732
                        } else {
733
                            file << "?";
×
734
                        }
735
                    });
736
                } catch (...) {
×
737
                    file << "Error";
×
738
                }
×
739
            }
740
            file << eol;
×
741
        }
742
        file.close();
×
743
        return true;
×
744
    } catch (std::exception const & e) {
×
745
        qWarning() << "Export failed:" << e.what();
×
746
        return false;
×
747
    }
×
748
}
749

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) {
754
    if (!view || !_data_manager) {
×
755
        return 0;
×
756
    }
757

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

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

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

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

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

797
    int files_exported = 0;
×
798

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

807
        // Convert to unordered_set for fast lookup
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
814
            bool has_match = false;
×
815
            for (auto const & entity_id : all_entity_ids[r]) {
×
816
                if (group_entity_set.count(entity_id) > 0) {
×
817
                    has_match = true;
×
818
                    break;
×
819
                }
820
            }
821
            if (has_match) {
×
822
                matching_rows.push_back(r);
×
823
            }
824
        }
825

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

831
        // Create filename for this group
832
        QString group_name = QString::fromStdString(group_desc.name);
×
833
        // Sanitize group name for filename (replace invalid characters)
834
        group_name = group_name.replace(QRegularExpression("[^a-zA-Z0-9_\\-]"), "_");
×
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 {
839
            std::ofstream file(filename.toStdString());
×
840
            if (!file.is_open()) {
×
841
                qWarning() << "Could not open file:" << filename;
×
842
                continue;
×
843
            }
844
            file << std::fixed << std::setprecision(precision);
×
845

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

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

860
                    try {
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

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

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

914
    return files_exported;
×
915
}
×
916

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

924
    file << "[";
×
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 {
929
            file << values[i];
×
930
        }
931
        if (i + 1 < values.size()) file << ",";
×
932
    }
933
    file << "]";
×
934
}
935

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

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

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

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

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

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

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

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

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

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

1000
void TableDesignerWidget::refreshRowDataSourceCombo() {
27✔
1001
    // Preserve current selection if possible
1002
    QString previous_selection;
27✔
1003
    if (ui->row_data_source_combo && ui->row_data_source_combo->count() > 0) {
27✔
1004
        previous_selection = ui->row_data_source_combo->currentText();
4✔
1005
    }
1006

1007
    ui->row_data_source_combo->clear();
27✔
1008

1009
    if (!_data_manager) {
27✔
1010
        qDebug() << "refreshRowDataSourceCombo: No table manager";
×
1011
        return;
×
1012
    }
1013

1014
    auto data_sources = getAvailableDataSources();
27✔
1015
    qDebug() << "refreshRowDataSourceCombo: Found" << data_sources.size() << "data sources:" << data_sources;
27✔
1016

1017
    for (QString const & source: data_sources) {
303✔
1018
        // Only include valid row sources in this combo
1019
        if (source.startsWith("TimeFrame: ") || source.startsWith("Events: ") || source.startsWith("Intervals: ")) {
276✔
1020
            ui->row_data_source_combo->addItem(source);
195✔
1021
        }
1022
    }
1023

1024
    // Try to restore previous selection if still available
1025
    if (!previous_selection.isEmpty()) {
27✔
1026
        for (int i = 0; i < ui->row_data_source_combo->count(); ++i) {
19✔
1027
            if (ui->row_data_source_combo->itemText(i) == previous_selection) {
19✔
1028
                ui->row_data_source_combo->setCurrentIndex(i);
4✔
1029
                break;
4✔
1030
            }
1031
        }
1032
    }
1033

1034
    if (ui->row_data_source_combo->count() == 0) {
27✔
1035
        ui->row_data_source_combo->addItem("(No data sources available)");
×
1036
        qDebug() << "refreshRowDataSourceCombo: No data sources available";
×
1037
    }
1038
}
27✔
1039

1040

1041
void TableDesignerWidget::loadTableInfo(QString const & table_id) {
38✔
1042
    if (table_id.isEmpty() || !_data_manager) {
38✔
1043
        clearUI();
×
1044
        return;
×
1045
    }
1046

1047
    auto * reg = _data_manager->getTableRegistry();
38✔
1048
    auto info = reg ? reg->getTableInfo(table_id.toStdString()) : TableInfo{};
38✔
1049
    if (info.id.empty()) {
38✔
1050
        clearUI();
×
1051
        return;
×
1052
    }
1053

1054
    // Load table information
1055
    if (_table_info_widget) {
38✔
1056
        _table_info_widget->setName(QString::fromStdString(info.name));
38✔
1057
        _table_info_widget->setDescription(QString::fromStdString(info.description));
38✔
1058
    }
1059

1060
    // Load row source if available
1061
    if (!info.rowSourceName.empty()) {
38✔
1062
        int row_index = ui->row_data_source_combo->findText(QString::fromStdString(info.rowSourceName));
20✔
1063
        if (row_index >= 0) {
20✔
1064
            // Block signals to prevent circular dependency when loading table info
1065
            ui->row_data_source_combo->blockSignals(true);
20✔
1066
            ui->row_data_source_combo->setCurrentIndex(row_index);
20✔
1067
            ui->row_data_source_combo->blockSignals(false);
20✔
1068

1069
            // Manually update the info label without triggering the signal handler
1070
            updateRowInfoLabel(QString::fromStdString(info.rowSourceName));
20✔
1071

1072
            // Update interval settings visibility
1073
            updateIntervalSettingsVisibility();
20✔
1074

1075
            // Since signals were blocked, this will ensure the tree is refreshed
1076
            // when the computers tree is populated later in this function
1077
        }
1078
    }
1079

1080
    // Clear old column list (deprecated)
1081
    // The computers tree will be populated based on available data sources
1082
    refreshComputersTree();
38✔
1083

1084
    updateBuildStatus(QString("Loaded table: %1").arg(QString::fromStdString(info.name)));
38✔
1085
    triggerPreviewDebounced();
38✔
1086
}
38✔
1087

1088
void TableDesignerWidget::clearUI() {
64✔
1089
    _current_table_id.clear();
64✔
1090

1091
    // Clear table info
1092
    if (_table_info_widget) {
64✔
1093
        _table_info_widget->setName("");
64✔
1094
        _table_info_widget->setDescription("");
64✔
1095
    }
1096

1097
    // Clear row source
1098
    ui->row_data_source_combo->setCurrentIndex(-1);
64✔
1099
    ui->row_info_label->setText("No row source selected");
64✔
1100

1101
    // Reset capture range and interval settings
1102
    setCaptureRange(30000);// Default value
64✔
1103
    if (ui->interval_beginning_radio) {
64✔
1104
        ui->interval_beginning_radio->setChecked(true);
64✔
1105
    }
1106
    if (ui->interval_itself_radio) {
64✔
1107
        ui->interval_itself_radio->setChecked(false);
64✔
1108
    }
1109
    if (ui->interval_settings_group) {
64✔
1110
        ui->interval_settings_group->setVisible(false);
64✔
1111
    }
1112

1113
    // Clear computers tree
1114
    if (ui->computers_tree) {
64✔
1115
        ui->computers_tree->clear();
64✔
1116
    }
1117

1118
    // Disable controls
1119
    ui->delete_table_btn->setEnabled(false);
64✔
1120
    // Table info section is controlled separately
1121
    ui->build_table_btn->setEnabled(false);
64✔
1122
    if (auto gb = this->findChild<QGroupBox *>("row_source_group")) gb->setEnabled(false);
128✔
1123
    if (auto gb = this->findChild<QGroupBox *>("column_design_group")) gb->setEnabled(false);
128✔
1124
    if (_table_info_section) _table_info_section->setEnabled(false);
64✔
1125

1126
    updateBuildStatus("No table selected");
64✔
1127
    if (_table_viewer) _table_viewer->clearTable();
64✔
1128
}
64✔
1129

1130
void TableDesignerWidget::updateBuildStatus(QString const & message, bool is_error) {
125✔
1131
    ui->build_status_label->setText(message);
125✔
1132

1133
    if (is_error) {
125✔
1134
        ui->build_status_label->setStyleSheet("QLabel { color: red; font-weight: bold; }");
×
1135
    } else {
1136
        ui->build_status_label->setStyleSheet("QLabel { color: green; }");
125✔
1137
    }
1138
}
125✔
1139

1140
QStringList TableDesignerWidget::getAvailableDataSources() const {
130✔
1141
    QStringList sources;
130✔
1142

1143
    if (!_data_manager) {
130✔
1144
        qDebug() << "getAvailableDataSources: No table manager";
×
1145
        return sources;
×
1146
    }
1147

1148
    auto * reg = _data_manager->getTableRegistry();
130✔
1149
    auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
130✔
1150
    if (!data_manager_extension) {
130✔
1151
        qDebug() << "getAvailableDataSources: No data manager extension";
×
1152
        return sources;
×
1153
    }
1154

1155
    if (!_data_manager) {
130✔
1156
        qDebug() << "getAvailableDataSources: No data manager";
×
1157
        return sources;
×
1158
    }
1159

1160
    // Add TimeFrame keys as potential row sources
1161
    // TimeFrames can define intervals for analysis
1162
    auto timeframe_keys = _data_manager->getTimeFrameKeys();
130✔
1163
    qDebug() << "getAvailableDataSources: TimeFrame keys:" << timeframe_keys.size();
130✔
1164
    for (auto const & key: timeframe_keys) {
660✔
1165
        QString source = QString("TimeFrame: %1").arg(QString::fromStdString(key.str()));
530✔
1166
        sources << source;
530✔
1167
        qDebug() << "  Added TimeFrame:" << source;
530✔
1168
    }
530✔
1169

1170
    // Add DigitalEventSeries keys as potential row sources
1171
    // Events can be used to define analysis windows or timestamps
1172
    auto event_keys = _data_manager->getKeys<DigitalEventSeries>();
130✔
1173
    qDebug() << "getAvailableDataSources: Event keys:" << event_keys.size();
130✔
1174
    for (auto const & key: event_keys) {
394✔
1175
        QString source = QString("Events: %1").arg(QString::fromStdString(key));
264✔
1176
        sources << source;
264✔
1177
        qDebug() << "  Added Events:" << source;
264✔
1178
    }
264✔
1179

1180
    // Add DigitalIntervalSeries keys as potential row sources
1181
    // Intervals directly define analysis windows
1182
    auto interval_keys = _data_manager->getKeys<DigitalIntervalSeries>();
130✔
1183
    qDebug() << "getAvailableDataSources: Interval keys:" << interval_keys.size();
130✔
1184
    for (auto const & key: interval_keys) {
276✔
1185
        QString source = QString("Intervals: %1").arg(QString::fromStdString(key));
146✔
1186
        sources << source;
146✔
1187
        qDebug() << "  Added Intervals:" << source;
146✔
1188
    }
146✔
1189

1190
    // Add AnalogTimeSeries keys as data sources (for computers; not row selectors)
1191
    auto analog_keys = _data_manager->getKeys<AnalogTimeSeries>();
130✔
1192
    qDebug() << "getAvailableDataSources: Analog keys:" << analog_keys.size();
130✔
1193
    for (auto const & key: analog_keys) {
390✔
1194
        QString source = QString("analog:%1").arg(QString::fromStdString(key));
260✔
1195
        sources << source;
260✔
1196
        qDebug() << "  Added Analog:" << source;
260✔
1197
    }
260✔
1198

1199
    // Add LineData keys as data sources (for computers; not row selectors)
1200
    auto line_keys = _data_manager->getKeys<LineData>();
130✔
1201
    qDebug() << "getAvailableDataSources: Line keys:" << line_keys.size();
130✔
1202
    for (auto const & key: line_keys) {
260✔
1203
        QString source = QString("lines:%1").arg(QString::fromStdString(key));
130✔
1204
        sources << source;
130✔
1205
        qDebug() << "  Added Lines:" << source;
130✔
1206
    }
130✔
1207

1208
    qDebug() << "getAvailableDataSources: Total sources found:" << sources.size();
130✔
1209

1210
    return sources;
130✔
1211
}
130✔
1212

1213
std::pair<std::optional<DataSourceVariant>, RowSelectorType>
UNCOV
1214
TableDesignerWidget::createDataSourceVariant(QString const & data_source_string,
×
1215
                                             std::shared_ptr<DataManagerExtension> data_manager_extension) const {
UNCOV
1216
    std::optional<DataSourceVariant> result;
×
UNCOV
1217
    RowSelectorType row_selector_type = RowSelectorType::IntervalBased;
×
1218

UNCOV
1219
    if (data_source_string.startsWith("TimeFrame: ")) {
×
1220
        // TimeFrames are used with TimestampSelector for rows; no concrete data source needed
UNCOV
1221
        row_selector_type = RowSelectorType::Timestamp;
×
1222

UNCOV
1223
    } else if (data_source_string.startsWith("Events: ")) {
×
UNCOV
1224
        QString source_name = data_source_string.mid(8);// Remove "Events: " prefix
×
1225
        // Event-based computers in the registry operate with interval rows
UNCOV
1226
        row_selector_type = RowSelectorType::IntervalBased;
×
1227

UNCOV
1228
        if (auto event_source = data_manager_extension->getEventSource(source_name.toStdString())) {
×
UNCOV
1229
            result = event_source;
×
UNCOV
1230
        }
×
1231

UNCOV
1232
    } else if (data_source_string.startsWith("Intervals: ")) {
×
UNCOV
1233
        QString source_name = data_source_string.mid(11);// Remove "Intervals: " prefix
×
UNCOV
1234
        row_selector_type = RowSelectorType::IntervalBased;
×
1235

UNCOV
1236
        if (auto interval_source = data_manager_extension->getIntervalSource(source_name.toStdString())) {
×
UNCOV
1237
            result = interval_source;
×
UNCOV
1238
        }
×
UNCOV
1239
    } else if (data_source_string.startsWith("analog:")) {
×
UNCOV
1240
        QString source_name = data_source_string.mid(7);// Remove "analog:" prefix
×
UNCOV
1241
        row_selector_type = RowSelectorType::IntervalBased;
×
1242

UNCOV
1243
        if (auto analog_source = data_manager_extension->getAnalogSource(source_name.toStdString())) {
×
UNCOV
1244
            result = analog_source;
×
UNCOV
1245
        }
×
UNCOV
1246
    } else if (data_source_string.startsWith("lines:")) {
×
UNCOV
1247
        QString source_name = data_source_string.mid(6);// Remove "lines:" prefix
×
UNCOV
1248
        row_selector_type = RowSelectorType::Timestamp; // LineData computers work with timestamp row selectors
×
1249

UNCOV
1250
        if (auto line_source = data_manager_extension->getLineSource(source_name.toStdString())) {
×
UNCOV
1251
            result = line_source;
×
UNCOV
1252
        }
×
UNCOV
1253
    }
×
1254

UNCOV
1255
    return {result, row_selector_type};
×
UNCOV
1256
}
×
1257

1258
std::optional<DataSourceVariant>
1259
TableDesignerWidget::createColumnDataSourceVariant(QString const & data_source_string,
1,058✔
1260
                                                   std::shared_ptr<DataManagerExtension> data_manager_extension) const {
1261
    std::optional<DataSourceVariant> result;
1,058✔
1262

1263
    if (data_source_string.startsWith("Events: ")) {
1,058✔
1264
        QString source_name = data_source_string.mid(8);// Remove "Events: " prefix
213✔
1265
        if (auto event_source = data_manager_extension->getEventSource(source_name.toStdString())) {
213✔
1266
            result = event_source;
213✔
1267
        }
213✔
1268

1269
    } else if (data_source_string.startsWith("Intervals: ")) {
1,058✔
1270
        QString source_name = data_source_string.mid(11);// Remove "Intervals: " prefix
116✔
1271
        if (auto interval_source = data_manager_extension->getIntervalSource(source_name.toStdString())) {
116✔
1272
            result = interval_source;
116✔
1273
        }
116✔
1274

1275
    } else if (data_source_string.startsWith("analog:")) {
845✔
1276
        QString source_name = data_source_string.mid(7);// Remove "analog:" prefix
206✔
1277
        if (auto analog_source = data_manager_extension->getAnalogSource(source_name.toStdString())) {
206✔
1278
            result = analog_source;
206✔
1279
        }
206✔
1280

1281
    } else if (data_source_string.startsWith("lines:")) {
729✔
1282
        QString source_name = data_source_string.mid(6);// Remove "lines:" prefix
103✔
1283
        if (auto line_source = data_manager_extension->getLineSource(source_name.toStdString())) {
103✔
1284
            result = line_source;
103✔
1285
        }
103✔
1286
    }
103✔
1287

1288
    return result;
1,058✔
UNCOV
1289
}
×
1290

1291
std::optional<RowSelectorType> TableDesignerWidget::getCurrentRowSelectorType() const {
124✔
1292
    QString row_source = ui->row_data_source_combo->currentText();
124✔
1293
    
1294
    if (row_source.isEmpty()) {
124✔
1295
        return std::nullopt;
17✔
1296
    }
1297

1298
    if (row_source.startsWith("TimeFrame: ")) {
107✔
1299
        return RowSelectorType::Timestamp;
63✔
1300
    } else if (row_source.startsWith("Events: ")) {
44✔
1301
        return RowSelectorType::Timestamp;  // Events create timestamp rows
2✔
1302
    } else if (row_source.startsWith("Intervals: ")) {
42✔
1303
        return RowSelectorType::IntervalBased;
42✔
1304
    }
1305

1306
    return std::nullopt;
×
1307
}
124✔
1308

1309
void TableDesignerWidget::updateRowInfoLabel(QString const & selected_source) {
73✔
1310
    if (selected_source.isEmpty()) {
73✔
UNCOV
1311
        ui->row_info_label->setText("No row source selected");
×
UNCOV
1312
        return;
×
1313
    }
1314

1315
    // Parse the selected source to get type and name
1316
    QString source_type;
73✔
1317
    QString source_name;
73✔
1318

1319
    if (selected_source.startsWith("TimeFrame: ")) {
73✔
1320
        source_type = "TimeFrame";
38✔
1321
        source_name = selected_source.mid(11);// Remove "TimeFrame: " prefix
38✔
1322
    } else if (selected_source.startsWith("Events: ")) {
35✔
1323
        source_type = "Events";
2✔
1324
        source_name = selected_source.mid(8);// Remove "Events: " prefix
2✔
1325
    } else if (selected_source.startsWith("Intervals: ")) {
33✔
1326
        source_type = "Intervals";
33✔
1327
        source_name = selected_source.mid(11);// Remove "Intervals: " prefix
33✔
1328
    }
1329

1330
    // Get additional information about the selected source
1331
    QString info_text = QString("Selected: %1 (%2)").arg(source_name, source_type);
73✔
1332

1333
    if (!_data_manager) {
73✔
UNCOV
1334
        ui->row_info_label->setText(info_text);
×
UNCOV
1335
        return;
×
1336
    }
1337

1338
    auto * reg3 = _data_manager->getTableRegistry();
73✔
1339
    auto data_manager_extension = reg3 ? reg3->getDataManagerExtension() : nullptr;
73✔
1340
    if (!data_manager_extension) {
73✔
UNCOV
1341
        ui->row_info_label->setText(info_text);
×
UNCOV
1342
        return;
×
1343
    }
1344

1345
    auto const source_name_str = source_name.toStdString();
73✔
1346

1347
    // Add specific information based on source type
1348
    if (source_type == "TimeFrame") {
73✔
1349
        auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
38✔
1350
        if (timeframe) {
38✔
1351
            info_text += QString(" - %1 time points").arg(timeframe->getTotalFrameCount());
38✔
1352
        }
1353
    } else if (source_type == "Events") {
73✔
1354
        auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
2✔
1355
        if (event_series) {
2✔
1356
            auto events = event_series->getEventSeries();
2✔
1357
            info_text += QString(" - %1 events").arg(events.size());
2✔
1358
        }
2✔
1359
    } else if (source_type == "Intervals") {
35✔
1360
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
33✔
1361
        if (interval_series) {
33✔
1362
            auto intervals = interval_series->getDigitalIntervalSeries();
33✔
1363
            info_text += QString(" - %1 intervals").arg(intervals.size());
33✔
1364

1365
            // Add capture range and interval setting information
1366
            if (isIntervalItselfSelected()) {
33✔
1367
                info_text += QString("\nUsing intervals as-is (no capture range)");
2✔
1368
            } else {
1369
                int capture_range = getCaptureRange();
31✔
1370
                QString interval_point = isIntervalBeginningSelected() ? "beginning" : "end";
31✔
1371
                info_text += QString("\nCapture range: ±%1 samples around %2 of intervals").arg(capture_range).arg(interval_point);
31✔
1372
            }
31✔
1373
        }
33✔
1374
    }
33✔
1375

1376
    ui->row_info_label->setText(info_text);
73✔
1377
}
73✔
1378

1379
std::unique_ptr<IRowSelector> TableDesignerWidget::createRowSelector(QString const & row_source) {
44✔
1380
    // Parse the row source to get type and name
1381
    QString source_type;
44✔
1382
    QString source_name;
44✔
1383

1384
    if (row_source.startsWith("TimeFrame: ")) {
44✔
1385
        source_type = "TimeFrame";
5✔
1386
        source_name = row_source.mid(11);// Remove "TimeFrame: " prefix
5✔
1387
    } else if (row_source.startsWith("Events: ")) {
39✔
UNCOV
1388
        source_type = "Events";
×
UNCOV
1389
        source_name = row_source.mid(8);// Remove "Events: " prefix
×
1390
    } else if (row_source.startsWith("Intervals: ")) {
39✔
1391
        source_type = "Intervals";
39✔
1392
        source_name = row_source.mid(11);// Remove "Intervals: " prefix
39✔
1393
    } else {
1394
        qDebug() << "Unknown row source format:" << row_source;
×
UNCOV
1395
        return nullptr;
×
1396
    }
1397

1398
    auto const source_name_str = source_name.toStdString();
44✔
1399

1400
    try {
1401
        if (source_type == "TimeFrame") {
44✔
1402
            // Create IntervalSelector using TimeFrame
1403
            auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
5✔
1404
            if (!timeframe) {
5✔
UNCOV
1405
                qDebug() << "TimeFrame not found:" << source_name;
×
UNCOV
1406
                return nullptr;
×
1407
            }
1408

1409
            // Use timestamps to select all rows
1410
            std::vector<TimeFrameIndex> timestamps;
5✔
1411
            for (int64_t i = 0; i < timeframe->getTotalFrameCount(); ++i) {
510✔
1412
                timestamps.push_back(TimeFrameIndex(i));
505✔
1413
            }
1414
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe);
5✔
1415

1416
        } else if (source_type == "Events") {
44✔
1417
            // Create TimestampSelector using DigitalEventSeries
UNCOV
1418
            auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
UNCOV
1419
            if (!event_series) {
×
UNCOV
1420
                qDebug() << "DigitalEventSeries not found:" << source_name;
×
UNCOV
1421
                return nullptr;
×
1422
            }
1423

UNCOV
1424
            auto events = event_series->getEventSeries();
×
UNCOV
1425
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
×
UNCOV
1426
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
×
UNCOV
1427
            if (!timeframe_obj) {
×
UNCOV
1428
                qDebug() << "TimeFrame not found for events:" << timeframe_key.str();
×
UNCOV
1429
                return nullptr;
×
1430
            }
1431

1432
            // Convert events to TimeFrameIndex
UNCOV
1433
            std::vector<TimeFrameIndex> timestamps;
×
UNCOV
1434
            for (auto const & event: events) {
×
UNCOV
1435
                timestamps.push_back(TimeFrameIndex(static_cast<int64_t>(event)));
×
1436
            }
1437

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

1440
        } else if (source_type == "Intervals") {
39✔
1441
            // Create IntervalSelector using DigitalIntervalSeries with capture range
1442
            auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
39✔
1443
            if (!interval_series) {
39✔
UNCOV
1444
                qDebug() << "DigitalIntervalSeries not found:" << source_name;
×
1445
                return nullptr;
×
1446
            }
1447

1448
            auto intervals = interval_series->getDigitalIntervalSeries();
39✔
1449
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
39✔
1450
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
39✔
1451
            if (!timeframe_obj) {
39✔
1452
                qDebug() << "TimeFrame not found for intervals:" << timeframe_key.str();
×
1453
                return nullptr;
×
1454
            }
1455

1456
            // Get capture range and interval setting
1457
            int capture_range = getCaptureRange();
39✔
1458
            bool use_beginning = isIntervalBeginningSelected();
39✔
1459
            bool use_interval_itself = isIntervalItselfSelected();
39✔
1460

1461
            // Create intervals based on the selected option
1462
            std::vector<TimeFrameInterval> tf_intervals;
39✔
1463
            for (auto const & interval: intervals) {
193✔
1464
                if (use_interval_itself) {
154✔
1465
                    // Use the interval as-is
1466
                    tf_intervals.emplace_back(TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
6✔
1467
                } else {
1468
                    // Determine the reference point (beginning or end of interval)
1469
                    int64_t reference_point;
1470
                    if (use_beginning) {
148✔
1471
                        reference_point = interval.start;
148✔
1472
                    } else {
1473
                        reference_point = interval.end;
×
1474
                    }
1475

1476
                    // Create a new interval around the reference point
1477
                    int64_t start_point = reference_point - capture_range;
148✔
1478
                    int64_t end_point = reference_point + capture_range;
148✔
1479

1480
                    // Ensure bounds are within the timeframe
1481
                    start_point = std::max(start_point, int64_t(0));
148✔
1482
                    end_point = std::min(end_point, static_cast<int64_t>(timeframe_obj->getTotalFrameCount() - 1));
148✔
1483

1484
                    tf_intervals.emplace_back(TimeFrameIndex(start_point), TimeFrameIndex(end_point));
148✔
1485
                }
1486
            }
1487

1488
            return std::make_unique<IntervalSelector>(std::move(tf_intervals), timeframe_obj);
39✔
1489
        }
39✔
1490

UNCOV
1491
    } catch (std::exception const & e) {
×
UNCOV
1492
        qDebug() << "Exception creating row selector:" << e.what();
×
UNCOV
1493
        return nullptr;
×
UNCOV
1494
    }
×
1495

UNCOV
1496
    qDebug() << "Unsupported row source type:" << source_type;
×
UNCOV
1497
    return nullptr;
×
1498
}
44✔
1499

UNCOV
1500
bool TableDesignerWidget::addColumnToBuilder(TableViewBuilder & builder, ColumnInfo const & column_info) {
×
1501
    // Use the simplified TableRegistry method that handles all the type checking internally
UNCOV
1502
    auto * reg = _data_manager->getTableRegistry();
×
UNCOV
1503
    if (!reg) {
×
UNCOV
1504
        qDebug() << "TableRegistry not available";
×
UNCOV
1505
        return false;
×
1506
    }
1507

1508
    bool success = reg->addColumnToBuilder(builder, column_info);
×
UNCOV
1509
    if (!success) {
×
UNCOV
1510
        qDebug() << "Failed to add column to builder:" << QString::fromStdString(column_info.name);
×
1511
    }
1512

UNCOV
1513
    return success;
×
1514
}
1515

1516
void TableDesignerWidget::updateIntervalSettingsVisibility() {
73✔
1517
    if (!ui->interval_settings_group) {
73✔
UNCOV
1518
        return;
×
1519
    }
1520

1521
    QString selected_key = ui->row_data_source_combo->currentText();
73✔
1522
    if (selected_key.isEmpty()) {
73✔
1523
        ui->interval_settings_group->setVisible(false);
×
UNCOV
1524
        if (ui->capture_range_spinbox) {
×
UNCOV
1525
            ui->capture_range_spinbox->setEnabled(false);
×
1526
        }
UNCOV
1527
        return;
×
1528
    }
1529

1530
    if (!_data_manager) {
73✔
UNCOV
1531
        ui->interval_settings_group->setVisible(false);
×
UNCOV
1532
        if (ui->capture_range_spinbox) {
×
UNCOV
1533
            ui->capture_range_spinbox->setEnabled(false);
×
1534
        }
UNCOV
1535
        return;
×
1536
    }
1537

1538
    // Check if the selected source is an interval series
1539
    if (selected_key.startsWith("Intervals: ")) {
73✔
1540
        ui->interval_settings_group->setVisible(true);
33✔
1541

1542
        // Enable/disable capture range based on interval setting
1543
        if (ui->capture_range_spinbox) {
33✔
1544
            bool use_interval_itself = isIntervalItselfSelected();
33✔
1545
            ui->capture_range_spinbox->setEnabled(!use_interval_itself);
33✔
1546
        }
1547
    } else {
1548
        ui->interval_settings_group->setVisible(false);
40✔
1549
        if (ui->capture_range_spinbox) {
40✔
1550
            ui->capture_range_spinbox->setEnabled(false);
40✔
1551
        }
1552
    }
1553
}
73✔
1554

1555
int TableDesignerWidget::getCaptureRange() const {
70✔
1556
    if (ui->capture_range_spinbox) {
70✔
1557
        return ui->capture_range_spinbox->value();
70✔
1558
    }
UNCOV
1559
    return 30000;// Default value
×
1560
}
1561

1562
void TableDesignerWidget::setCaptureRange(int value) {
64✔
1563
    if (ui->capture_range_spinbox) {
64✔
1564
        ui->capture_range_spinbox->blockSignals(true);
64✔
1565
        ui->capture_range_spinbox->setValue(value);
64✔
1566
        ui->capture_range_spinbox->blockSignals(false);
64✔
1567
    }
1568
}
64✔
1569

1570
bool TableDesignerWidget::isIntervalBeginningSelected() const {
70✔
1571
    if (ui->interval_beginning_radio) {
70✔
1572
        return ui->interval_beginning_radio->isChecked();
70✔
1573
    }
UNCOV
1574
    return true;// Default to beginning
×
1575
}
1576

1577
bool TableDesignerWidget::isIntervalItselfSelected() const {
105✔
1578
    if (ui->interval_itself_radio) {
105✔
1579
        return ui->interval_itself_radio->isChecked();
105✔
1580
    }
UNCOV
1581
    return false;// Default to not selected
×
1582
}
1583

1584
void TableDesignerWidget::triggerPreviewDebounced() {
228✔
1585
    if (_preview_debounce_timer) _preview_debounce_timer->start();
228✔
1586
    // Also trigger an immediate rebuild to support non-interactive/test contexts
1587
    rebuildPreviewNow();
228✔
1588
}
228✔
1589

1590
void TableDesignerWidget::rebuildPreviewNow() {
228✔
1591
    if (!_data_manager || !_table_viewer) return;
228✔
1592
    if (_current_table_id.isEmpty()) {
228✔
1593
        _table_viewer->clearTable();
111✔
1594
        return;
111✔
1595
    }
1596

1597
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
117✔
1598
    if (row_source.isEmpty()) {
117✔
1599
        _table_viewer->clearTable();
17✔
1600
        return;
17✔
1601
    }
1602

1603
    // Get enabled column infos from the computers tree
1604
    auto column_infos = getEnabledColumnInfos();
100✔
1605
    if (column_infos.empty()) {
100✔
1606
        _table_viewer->clearTable();
60✔
1607
        return;
60✔
1608
    }
1609

1610
    // Create row selector for the entire dataset
1611
    auto selector = createRowSelector(row_source);
40✔
1612
    if (!selector) {
40✔
UNCOV
1613
        _table_viewer->clearTable();
×
UNCOV
1614
        return;
×
1615
    }
1616

1617
    // Apply any saved column order for this table id
1618
    auto desiredOrder = _table_column_order.value(_current_table_id);
40✔
1619
    if (!desiredOrder.isEmpty()) {
40✔
1620
        std::vector<ColumnInfo> reordered;
32✔
1621
        reordered.reserve(column_infos.size());
32✔
1622
        for (auto const & name: desiredOrder) {
89✔
1623
            auto it = std::find_if(column_infos.begin(), column_infos.end(), [&](ColumnInfo const & ci) { return QString::fromStdString(ci.name) == name; });
127✔
1624
            if (it != column_infos.end()) {
57✔
1625
                reordered.push_back(*it);
39✔
1626
            }
1627
        }
1628
        for (auto const & ci: column_infos) {
78✔
1629
            if (std::find_if(reordered.begin(), reordered.end(), [&](ColumnInfo const & x) { return x.name == ci.name; }) == reordered.end()) {
103✔
1630
                reordered.push_back(ci);
7✔
1631
            }
1632
        }
1633
        column_infos = std::move(reordered);
32✔
1634
    }
32✔
1635

1636
    // Set up the table viewer with pagination
1637
    _table_viewer->setTableConfiguration(
200✔
1638
            std::move(selector),
40✔
1639
            std::move(column_infos),
40✔
1640
            _data_manager,
40✔
1641
            QString("Preview: %1").arg(_current_table_id),
80✔
1642
            row_source);
1643

1644
    // Capture the current visual order from the viewer
1645
    QStringList currentOrder;
40✔
1646
    if (_table_viewer) {
40✔
1647
        auto * tv = _table_viewer->findChild<QTableView *>();
40✔
1648
        if (tv && tv->model()) {
40✔
1649
            auto * header = tv->horizontalHeader();
40✔
1650
            int cols = tv->model()->columnCount();
40✔
1651
            for (int v = 0; header && v < cols; ++v) {
114✔
1652
                int logical = header->logicalIndex(v);
74✔
1653
                auto name = tv->model()->headerData(logical, Qt::Horizontal, Qt::DisplayRole).toString();
74✔
1654
                currentOrder.push_back(name);
74✔
1655
            }
74✔
1656
        }
1657
    }
1658
    if (!currentOrder.isEmpty()) {
40✔
1659
        _table_column_order[_current_table_id] = currentOrder;
40✔
1660
    }
1661
}
177✔
1662

1663
void TableDesignerWidget::refreshComputersTree() {
120✔
1664
    if (!_data_manager) return;
120✔
1665

1666
    _updating_computers_tree = true;
120✔
1667

1668
    // Preserve previous checkbox states and custom column names
1669
    std::map<std::string, std::pair<Qt::CheckState, QString>> previous_states;
120✔
1670
    if (ui->computers_tree && ui->computers_tree->topLevelItemCount() > 0) {
120✔
1671
        for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
358✔
1672
            auto * top_level_item = ui->computers_tree->topLevelItem(i);
293✔
1673
            // Handle both grouped and individual modes
1674
            for (int j = 0; j < top_level_item->childCount(); ++j) {
1,302✔
1675
                auto * child_item = top_level_item->child(j);
1,009✔
1676
                if (child_item->childCount() > 0) {
1,009✔
1677
                    // This is a computer item under a group/data source
UNCOV
1678
                    for (int k = 0; k < child_item->childCount(); ++k) {
×
UNCOV
1679
                        auto * computer_item = child_item->child(k);
×
UNCOV
1680
                        QString ds = computer_item->data(0, Qt::UserRole).toString();
×
UNCOV
1681
                        QString cn = computer_item->data(1, Qt::UserRole).toString();
×
UNCOV
1682
                        std::string key = (ds + "||" + cn).toStdString();
×
1683
                        previous_states[key] = {computer_item->checkState(1), computer_item->text(2)};
×
1684
                    }
×
1685
                } else {
1686
                    // This is a computer item directly under data source (individual mode)
1687
                    QString ds = child_item->data(0, Qt::UserRole).toString();
1,009✔
1688
                    QString cn = child_item->data(1, Qt::UserRole).toString();
1,009✔
1689
                    std::string key = (ds + "||" + cn).toStdString();
1,009✔
1690
                    previous_states[key] = {child_item->checkState(1), child_item->text(2)};
1,009✔
1691
                }
1,009✔
1692
            }
1693
        }
1694
    }
1695

1696
    // Merge any persisted states (these should take precedence)
1697
    for (auto it = _persisted_computer_states.constBegin(); it != _persisted_computer_states.constEnd(); ++it) {
142✔
1698
        previous_states[it.key().toStdString()] = {it.value().first, it.value().second};
22✔
1699
    }
1700

1701
    ui->computers_tree->clear();
120✔
1702
    ui->computers_tree->setHeaderLabels({"Data Source / Computer", "Enabled", "Column Name", "Parameters"});
720✔
1703

1704
    // Clean up parameter widgets
1705
    _computer_parameter_widgets.clear();
120✔
1706
    _parameter_controls.clear();
120✔
1707

1708
    auto * registry = _data_manager->getTableRegistry();
120✔
1709
    if (!registry) {
120✔
1710
        _updating_computers_tree = false;
×
1711
        return;
×
1712
    }
1713

1714
    auto data_manager_extension = registry->getDataManagerExtension();
120✔
1715
    if (!data_manager_extension) {
120✔
1716
        _updating_computers_tree = false;
×
UNCOV
1717
        return;
×
1718
    }
1719

1720
    auto & computer_registry = registry->getComputerRegistry();
120✔
1721

1722
    // Get the current row selector type from the selected row source
1723
    auto current_row_selector_type = getCurrentRowSelectorType();
120✔
1724
    if (!current_row_selector_type.has_value()) {
120✔
1725
        // No row source selected, clear the tree and exit
1726
        _updating_computers_tree = false;
17✔
1727
        return;
17✔
1728
    }
1729

1730
    // Get available data sources
1731
    auto data_sources = getAvailableDataSources();
103✔
1732

1733
    if (_group_mode) {
103✔
1734
        // Group similar data sources together
1735
        std::map<std::string, QStringList> groups;
103✔
1736

1737
        // First pass: group data sources by their extracted group name
1738
        for (QString const & data_source: data_sources) {
1,157✔
1739
            std::string group_name = extractGroupName(data_source);
1,054✔
1740
            groups[group_name].append(data_source);
1,054✔
1741
        }
1,054✔
1742

1743
        // Second pass: create tree structure
1744
        for (auto const & [group_name, group_members]: groups) {
1,157✔
1745
            if (group_members.size() > 1) {
1,054✔
1746
                // Create group item
UNCOV
1747
                auto * group_item = new QTreeWidgetItem(ui->computers_tree);
×
UNCOV
1748
                group_item->setText(0, QString::fromStdString(group_name) + " (Group)");
×
UNCOV
1749
                group_item->setFlags(Qt::ItemIsEnabled);
×
UNCOV
1750
                group_item->setExpanded(false);// Start collapsed
×
1751

1752
                // Get computers available for this group (use first member to determine available computers)
UNCOV
1753
                auto first_variant = createColumnDataSourceVariant(group_members.first(), data_manager_extension);
×
UNCOV
1754
                if (!first_variant.has_value()) {
×
UNCOV
1755
                    continue;
×
1756
                }
1757

UNCOV
1758
                auto available_computers = computer_registry.getAvailableComputers(current_row_selector_type.value(), first_variant.value());
×
1759

1760
                // Skip this group if no computers are available
UNCOV
1761
                if (available_computers.empty()) {
×
UNCOV
1762
                    delete group_item;
×
UNCOV
1763
                    continue;
×
1764
                }
1765

1766
                // Add computers as children of the group
UNCOV
1767
                for (auto const & computer_info: available_computers) {
×
UNCOV
1768
                    auto * computer_item = new QTreeWidgetItem(group_item);
×
UNCOV
1769
                    computer_item->setText(0, QString::fromStdString(computer_info.name));
×
UNCOV
1770
                    computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
×
UNCOV
1771
                    computer_item->setCheckState(1, Qt::Unchecked);
×
1772

1773
                    // Generate default column name using group name
UNCOV
1774
                    QString default_name = QString("%1_%2").arg(QString::fromStdString(group_name), QString::fromStdString(computer_info.name));
×
UNCOV
1775
                    computer_item->setText(2, default_name);
×
UNCOV
1776
                    computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
×
1777

1778
                    // Store group members and computer name for later use
UNCOV
1779
                    computer_item->setData(0, Qt::UserRole, group_members.join("||"));// Store all group members
×
UNCOV
1780
                    computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
×
UNCOV
1781
                    computer_item->setData(2, Qt::UserRole, true);// Mark as group computer
×
1782

1783
                    // Create parameter widget if computer has parameters
UNCOV
1784
                    if (!computer_info.parameterDescriptors.empty()) {
×
UNCOV
1785
                        auto * param_widget = createParameterWidget(QString::fromStdString(computer_info.name),
×
UNCOV
1786
                                                                    computer_info.parameterDescriptors);
×
UNCOV
1787
                        if (param_widget) {
×
UNCOV
1788
                            ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
×
UNCOV
1789
                            _computer_parameter_widgets[computer_item] = param_widget;
×
1790
                        }
1791
                    }
1792

1793
                    // Restore previous state if present
1794
                    // Try joined-members key first, then fallback to first member key for backward compatibility
NEW
1795
                    QString joined = group_members.join("||");
×
NEW
1796
                    std::string key_joined = (joined + "||" + QString::fromStdString(computer_info.name)).toStdString();
×
NEW
1797
                    std::string key_first = (group_members.first() + "||" + QString::fromStdString(computer_info.name)).toStdString();
×
NEW
1798
                    auto it_prev = previous_states.find(key_joined);
×
NEW
1799
                    if (it_prev == previous_states.end()) it_prev = previous_states.find(key_first);
×
1800
                    if (it_prev != previous_states.end()) {
×
1801
                        computer_item->setCheckState(1, it_prev->second.first);
×
UNCOV
1802
                        if (!it_prev->second.second.isEmpty()) {
×
1803
                            computer_item->setText(2, it_prev->second.second);
×
1804
                        }
1805
                    }
UNCOV
1806
                }
×
UNCOV
1807
            } else {
×
1808
                // Single item - create as individual data source
1809
                QString const & data_source = group_members.first();
1,054✔
1810
                auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
1,054✔
1811
                data_source_item->setText(0, data_source);
1,054✔
1812
                data_source_item->setFlags(Qt::ItemIsEnabled);
1,054✔
1813
                data_source_item->setExpanded(false);
1,054✔
1814

1815
                auto data_source_variant = createColumnDataSourceVariant(data_source, data_manager_extension);
1,054✔
1816
                if (!data_source_variant.has_value()) {
1,054✔
1817
                    delete data_source_item;
420✔
1818
                    continue;
420✔
1819
                }
1820

1821
                auto available_computers = computer_registry.getAvailableComputers(current_row_selector_type.value(), data_source_variant.value());
634✔
1822

1823
                // Skip this data source if no computers are available
1824
                if (available_computers.empty()) {
634✔
1825
                    delete data_source_item;
169✔
1826
                    continue;
169✔
1827
                }
1828

1829
                for (auto const & computer_info: available_computers) {
2,130✔
1830
                    auto * computer_item = new QTreeWidgetItem(data_source_item);
1,665✔
1831
                    computer_item->setText(0, QString::fromStdString(computer_info.name));
1,665✔
1832
                    computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
1,665✔
1833
                    computer_item->setCheckState(1, Qt::Unchecked);
1,665✔
1834

1835
                    QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
1,665✔
1836
                    computer_item->setText(2, default_name);
1,665✔
1837
                    computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
1,665✔
1838

1839
                    computer_item->setData(0, Qt::UserRole, data_source);
1,665✔
1840
                    computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
1,665✔
1841
                    computer_item->setData(2, Qt::UserRole, false);// Mark as individual computer
1,665✔
1842

1843
                    // Create parameter widget if computer has parameters
1844
                    if (!computer_info.parameterDescriptors.empty()) {
1,665✔
1845
                        auto * param_widget = createParameterWidget(QString::fromStdString(computer_info.name),
143✔
1846
                                                                    computer_info.parameterDescriptors);
143✔
1847
                        if (param_widget) {
143✔
1848
                            ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
143✔
1849
                            _computer_parameter_widgets[computer_item] = param_widget;
143✔
1850
                        }
1851
                    }
1852

1853
                    std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
1,665✔
1854
                    auto it_prev = previous_states.find(prev_key);
1,665✔
1855
                    if (it_prev != previous_states.end()) {
1,665✔
1856
                        computer_item->setCheckState(1, it_prev->second.first);
888✔
1857
                        if (!it_prev->second.second.isEmpty()) {
888✔
1858
                            computer_item->setText(2, it_prev->second.second);
888✔
1859
                        }
1860
                    }
1861
                }
1,665✔
1862
            }
1,223✔
1863
        }
1864
    } else {
103✔
1865
        // Individual mode - create tree structure: Data Source -> Computers (original behavior)
UNCOV
1866
        for (QString const & data_source: data_sources) {
×
UNCOV
1867
            auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
×
UNCOV
1868
            data_source_item->setText(0, data_source);
×
1869
            data_source_item->setFlags(Qt::ItemIsEnabled);
×
1870
            data_source_item->setExpanded(false);// Start collapsed
×
1871

1872
            // Convert data source string to DataSourceVariant
UNCOV
1873
            auto data_source_variant = createColumnDataSourceVariant(data_source, data_manager_extension);
×
1874

UNCOV
1875
            if (!data_source_variant.has_value()) {
×
UNCOV
1876
                qDebug() << "Failed to create data source variant for:" << data_source;
×
UNCOV
1877
                delete data_source_item;
×
UNCOV
1878
                continue;
×
1879
            }
1880

1881
            // Get available computers for this specific data source and current row selector type
UNCOV
1882
            auto available_computers = computer_registry.getAvailableComputers(current_row_selector_type.value(), data_source_variant.value());
×
1883

1884
            // Skip this data source if no computers are available
UNCOV
1885
            if (available_computers.empty()) {
×
UNCOV
1886
                delete data_source_item;
×
UNCOV
1887
                continue;
×
1888
            }
1889

1890
            // Add compatible computers as children
UNCOV
1891
            for (auto const & computer_info: available_computers) {
×
UNCOV
1892
                auto * computer_item = new QTreeWidgetItem(data_source_item);
×
UNCOV
1893
                computer_item->setText(0, QString::fromStdString(computer_info.name));
×
1894
                computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
×
UNCOV
1895
                computer_item->setCheckState(1, Qt::Unchecked);
×
1896

1897
                // Generate default column name
UNCOV
1898
                QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
×
UNCOV
1899
                computer_item->setText(2, default_name);
×
UNCOV
1900
                computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
×
1901

1902
                // Store data source and computer name for later use
UNCOV
1903
                computer_item->setData(0, Qt::UserRole, data_source);
×
UNCOV
1904
                computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
×
UNCOV
1905
                computer_item->setData(2, Qt::UserRole, false);// Mark as individual computer
×
1906

1907
                // Create parameter widget if computer has parameters
UNCOV
1908
                if (!computer_info.parameterDescriptors.empty()) {
×
UNCOV
1909
                    auto * param_widget = createParameterWidget(QString::fromStdString(computer_info.name),
×
UNCOV
1910
                                                                computer_info.parameterDescriptors);
×
UNCOV
1911
                    if (param_widget) {
×
UNCOV
1912
                        ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
×
UNCOV
1913
                        _computer_parameter_widgets[computer_item] = param_widget;
×
1914
                    }
1915
                }
1916

1917
                // Restore previous state if present
UNCOV
1918
                std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
×
UNCOV
1919
                auto it_prev = previous_states.find(prev_key);
×
UNCOV
1920
                if (it_prev != previous_states.end()) {
×
UNCOV
1921
                    computer_item->setCheckState(1, it_prev->second.first);
×
UNCOV
1922
                    if (!it_prev->second.second.isEmpty()) {
×
UNCOV
1923
                        computer_item->setText(2, it_prev->second.second);
×
1924
                    }
1925
                }
UNCOV
1926
            }
×
UNCOV
1927
        }
×
1928
    }
1929

1930
    // Resize columns to content
1931
    ui->computers_tree->resizeColumnToContents(0);
103✔
1932
    ui->computers_tree->resizeColumnToContents(1);
103✔
1933
    ui->computers_tree->resizeColumnToContents(2);
103✔
1934
    ui->computers_tree->resizeColumnToContents(3);
103✔
1935

1936
    _updating_computers_tree = false;
103✔
1937

1938
    // Update preview after refresh
1939
    triggerPreviewDebounced();
103✔
1940
}
257✔
1941

1942
void TableDesignerWidget::setJsonTemplateFromCurrentState() {
4✔
1943
    if (!_table_json_widget) return;
4✔
1944
    // Build a minimal JSON template representing current UI state
1945
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
4✔
1946
    auto columns = getEnabledColumnInfos();
4✔
1947
    if (row_source.isEmpty() && columns.empty()) {
4✔
UNCOV
1948
        _table_json_widget->setJsonText("{}");
×
UNCOV
1949
        return;
×
1950
    }
1951

1952
    QString row_type;
4✔
1953
    QString row_source_name;
4✔
1954
    if (row_source.startsWith("TimeFrame: ")) {
4✔
1955
        row_type = "timestamp";
1✔
1956
        row_source_name = row_source.mid(11);
1✔
1957
    } else if (row_source.startsWith("Events: ")) {
3✔
1958
        row_type = "timestamp";
×
1959
        row_source_name = row_source.mid(8);
×
1960
    } else if (row_source.startsWith("Intervals: ")) {
3✔
1961
        row_type = "interval";
3✔
1962
        row_source_name = row_source.mid(11);
3✔
1963
    }
1964

1965
    QStringList column_entries;
4✔
1966
    for (auto const & c: columns) {
10✔
1967
        // Strip any internal prefixes for JSON to keep schema user-friendly
1968
        QString ds = QString::fromStdString(c.dataSourceName);
6✔
1969
        if (ds.startsWith("events:")) ds = ds.mid(7);
6✔
1970
        else if (ds.startsWith("intervals:"))
4✔
1971
            ds = ds.mid(10);
3✔
1972
        else if (ds.startsWith("analog:"))
1✔
UNCOV
1973
            ds = ds.mid(7);
×
1974

1975
        QString entry = QString(
18✔
1976
                                "{\n  \"name\": \"%1\",\n  \"description\": \"%2\",\n  \"data_source\": \"%3\",\n  \"computer\": \"%4\"%5\n}")
1977
                                .arg(QString::fromStdString(c.name))
24✔
1978
                                .arg(QString::fromStdString(c.description))
24✔
1979
                                .arg(ds)
24✔
1980
                                .arg(QString::fromStdString(c.computerName))
24✔
1981
                                .arg(c.parameters.empty() ? QString() : QString(",\n  \"parameters\": {}"));
12✔
1982
        column_entries << entry;
6✔
1983
    }
6✔
1984

1985
    QString table_name = _table_info_widget ? _table_info_widget->getName() : _current_table_id;
4✔
1986
    QString json = QString(
12✔
1987
                           "{\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}")
1988
                           .arg(_current_table_id)
16✔
1989
                           .arg(table_name)
16✔
1990
                           .arg(row_type)
16✔
1991
                           .arg(row_source_name)
16✔
1992
                           .arg(column_entries.join(",\n"));
8✔
1993

1994
    _table_json_widget->setJsonText(json);
4✔
1995
}
4✔
1996

1997
void TableDesignerWidget::applyJsonTemplateToUI(QString const & jsonText) {
7✔
1998
    // Very light-weight parser using Qt to extract essential fields.
1999
    // Assumes a schema similar to tests under computers *.test.cpp.
2000
    QJsonParseError err;
7✔
2001
    QByteArray bytes = jsonText.toUtf8();
7✔
2002
    QJsonDocument doc = QJsonDocument::fromJson(bytes, &err);
7✔
2003
    if (err.error != QJsonParseError::NoError || !doc.isObject()) {
7✔
2004
        // Compute line/column from byte offset if possible
2005
        int64_t offset = static_cast<int64_t>(err.offset);
1✔
2006
        int line = 1;
1✔
2007
        int col = 1;
1✔
2008
        // Avoid operator[] ambiguity on some compilers by using qsizetype and at()
2009
        qsizetype len = std::min<qsizetype>(bytes.size(), static_cast<qsizetype>(offset));
1✔
2010
        for (qsizetype i = 0; i < len; ++i) {
149✔
2011
            char ch = bytes.at(i);
148✔
2012
            if (ch == '\n') {
148✔
2013
                ++line;
6✔
2014
                col = 1;
6✔
2015
            } else {
2016
                ++col;
142✔
2017
            }
2018
        }
2019
        QString detail = err.error != QJsonParseError::NoError
1✔
2020
                                 ? QString("%1 (line %2, column %3)").arg(err.errorString()).arg(line).arg(col)
3✔
2021
                                 : QString("JSON root must be an object");
2✔
2022
        auto * box = new QMessageBox(this);
1✔
2023
        box->setIcon(QMessageBox::Critical);
1✔
2024
        box->setWindowTitle("Invalid JSON");
1✔
2025
        box->setText(QString("JSON format is invalid: %1").arg(detail));
1✔
2026
        box->setAttribute(Qt::WA_DeleteOnClose);
1✔
2027
        box->show();
1✔
2028
        return;
1✔
2029
    }
1✔
2030
    auto obj = doc.object();
6✔
2031
    if (!obj.contains("tables") || !obj["tables"].isArray()) {
6✔
UNCOV
2032
        auto * box = new QMessageBox(this);
×
UNCOV
2033
        box->setIcon(QMessageBox::Critical);
×
UNCOV
2034
        box->setWindowTitle("Invalid JSON");
×
UNCOV
2035
        box->setText("Missing required key: tables (array)");
×
UNCOV
2036
        box->setAttribute(Qt::WA_DeleteOnClose);
×
UNCOV
2037
        box->show();
×
UNCOV
2038
        return;
×
2039
    }
2040
    auto tables = obj["tables"].toArray();
6✔
2041
    if (tables.isEmpty() || !tables[0].isObject()) return;
6✔
2042
    auto table = tables[0].toObject();
6✔
2043

2044
    // Row selector
2045
    QStringList errors;
6✔
2046
    QString rs_type;
6✔
2047
    QString rs_source;
6✔
2048
    if (table.contains("row_selector") && table["row_selector"].isObject()) {
6✔
2049
        auto rs = table["row_selector"].toObject();
5✔
2050
        rs_type = rs.value("type").toString();
5✔
2051
        rs_source = rs.value("source").toString();
5✔
2052
        if (rs_type.isEmpty() || rs_source.isEmpty()) {
5✔
UNCOV
2053
            errors << "Missing required keys in row_selector: 'type' and/or 'source'";
×
2054
        } else {
2055
            // Validate existence
2056
            bool source_ok = false;
5✔
2057
            if (rs_type == "interval") {
5✔
2058
                source_ok = (_data_manager && _data_manager->getData<DigitalIntervalSeries>(rs_source.toStdString()) != nullptr);
5✔
UNCOV
2059
            } else if (rs_type == "timestamp") {
×
UNCOV
2060
                source_ok = (_data_manager && (_data_manager->getTime(TimeKey(rs_source.toStdString())) != nullptr ||
×
UNCOV
2061
                                               _data_manager->getData<DigitalEventSeries>(rs_source.toStdString()) != nullptr));
×
2062
            } else {
2063
                errors << QString("Unsupported row_selector type: %1").arg(rs_type);
×
2064
            }
2065
            if (!source_ok) {
5✔
2066
                errors << QString("Row selector data key not found in DataManager: %1").arg(rs_source);
1✔
2067
            } else {
2068
                // Apply selection to UI
2069
                QString entry;
4✔
2070
                if (rs_type == "interval") {
4✔
2071
                    entry = QString("Intervals: %1").arg(rs_source);
4✔
UNCOV
2072
                } else if (rs_type == "timestamp") {
×
2073
                    // Prefer TimeFrame, fallback to Events
UNCOV
2074
                    entry = QString("TimeFrame: %1").arg(rs_source);
×
UNCOV
2075
                    int idx_tf = ui->row_data_source_combo->findText(entry);
×
UNCOV
2076
                    if (idx_tf < 0) entry = QString("Events: %1").arg(rs_source);
×
2077
                }
2078
                int idx = ui->row_data_source_combo->findText(entry);
4✔
2079
                if (idx >= 0) {
4✔
2080
                    ui->row_data_source_combo->setCurrentIndex(idx);
4✔
2081
                    // Ensure computers tree reflects this row selector before enabling columns
2082
                    refreshComputersTree();
4✔
2083
                } else {
UNCOV
2084
                    errors << QString("Row selector entry not available in UI: %1").arg(entry);
×
2085
                }
2086
            }
4✔
2087
        }
2088
    } else {
5✔
2089
        errors << "Missing required key: row_selector (object)";
1✔
2090
    }
2091

2092
    // If row selector validation already produced errors, surface them immediately
2093
    if (!errors.isEmpty()) {
6✔
2094
        auto * box = new QMessageBox(this);
2✔
2095
        box->setIcon(QMessageBox::Critical);
2✔
2096
        box->setWindowTitle("Invalid Table JSON");
2✔
2097
        box->setText(errors.join("\n"));
2✔
2098
        box->setAttribute(Qt::WA_DeleteOnClose);
2✔
2099
        box->show();
2✔
2100
        return;
2✔
2101
    }
2102

2103
    // Columns: enable matching computers and set column names
2104
    if (table.contains("columns") && table["columns"].isArray()) {
4✔
2105
        auto cols = table["columns"].toArray();
4✔
2106
        auto * tree = ui->computers_tree;
4✔
2107
        // Avoid recursive preview rebuilds while we toggle many items
2108
        bool prevBlocked = tree->blockSignals(true);
4✔
2109
        for (auto const & cval: cols) {
8✔
2110
            if (!cval.isObject()) continue;
4✔
2111
            auto cobj = cval.toObject();
4✔
2112
            QString data_source = cobj.value("data_source").toString();
4✔
2113
            QString computer = cobj.value("computer").toString();
4✔
2114
            QString name = cobj.value("name").toString();
4✔
2115
            if (data_source.isEmpty() || computer.isEmpty() || name.isEmpty()) {
4✔
UNCOV
2116
                errors << "Missing required keys in column: 'name', 'data_source', and 'computer'";
×
UNCOV
2117
                continue;
×
2118
            }
2119
            // Validate data source existence
2120
            bool has_ds = (_data_manager && (_data_manager->getData<DigitalEventSeries>(data_source.toStdString()) != nullptr ||
12✔
2121
                                             _data_manager->getData<DigitalIntervalSeries>(data_source.toStdString()) != nullptr ||
4✔
2122
                                             _data_manager->getData<AnalogTimeSeries>(data_source.toStdString()) != nullptr));
8✔
2123
            if (!has_ds) {
4✔
UNCOV
2124
                errors << QString("Data key not found in DataManager: %1").arg(data_source);
×
2125
            }
2126
            // Validate computer exists
2127
            bool computer_exists = false;
4✔
2128
            if (_data_manager) {
4✔
2129
                if (auto * reg = _data_manager->getTableRegistry()) {
4✔
2130
                    auto & cr = reg->getComputerRegistry();
4✔
2131
                    computer_exists = cr.findComputerInfo(computer.toStdString());
4✔
2132
                }
2133
            }
2134
            if (!computer_exists) {
4✔
2135
                errors << QString("Requested computer does not exist: %1").arg(computer);
2✔
2136
            }
2137
            
2138
            // Validate compatibility using registry and current row selector type
2139
            if (_data_manager) {
4✔
2140
                if (auto * reg = _data_manager->getTableRegistry()) {
4✔
2141
                    auto data_manager_ext = reg->getDataManagerExtension();
4✔
2142
                    if (data_manager_ext) {
4✔
2143
                        auto & cr = reg->getComputerRegistry();
4✔
2144
                        auto current_row_type = getCurrentRowSelectorType();
4✔
2145
                        
2146
                        // Build data source variant based on data type
2147
                        bool type_event = false, type_interval = false, type_analog = false;
4✔
2148
                        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
24✔
2149
                            auto * ds_item = tree->topLevelItem(i);
20✔
2150
                            QString ds_text = ds_item->text(0);
20✔
2151
                            if (ds_text.contains(data_source)) {
20✔
2152
                                if (ds_text.startsWith("Events: ")) type_event = true;
4✔
UNCOV
2153
                                else if (ds_text.startsWith("Intervals: ")) type_interval = true;
×
UNCOV
2154
                                else if (ds_text.startsWith("analog:")) type_analog = true;
×
2155
                            }
2156
                        }
20✔
2157
                        
2158
                        QString ds_repr;
4✔
2159
                        if (type_event) ds_repr = QString("Events: %1").arg(data_source);
4✔
UNCOV
2160
                        else if (type_interval) ds_repr = QString("Intervals: %1").arg(data_source);
×
UNCOV
2161
                        else if (type_analog) ds_repr = QString("analog:%1").arg(data_source);
×
2162
                        
2163
                        if (!ds_repr.isEmpty() && current_row_type.has_value()) {
4✔
2164
                            auto ds_variant = createColumnDataSourceVariant(ds_repr, data_manager_ext);
4✔
2165
                            if (ds_variant.has_value()) {
4✔
2166
                                auto available = cr.getAvailableComputers(current_row_type.value(), ds_variant.value());
4✔
2167
                                bool found = false;
4✔
2168
                                for (auto const & comp_info : available) {
10✔
2169
                                    if (comp_info.name == computer.toStdString()) {
8✔
2170
                                        found = true;
2✔
2171
                                        break;
2✔
2172
                                    }
2173
                                }
2174
                                if (!found) {
4✔
2175
                                    // Phrase includes 'not valid for data source type' to satisfy tests
2176
                                    errors << QString("Computer '%1' is not valid for data source type and not compatible with '%2' for the current row selector type")
6✔
2177
                                                  .arg(computer, ds_repr);
6✔
2178
                                }
2179
                            }
4✔
2180
                        }
4✔
2181
                    }
4✔
2182
                }
4✔
2183
            }
2184

2185
            // Find matching tree item with strict preference
2186
            QString exact_events = QString("Events: %1").arg(data_source);
4✔
2187
            QString exact_intervals = QString("Intervals: %1").arg(data_source);
4✔
2188
            QString exact_analog = QString("analog:%1").arg(data_source);
4✔
2189
            QTreeWidgetItem * matched_ds = nullptr;
4✔
2190
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
16✔
2191
                auto * ds_item = tree->topLevelItem(i);
16✔
2192
                QString t = ds_item->text(0);
16✔
2193
                if (t == exact_events || t == exact_intervals || t == exact_analog) {
16✔
2194
                    matched_ds = ds_item;
4✔
2195
                    break;
4✔
2196
                }
2197
            }
16✔
2198
            if (!matched_ds) {
4✔
UNCOV
2199
                for (int i = 0; i < tree->topLevelItemCount(); ++i) {
×
UNCOV
2200
                    auto * ds_item = tree->topLevelItem(i);
×
2201
                    QString t = ds_item->text(0);
×
UNCOV
2202
                    if (t.contains(data_source) || t.endsWith(data_source)) {
×
UNCOV
2203
                        matched_ds = ds_item;
×
2204
                        break;
×
2205
                    }
2206
                }
×
2207
            }
2208
            if (matched_ds) {
4✔
2209
                for (int j = 0; j < matched_ds->childCount(); ++j) {
16✔
2210
                    auto * comp_item = matched_ds->child(j);
12✔
2211
                    QString comp_text = comp_item->text(0).trimmed();
12✔
2212
                    if (comp_text == computer || comp_text.contains(computer)) {
12✔
2213
                        comp_item->setCheckState(1, Qt::Checked);
2✔
2214
                        if (!name.isEmpty()) comp_item->setText(2, name);
2✔
2215
                    }
2216
                }
12✔
2217
            } else {
2218
                errors << QString("Data source not found in tree: %1").arg(data_source);
×
2219
            }
2220
        }
4✔
2221
        tree->blockSignals(prevBlocked);
4✔
2222
        if (!errors.isEmpty()) {
4✔
2223
            auto * box = new QMessageBox(this);
2✔
2224
            box->setIcon(QMessageBox::Critical);
2✔
2225
            box->setWindowTitle("Invalid Table JSON");
2✔
2226
            box->setText(errors.join("\n"));
2✔
2227
            box->setAttribute(Qt::WA_DeleteOnClose);
2✔
2228
            box->show();
2✔
2229
            return;
2✔
2230
        }
2231
        triggerPreviewDebounced();
2✔
2232
    }
4✔
2233
}
36✔
2234

2235
void TableDesignerWidget::onComputersTreeItemChanged() {
15,460✔
2236
    if (_updating_computers_tree) return;
15,460✔
2237

2238
    // Trigger preview update when checkbox states change
2239
    triggerPreviewDebounced();
16✔
2240
}
2241

2242
void TableDesignerWidget::onComputersTreeItemEdited(QTreeWidgetItem * item, int column) {
15,460✔
2243
    if (_updating_computers_tree) return;
15,460✔
2244

2245
    // Persist state on either checkbox (column 1) or name edits (column 2)
2246
    persistComputerItemState(item, column);
16✔
2247

2248
    // Trigger preview update when something meaningful changes
2249
    if (column == 1 || column == 2) {
16✔
2250
        triggerPreviewDebounced();
16✔
2251
    }
2252
}
2253

2254
void TableDesignerWidget::persistComputerItemState(QTreeWidgetItem * item, int column) {
16✔
2255
    Q_UNUSED(column)
2256
    if (!item) return;
16✔
2257
    // Only persist for actual computer rows which carry UserRole data
2258
    QVariant dsVar = item->data(0, Qt::UserRole);
16✔
2259
    QVariant compVar = item->data(1, Qt::UserRole);
16✔
2260
    if (!dsVar.isValid() || !compVar.isValid()) return;
16✔
2261

2262
    QString key = dsVar.toString() + "||" + compVar.toString();
16✔
2263
    Qt::CheckState cs = item->checkState(1);
16✔
2264
    QString custom = item->text(2);
16✔
2265
    _persisted_computer_states[key] = qMakePair(cs, custom);
16✔
2266
}
16✔
2267

2268
std::vector<ColumnInfo> TableDesignerWidget::getEnabledColumnInfos() const {
114✔
2269
    std::vector<ColumnInfo> column_infos;
114✔
2270

2271
    if (!ui->computers_tree) return column_infos;
114✔
2272

2273
    // Iterate through all data source items
2274
    for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
672✔
2275
        auto * data_source_item = ui->computers_tree->topLevelItem(i);
558✔
2276

2277
        // Iterate through computer items under each data source
2278
        for (int j = 0; j < data_source_item->childCount(); ++j) {
3,388✔
2279
            auto * computer_item = data_source_item->child(j);
2,830✔
2280

2281
            // Check if this computer is enabled
2282
            if (computer_item->checkState(1) == Qt::Checked) {
2,830✔
2283
                QString data_source = computer_item->data(0, Qt::UserRole).toString();
75✔
2284
                QString computer_name = computer_item->data(1, Qt::UserRole).toString();
75✔
2285
                QString column_name = computer_item->text(2);
75✔
2286
                bool is_group_computer = computer_item->data(2, Qt::UserRole).toBool();
75✔
2287

2288
                // Get parameter values for this computer
2289
                std::map<std::string, std::string> parameters = getParameterValues(computer_name);
75✔
2290

2291
                if (is_group_computer) {
75✔
2292
                    // This is a group computer - create columns for all members
UNCOV
2293
                    QStringList group_members = data_source.split("||");
×
2294

UNCOV
2295
                    for (QString const & member: group_members) {
×
2296
                        // Generate individual column name (e.g., "spike_1_Mean", "spike_2_Mean")
UNCOV
2297
                        QString individual_column_name = generateDefaultColumnName(member, computer_name);
×
2298

2299
                        // Create ColumnInfo for each group member
UNCOV
2300
                        QString source_key = member;
×
UNCOV
2301
                        if (source_key.startsWith("Events: ")) {
×
UNCOV
2302
                            source_key = QString("events:%1").arg(source_key.mid(8));
×
UNCOV
2303
                        } else if (source_key.startsWith("Intervals: ")) {
×
UNCOV
2304
                            source_key = QString("intervals:%1").arg(source_key.mid(11));
×
UNCOV
2305
                        } else if (source_key.startsWith("analog:")) {
×
UNCOV
2306
                            source_key = source_key;// already prefixed
×
UNCOV
2307
                        } else if (source_key.startsWith("lines:")) {
×
2308
                            source_key = source_key;// already prefixed
×
UNCOV
2309
                        } else if (source_key.startsWith("TimeFrame: ")) {
×
2310
                            // TimeFrame used only for row selector; columns require concrete sources
2311
                            source_key = source_key.mid(11);
×
2312
                        }
2313

2314
                        ColumnInfo info(individual_column_name.toStdString(),
×
2315
                                        QString("Column from %1 using %2 (group applied)").arg(member, computer_name).toStdString(),
×
UNCOV
2316
                                        source_key.toStdString(),
×
2317
                                        computer_name.toStdString());
×
2318

2319
                        // Set parameters
UNCOV
2320
                        info.parameters = parameters;
×
2321

2322
                        // Set output type based on computer info
UNCOV
2323
                        if (auto * registry = _data_manager->getTableRegistry()) {
×
UNCOV
2324
                            auto & computer_registry = registry->getComputerRegistry();
×
UNCOV
2325
                            auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
×
UNCOV
2326
                            if (computer_info) {
×
UNCOV
2327
                                info.outputType = computer_info->outputType;
×
UNCOV
2328
                                info.outputTypeName = computer_info->outputTypeName;
×
UNCOV
2329
                                info.isVectorType = computer_info->isVectorType;
×
UNCOV
2330
                                if (info.isVectorType) {
×
UNCOV
2331
                                    info.elementType = computer_info->elementType;
×
UNCOV
2332
                                    info.elementTypeName = computer_info->elementTypeName;
×
2333
                                }
2334
                            }
2335
                        }
2336

2337
                        column_infos.push_back(std::move(info));
×
2338
                    }
×
UNCOV
2339
                } else {
×
2340
                    // Individual computer - original behavior
2341
                    if (column_name.isEmpty()) {
75✔
2342
                        // Clean the data source name before generating column name
UNCOV
2343
                        QString clean_data_source = data_source;
×
UNCOV
2344
                        if (clean_data_source.startsWith("lines:")) {
×
UNCOV
2345
                            clean_data_source = clean_data_source.mid(6);// Remove "lines:" prefix
×
2346
                        }
UNCOV
2347
                        column_name = generateDefaultColumnName(clean_data_source, computer_name);
×
UNCOV
2348
                    }
×
2349

2350
                    // Create ColumnInfo (use raw key without UI prefixes)
2351
                    QString source_key = data_source;
75✔
2352
                    if (source_key.startsWith("Events: ")) {
75✔
2353
                        source_key = QString("events:%1").arg(source_key.mid(8));
38✔
2354
                    } else if (source_key.startsWith("Intervals: ")) {
37✔
2355
                        source_key = QString("intervals:%1").arg(source_key.mid(11));
30✔
2356
                    } else if (source_key.startsWith("analog:")) {
7✔
UNCOV
2357
                        source_key = source_key;// already prefixed
×
2358
                    } else if (source_key.startsWith("lines:")) {
7✔
2359
                        source_key = source_key;// already prefixed
7✔
UNCOV
2360
                    } else if (source_key.startsWith("TimeFrame: ")) {
×
2361
                        // TimeFrame used only for row selector; columns require concrete sources
UNCOV
2362
                        source_key = source_key.mid(11);
×
2363
                    }
2364

2365
                    ColumnInfo info(column_name.toStdString(),
225✔
2366
                                    QString("Column from %1 using %2").arg(data_source, computer_name).toStdString(),
150✔
2367
                                    source_key.toStdString(),
150✔
2368
                                    computer_name.toStdString());
375✔
2369

2370
                    // Set parameters
2371
                    info.parameters = parameters;
75✔
2372

2373
                    // Set output type based on computer info
2374
                    if (auto * registry = _data_manager->getTableRegistry()) {
75✔
2375
                        auto & computer_registry = registry->getComputerRegistry();
75✔
2376
                        auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
75✔
2377
                        if (computer_info) {
75✔
2378
                            info.outputType = computer_info->outputType;
75✔
2379
                            info.outputTypeName = computer_info->outputTypeName;
75✔
2380
                            info.isVectorType = computer_info->isVectorType;
75✔
2381
                            if (info.isVectorType) {
75✔
2382
                                info.elementType = computer_info->elementType;
7✔
2383
                                info.elementTypeName = computer_info->elementTypeName;
7✔
2384
                            }
2385
                        }
2386
                    }
2387

2388
                    column_infos.push_back(std::move(info));
75✔
2389
                }
75✔
2390
            }
75✔
2391
        }
2392
    }
2393

2394
    return column_infos;
114✔
UNCOV
2395
}
×
2396

2397
QString TableDesignerWidget::generateDefaultColumnName(QString const & data_source, QString const & computer_name) const {
1,665✔
2398
    QString source_name = data_source;
1,665✔
2399

2400
    // Extract the actual name from prefixed data sources
2401
    if (source_name.startsWith("Events: ")) {
1,665✔
2402
        source_name = source_name.mid(8);
234✔
2403
    } else if (source_name.startsWith("Intervals: ")) {
1,431✔
2404
        source_name = source_name.mid(11);
368✔
2405
    } else if (source_name.startsWith("analog:")) {
1,063✔
2406
        source_name = source_name.mid(7);
868✔
2407
    } else if (source_name.startsWith("lines:")) {
195✔
2408
        source_name = source_name.mid(6);
195✔
UNCOV
2409
    } else if (source_name.startsWith("TimeFrame: ")) {
×
UNCOV
2410
        source_name = source_name.mid(11);
×
2411
    }
2412

2413
    // Create a concise name
2414
    return QString("%1_%2").arg(source_name, computer_name);
4,995✔
2415
}
1,665✔
2416

2417
std::string TableDesignerWidget::extractGroupName(QString const & data_source) const {
1,054✔
2418
    QString source_name = data_source;
1,054✔
2419

2420
    // Extract the actual name from prefixed data sources
2421
    if (source_name.startsWith("Events: ")) {
1,054✔
2422
        source_name = source_name.mid(8);
209✔
2423
    } else if (source_name.startsWith("Intervals: ")) {
845✔
2424
        source_name = source_name.mid(11);
116✔
2425
    } else if (source_name.startsWith("analog:")) {
729✔
2426
        source_name = source_name.mid(7);
206✔
2427
    } else if (source_name.startsWith("lines:")) {
523✔
2428
        source_name = source_name.mid(6);
103✔
2429
    } else if (source_name.startsWith("TimeFrame: ")) {
420✔
2430
        source_name = source_name.mid(11);
420✔
2431
    }
2432

2433
    // Use the same grouping pattern as Feature_Tree_Widget
2434
    std::regex const pattern{_grouping_pattern};
1,054✔
2435
    std::smatch matches{};
1,054✔
2436
    std::string key = source_name.toStdString();
1,054✔
2437

2438
    if (std::regex_search(key, matches, pattern) && matches.size() > 1) {
1,054✔
UNCOV
2439
        return matches[1].str();
×
2440
    }
2441

2442
    return key;// Return the key itself if no match
1,054✔
2443
}
1,054✔
2444

UNCOV
2445
void TableDesignerWidget::onGroupModeToggled(bool enabled) {
×
UNCOV
2446
    _group_mode = enabled;
×
2447

2448
    // Update button text to reflect current mode
UNCOV
2449
    if (enabled) {
×
UNCOV
2450
        ui->group_mode_toggle_btn->setText("Group Mode");
×
UNCOV
2451
        ui->computers_info_label->setText("Select computers by checking the boxes. Similar data will be grouped and transformed together.");
×
2452
    } else {
UNCOV
2453
        ui->group_mode_toggle_btn->setText("Individual Mode");
×
2454
        ui->computers_info_label->setText("Select computers by checking the boxes. Each data source will be handled individually.");
×
2455
    }
2456

2457
    // Refresh the tree to apply the new grouping mode
2458
    refreshComputersTree();
×
2459
}
×
2460

2461
QWidget * TableDesignerWidget::createParameterWidget(QString const & computer_name,
143✔
2462
                                                     std::vector<std::unique_ptr<IParameterDescriptor>> const & parameter_descriptors) {
2463
    if (parameter_descriptors.empty()) {
143✔
2464
        return nullptr;
×
2465
    }
2466

2467
    auto * widget = new QWidget();
143✔
2468
    auto * layout = new QHBoxLayout(widget);
143✔
2469
    layout->setContentsMargins(2, 2, 2, 2);
143✔
2470
    layout->setSpacing(4);
143✔
2471

2472
    for (auto const & param_desc: parameter_descriptors) {
286✔
2473
        QString param_name = QString::fromStdString(param_desc->getName());
143✔
2474
        QString param_key = computer_name + "::" + param_name;
143✔
2475

2476
        // Add parameter label
2477
        auto * label = new QLabel(QString::fromStdString(param_desc->getName()) + ":");
143✔
2478
        label->setToolTip(QString::fromStdString(param_desc->getDescription()));
143✔
2479
        layout->addWidget(label);
143✔
2480

2481
        if (param_desc->getUIHint() == "enum") {
143✔
2482
            // Create combo box for enum parameters
2483
            auto * combo = new QComboBox();
78✔
2484
            combo->setObjectName(param_key);// Store parameter key for retrieval
78✔
2485

2486
            auto ui_props = param_desc->getUIProperties();
78✔
2487
            QString options_str = QString::fromStdString(ui_props["options"]);
234✔
2488
            QString default_value = QString::fromStdString(ui_props["default"]);
234✔
2489

2490
            QStringList options = options_str.split(',', Qt::SkipEmptyParts);
78✔
2491
            combo->addItems(options);
78✔
2492

2493
            // Set default value
2494
            int default_index = combo->findText(default_value);
78✔
2495
            if (default_index >= 0) {
78✔
2496
                combo->setCurrentIndex(default_index);
78✔
2497
            }
2498

2499
            combo->setToolTip(QString::fromStdString(param_desc->getDescription()));
78✔
2500
            layout->addWidget(combo);
78✔
2501

2502
            // Store the widget for parameter retrieval
2503
            _parameter_controls[param_key.toStdString()] = combo;
78✔
2504

2505
        } else if (param_desc->getUIHint() == "number") {
143✔
2506
            // Create spin box for numeric parameters
2507
            auto * spinbox = new QSpinBox();
65✔
2508
            spinbox->setObjectName(param_key);
65✔
2509

2510
            auto ui_props = param_desc->getUIProperties();
65✔
2511
            QString default_str = QString::fromStdString(ui_props["default"]);
195✔
2512
            QString min_str = QString::fromStdString(ui_props["min"]);
195✔
2513
            QString max_str = QString::fromStdString(ui_props["max"]);
195✔
2514

2515
            if (!min_str.isEmpty()) spinbox->setMinimum(min_str.toInt());
65✔
2516
            if (!max_str.isEmpty()) spinbox->setMaximum(max_str.toInt());
65✔
2517
            if (!default_str.isEmpty()) spinbox->setValue(default_str.toInt());
65✔
2518

2519
            spinbox->setToolTip(QString::fromStdString(param_desc->getDescription()));
65✔
2520
            layout->addWidget(spinbox);
65✔
2521

2522
            _parameter_controls[param_key.toStdString()] = spinbox;
65✔
2523

2524
        } else {
65✔
2525
            // Default to text input
UNCOV
2526
            auto * lineedit = new QLineEdit();
×
UNCOV
2527
            lineedit->setObjectName(param_key);
×
2528

UNCOV
2529
            auto ui_props = param_desc->getUIProperties();
×
UNCOV
2530
            QString default_value = QString::fromStdString(ui_props["default"]);
×
UNCOV
2531
            lineedit->setText(default_value);
×
2532

UNCOV
2533
            lineedit->setToolTip(QString::fromStdString(param_desc->getDescription()));
×
UNCOV
2534
            layout->addWidget(lineedit);
×
2535

UNCOV
2536
            _parameter_controls[param_key.toStdString()] = lineedit;
×
UNCOV
2537
        }
×
2538
    }
143✔
2539

2540
    return widget;
143✔
2541
}
2542

2543
std::map<std::string, std::string> TableDesignerWidget::getParameterValues(QString const & computer_name) const {
75✔
2544
    std::map<std::string, std::string> parameters;
75✔
2545

2546
    // Look for parameter controls with this computer name prefix
2547
    QString prefix = computer_name + "::";
75✔
2548

2549
    for (auto const & [key, widget]: _parameter_controls) {
150✔
2550
        QString key_str = QString::fromStdString(key);
75✔
2551
        if (key_str.startsWith(prefix)) {
75✔
2552
            QString param_name = key_str.mid(prefix.length());
7✔
2553

2554
            if (auto * combo = qobject_cast<QComboBox *>(widget)) {
7✔
UNCOV
2555
                parameters[param_name.toStdString()] = combo->currentText().toStdString();
×
2556
            } else if (auto * spinbox = qobject_cast<QSpinBox *>(widget)) {
7✔
2557
                parameters[param_name.toStdString()] = QString::number(spinbox->value()).toStdString();
7✔
UNCOV
2558
            } else if (auto * lineedit = qobject_cast<QLineEdit *>(widget)) {
×
UNCOV
2559
                parameters[param_name.toStdString()] = lineedit->text().toStdString();
×
2560
            }
2561
        }
7✔
2562
    }
75✔
2563

2564
    return parameters;
75✔
2565
}
75✔
2566

2567
// Explicit template instantiations for formatVectorForCsv
2568
template void TableDesignerWidget::formatVectorForCsv<double>(std::ofstream & file, std::vector<double> const & values, int precision);
2569
template void TableDesignerWidget::formatVectorForCsv<int>(std::ofstream & file, std::vector<int> const & values, int precision);
2570
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