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

paulmthompson / WhiskerToolbox / 18076144203

28 Sep 2025 03:15PM UTC coverage: 70.31% (+0.2%) from 70.089%
18076144203

push

github

paulmthompson
clean up horrific try catch block for csv export and replace with variant

20 of 61 new or added lines in 1 file covered. (32.79%)

92 existing lines in 4 files now uncovered.

44222 of 62896 relevant lines covered (70.31%)

1124.71 hits per line

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

62.45
/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 "TableExportWidget.hpp"
22
#include "TableInfoWidget.hpp"
23
#include "TableJSONWidget.hpp"
24
#include "TableTransformWidget.hpp"
25
#include "TableViewerWidget/TableViewerWidget.hpp"
26

27

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

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

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

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

67
    ui->setupUi(this);
18✔
68

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

72
    _parameter_widget = nullptr;
18✔
73
    _parameter_layout = nullptr;
18✔
74

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

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

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

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

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

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

103
    connectSignals();
18✔
104

105

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

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

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

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

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

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

154
TableDesignerWidget::~TableDesignerWidget() {
18✔
155
    delete ui;
18✔
156
}
18✔
157

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

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

169

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

179
    // Table info signals are connected via TableInfoWidget
180

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

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

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

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

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

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

242
    _current_table_id = table_id;
14✔
243
    loadTableInfo(table_id);
14✔
244

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

254
    updateBuildStatus("Table selected: " + table_id);
14✔
255

256
    qDebug() << "Selected table:" << table_id;
14✔
257
}
32✔
258

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

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

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

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

285
void TableDesignerWidget::onDeleteTable() {
×
286
    if (_current_table_id.isEmpty()) {
×
287
        return;
×
288
    }
289

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

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

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

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

320
    // Update the info label
321
    updateRowInfoLabel(selected);
31✔
322

323
    // Update interval settings visibility
324
    updateIntervalSettingsVisibility();
31✔
325

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

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

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

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

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

354

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

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

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

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

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

390
        // Create the TableViewBuilder
391
        TableViewBuilder builder(data_manager_extension);
×
392
        builder.setRowSelector(std::move(row_selector));
×
393

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

404
        if (!all_columns_valid) {
×
405
            return;
×
406
        }
407

408
        // Build the table
409
        auto table_view = builder.build();
×
410

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

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

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

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

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

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

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

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

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

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

488
        if (!all_columns_valid) {
4✔
489
            return false;
×
490
        }
491

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

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

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

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

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

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

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

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

561
    try {
562
        PCATransform pca(cfg);
×
563
        TableView derived = pca.apply(*base_view);
×
564

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

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

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

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

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

613
    QString filename = promptSaveCsvFilename();
×
614
    if (filename.isEmpty()) return;
×
615
    if (!filename.endsWith(".csv", Qt::CaseInsensitive)) filename += ".csv";
×
616

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

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

630
    try {
631
        std::ofstream file(filename.toStdString());
×
632
        if (!file.is_open()) {
×
633
            updateBuildStatus("Could not open file for writing", true);
×
634
            return;
×
635
        }
636
        file << std::fixed << std::setprecision(precision);
×
637

638
        auto names = view->getColumnNames();
×
639
        if (includeHeader) {
×
640
            for (size_t i = 0; i < names.size(); ++i) {
×
641
                if (i > 0) file << delim;
×
642
                file << names[i];
×
643
            }
644
            file << eol;
×
645
        }
646
        size_t rows = view->getRowCount();
×
647
        for (size_t r = 0; r < rows; ++r) {
×
648
            for (size_t c = 0; c < names.size(); ++c) {
×
649
                if (c > 0) file << delim;
×
650

651
                // Use efficient visitColumnData approach instead of try/catch
652
                try {
NEW
653
                    view->visitColumnData(names[c], [&file, r, this](auto const & vec) {
×
654
                        using VecT = std::decay_t<decltype(vec)>;
655
                        using ElemT = typename VecT::value_type;
656

NEW
657
                        if (r >= vec.size()) {
×
NEW
658
                            if constexpr (std::is_same_v<ElemT, double>) file << "NaN";
×
659
                            else if constexpr (std::is_same_v<ElemT, float>)
NEW
660
                                file << "NaN";
×
661
                            else if constexpr (std::is_same_v<ElemT, int>)
NEW
662
                                file << "NaN";
×
663
                            else if constexpr (std::is_same_v<ElemT, int64_t>)
NEW
664
                                file << "NaN";
×
665
                            else if constexpr (std::is_same_v<ElemT, bool>)
NEW
666
                                file << "false";
×
667
                            else
NEW
668
                                file << "N/A";
×
NEW
669
                            return;
×
670
                        }
671

672
                        if constexpr (std::is_same_v<ElemT, double>) {
NEW
673
                            file << std::fixed << std::setprecision(3) << vec[r];
×
674
                        } else if constexpr (std::is_same_v<ElemT, float>) {
NEW
675
                            file << std::fixed << std::setprecision(3) << vec[r];
×
676
                        } else if constexpr (std::is_same_v<ElemT, int>) {
NEW
677
                            file << vec[r];
×
678
                        } else if constexpr (std::is_same_v<ElemT, int64_t>) {
NEW
679
                            file << static_cast<int64_t>(vec[r]);
×
680
                        } else if constexpr (std::is_same_v<ElemT, bool>) {
NEW
681
                            file << (vec[r] ? 1 : 0);
×
682
                        } else if constexpr (
683
                                std::is_same_v<ElemT, std::vector<double>> ||
684
                                std::is_same_v<ElemT, std::vector<int>> ||
685
                                std::is_same_v<ElemT, std::vector<float>>) {
686
                            // Format vector data as comma-separated values
NEW
687
                            formatVectorForCsv(file, vec[r], 3);
×
688
                        } else {
NEW
689
                            file << "?";
×
690
                        }
691
                    });
NEW
692
                } catch (...) {
×
NEW
693
                    file << "Error";
×
694
                }
×
695
            }
UNCOV
696
            file << eol;
×
697
        }
698
        file.close();
×
699
        updateBuildStatus(QString("Exported CSV: %1").arg(filename));
×
700
    } catch (std::exception const & e) {
×
701
        updateBuildStatus(QString("Export failed: %1").arg(e.what()), true);
×
702
    }
×
703
}
×
704

705
QString TableDesignerWidget::promptSaveCsvFilename() const {
×
706
    return QFileDialog::getSaveFileName(const_cast<TableDesignerWidget *>(this), "Export Table to CSV", QString(), "CSV Files (*.csv)");
×
707
}
708

709
template<typename T>
NEW
710
void TableDesignerWidget::formatVectorForCsv(std::ofstream & file, std::vector<T> const & values, int precision) {
×
NEW
711
    if (values.empty()) {
×
NEW
712
        file << "[]";
×
NEW
713
        return;
×
714
    }
715

NEW
716
    file << "[";
×
NEW
717
    for (size_t i = 0; i < values.size(); ++i) {
×
718
        if constexpr (std::is_same_v<T, double> || std::is_same_v<T, float>) {
NEW
719
            file << std::fixed << std::setprecision(precision) << values[i];
×
720
        } else {
NEW
721
            file << values[i];
×
722
        }
NEW
723
        if (i + 1 < values.size()) file << ",";
×
724
    }
NEW
725
    file << "]";
×
726
}
727

728
void TableDesignerWidget::onSaveTableInfo() {
×
729
    if (_current_table_id.isEmpty()) {
×
730
        return;
×
731
    }
732

733
    QString name = _table_info_widget ? _table_info_widget->getName() : QString();
×
734
    QString description = _table_info_widget ? _table_info_widget->getDescription() : QString();
×
735

736
    if (name.isEmpty()) {
×
737
        QMessageBox::warning(this, "Error", "Table name cannot be empty");
×
738
        return;
×
739
    }
740

741
    if (auto * reg = _data_manager->getTableRegistry(); reg && reg->updateTableInfo(_current_table_id.toStdString(), name.toStdString(), description.toStdString())) {
×
742
        updateBuildStatus("Table information saved");
×
743
        // Refresh the combo to show updated name
744
        refreshTableCombo();
×
745
        // Restore selection
746
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
747
            if (ui->table_combo->itemData(i).toString() == _current_table_id) {
×
748
                ui->table_combo->setCurrentIndex(i);
×
749
                break;
×
750
            }
751
        }
752
    } else {
753
        QMessageBox::warning(this, "Error", "Failed to save table information");
×
754
    }
755
}
×
756

757
void TableDesignerWidget::onTableManagerTableCreated(QString const & table_id) {
13✔
758
    refreshTableCombo();
13✔
759
    qDebug() << "Table created signal received:" << table_id;
13✔
760
}
13✔
761

762
void TableDesignerWidget::onTableManagerTableRemoved(QString const & table_id) {
×
763
    refreshTableCombo();
×
764
    if (_current_table_id == table_id) {
×
765
        _current_table_id.clear();
×
766
        clearUI();
×
767
    }
768
    qDebug() << "Table removed signal received:" << table_id;
×
769
}
×
770

771
void TableDesignerWidget::onTableManagerTableInfoUpdated(QString const & table_id) {
13✔
772
    if (_current_table_id == table_id && !_loading_column_configuration) {
13✔
773
        loadTableInfo(table_id);
13✔
774
    }
775
    qDebug() << "Table info updated signal received:" << table_id;
13✔
776
}
13✔
777

778
void TableDesignerWidget::refreshTableCombo() {
31✔
779
    ui->table_combo->clear();
31✔
780

781
    auto * reg = _data_manager->getTableRegistry();
31✔
782
    auto table_infos = reg ? reg->getAllTableInfo() : std::vector<TableInfo>{};
31✔
783
    for (auto const & info: table_infos) {
47✔
784
        ui->table_combo->addItem(QString::fromStdString(info.name), QString::fromStdString(info.id));
16✔
785
    }
786

787
    if (ui->table_combo->count() == 0) {
31✔
788
        ui->table_combo->addItem("(No tables available)", "");
18✔
789
    }
790
}
62✔
791

792
void TableDesignerWidget::refreshRowDataSourceCombo() {
22✔
793
    ui->row_data_source_combo->clear();
22✔
794

795
    if (!_data_manager) {
22✔
796
        qDebug() << "refreshRowDataSourceCombo: No table manager";
×
797
        return;
×
798
    }
799

800
    auto data_sources = getAvailableDataSources();
22✔
801
    qDebug() << "refreshRowDataSourceCombo: Found" << data_sources.size() << "data sources:" << data_sources;
22✔
802

803
    for (QString const & source: data_sources) {
248✔
804
        // Only include valid row sources in this combo
805
        if (source.startsWith("TimeFrame: ") || source.startsWith("Events: ") || source.startsWith("Intervals: ")) {
226✔
806
            ui->row_data_source_combo->addItem(source);
160✔
807
        }
808
    }
809

810
    if (ui->row_data_source_combo->count() == 0) {
22✔
811
        ui->row_data_source_combo->addItem("(No data sources available)");
×
812
        qDebug() << "refreshRowDataSourceCombo: No data sources available";
×
813
    }
814
}
22✔
815

816

817
void TableDesignerWidget::loadTableInfo(QString const & table_id) {
27✔
818
    if (table_id.isEmpty() || !_data_manager) {
27✔
819
        clearUI();
×
820
        return;
×
821
    }
822

823
    auto * reg = _data_manager->getTableRegistry();
27✔
824
    auto info = reg ? reg->getTableInfo(table_id.toStdString()) : TableInfo{};
27✔
825
    if (info.id.empty()) {
27✔
826
        clearUI();
×
827
        return;
×
828
    }
829

830
    // Load table information
831
    if (_table_info_widget) {
27✔
832
        _table_info_widget->setName(QString::fromStdString(info.name));
27✔
833
        _table_info_widget->setDescription(QString::fromStdString(info.description));
27✔
834
    }
835

836
    // Load row source if available
837
    if (!info.rowSourceName.empty()) {
27✔
838
        int row_index = ui->row_data_source_combo->findText(QString::fromStdString(info.rowSourceName));
14✔
839
        if (row_index >= 0) {
14✔
840
            // Block signals to prevent circular dependency when loading table info
841
            ui->row_data_source_combo->blockSignals(true);
14✔
842
            ui->row_data_source_combo->setCurrentIndex(row_index);
14✔
843
            ui->row_data_source_combo->blockSignals(false);
14✔
844

845
            // Manually update the info label without triggering the signal handler
846
            updateRowInfoLabel(QString::fromStdString(info.rowSourceName));
14✔
847

848
            // Update interval settings visibility
849
            updateIntervalSettingsVisibility();
14✔
850

851
            // Since signals were blocked, this will ensure the tree is refreshed
852
            // when the computers tree is populated later in this function
853
        }
854
    }
855

856
    // Clear old column list (deprecated)
857
    // The computers tree will be populated based on available data sources
858
    refreshComputersTree();
27✔
859

860
    updateBuildStatus(QString("Loaded table: %1").arg(QString::fromStdString(info.name)));
27✔
861
    triggerPreviewDebounced();
27✔
862
}
27✔
863

864
void TableDesignerWidget::clearUI() {
49✔
865
    _current_table_id.clear();
49✔
866

867
    // Clear table info
868
    if (_table_info_widget) {
49✔
869
        _table_info_widget->setName("");
49✔
870
        _table_info_widget->setDescription("");
49✔
871
    }
872

873
    // Clear row source
874
    ui->row_data_source_combo->setCurrentIndex(-1);
49✔
875
    ui->row_info_label->setText("No row source selected");
49✔
876

877
    // Reset capture range and interval settings
878
    setCaptureRange(30000);// Default value
49✔
879
    if (ui->interval_beginning_radio) {
49✔
880
        ui->interval_beginning_radio->setChecked(true);
49✔
881
    }
882
    if (ui->interval_itself_radio) {
49✔
883
        ui->interval_itself_radio->setChecked(false);
49✔
884
    }
885
    if (ui->interval_settings_group) {
49✔
886
        ui->interval_settings_group->setVisible(false);
49✔
887
    }
888

889
    // Clear computers tree
890
    if (ui->computers_tree) {
49✔
891
        ui->computers_tree->clear();
49✔
892
    }
893

894
    // Disable controls
895
    ui->delete_table_btn->setEnabled(false);
49✔
896
    // Table info section is controlled separately
897
    ui->build_table_btn->setEnabled(false);
49✔
898
    if (auto gb = this->findChild<QGroupBox *>("row_source_group")) gb->setEnabled(false);
98✔
899
    if (auto gb = this->findChild<QGroupBox *>("column_design_group")) gb->setEnabled(false);
98✔
900
    if (_table_info_section) _table_info_section->setEnabled(false);
49✔
901

902
    updateBuildStatus("No table selected");
49✔
903
    if (_table_viewer) _table_viewer->clearTable();
49✔
904
}
49✔
905

906
void TableDesignerWidget::updateBuildStatus(QString const & message, bool is_error) {
94✔
907
    ui->build_status_label->setText(message);
94✔
908

909
    if (is_error) {
94✔
910
        ui->build_status_label->setStyleSheet("QLabel { color: red; font-weight: bold; }");
×
911
    } else {
912
        ui->build_status_label->setStyleSheet("QLabel { color: green; }");
94✔
913
    }
914
}
94✔
915

916
QStringList TableDesignerWidget::getAvailableDataSources() const {
106✔
917
    QStringList sources;
106✔
918

919
    if (!_data_manager) {
106✔
920
        qDebug() << "getAvailableDataSources: No table manager";
×
921
        return sources;
×
922
    }
923

924
    auto * reg = _data_manager->getTableRegistry();
106✔
925
    auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
106✔
926
    if (!data_manager_extension) {
106✔
927
        qDebug() << "getAvailableDataSources: No data manager extension";
×
928
        return sources;
×
929
    }
930

931
    if (!_data_manager) {
106✔
932
        qDebug() << "getAvailableDataSources: No data manager";
×
933
        return sources;
×
934
    }
935

936
    // Add TimeFrame keys as potential row sources
937
    // TimeFrames can define intervals for analysis
938
    auto timeframe_keys = _data_manager->getTimeFrameKeys();
106✔
939
    qDebug() << "getAvailableDataSources: TimeFrame keys:" << timeframe_keys.size();
106✔
940
    for (auto const & key: timeframe_keys) {
539✔
941
        QString source = QString("TimeFrame: %1").arg(QString::fromStdString(key.str()));
433✔
942
        sources << source;
433✔
943
        qDebug() << "  Added TimeFrame:" << source;
433✔
944
    }
433✔
945

946
    // Add DigitalEventSeries keys as potential row sources
947
    // Events can be used to define analysis windows or timestamps
948
    auto event_keys = _data_manager->getKeys<DigitalEventSeries>();
106✔
949
    qDebug() << "getAvailableDataSources: Event keys:" << event_keys.size();
106✔
950
    for (auto const & key: event_keys) {
321✔
951
        QString source = QString("Events: %1").arg(QString::fromStdString(key));
215✔
952
        sources << source;
215✔
953
        qDebug() << "  Added Events:" << source;
215✔
954
    }
215✔
955

956
    // Add DigitalIntervalSeries keys as potential row sources
957
    // Intervals directly define analysis windows
958
    auto interval_keys = _data_manager->getKeys<DigitalIntervalSeries>();
106✔
959
    qDebug() << "getAvailableDataSources: Interval keys:" << interval_keys.size();
106✔
960
    for (auto const & key: interval_keys) {
227✔
961
        QString source = QString("Intervals: %1").arg(QString::fromStdString(key));
121✔
962
        sources << source;
121✔
963
        qDebug() << "  Added Intervals:" << source;
121✔
964
    }
121✔
965

966
    // Add AnalogTimeSeries keys as data sources (for computers; not row selectors)
967
    auto analog_keys = _data_manager->getKeys<AnalogTimeSeries>();
106✔
968
    qDebug() << "getAvailableDataSources: Analog keys:" << analog_keys.size();
106✔
969
    for (auto const & key: analog_keys) {
318✔
970
        QString source = QString("analog:%1").arg(QString::fromStdString(key));
212✔
971
        sources << source;
212✔
972
        qDebug() << "  Added Analog:" << source;
212✔
973
    }
212✔
974

975
    // Add LineData keys as data sources (for computers; not row selectors)
976
    auto line_keys = _data_manager->getKeys<LineData>();
106✔
977
    qDebug() << "getAvailableDataSources: Line keys:" << line_keys.size();
106✔
978
    for (auto const & key: line_keys) {
212✔
979
        QString source = QString("lines:%1").arg(QString::fromStdString(key));
106✔
980
        sources << source;
106✔
981
        qDebug() << "  Added Lines:" << source;
106✔
982
    }
106✔
983

984
    qDebug() << "getAvailableDataSources: Total sources found:" << sources.size();
106✔
985

986
    return sources;
106✔
987
}
106✔
988

989
std::pair<std::optional<DataSourceVariant>, RowSelectorType>
990
TableDesignerWidget::createDataSourceVariant(QString const & data_source_string,
861✔
991
                                             std::shared_ptr<DataManagerExtension> data_manager_extension) const {
992
    std::optional<DataSourceVariant> result;
861✔
993
    RowSelectorType row_selector_type = RowSelectorType::IntervalBased;
861✔
994

995
    if (data_source_string.startsWith("TimeFrame: ")) {
861✔
996
        // TimeFrames are used with TimestampSelector for rows; no concrete data source needed
997
        row_selector_type = RowSelectorType::Timestamp;
343✔
998

999
    } else if (data_source_string.startsWith("Events: ")) {
518✔
1000
        QString source_name = data_source_string.mid(8);// Remove "Events: " prefix
170✔
1001
        // Event-based computers in the registry operate with interval rows
1002
        row_selector_type = RowSelectorType::IntervalBased;
170✔
1003

1004
        if (auto event_source = data_manager_extension->getEventSource(source_name.toStdString())) {
170✔
1005
            result = event_source;
170✔
1006
        }
170✔
1007

1008
    } else if (data_source_string.startsWith("Intervals: ")) {
518✔
1009
        QString source_name = data_source_string.mid(11);// Remove "Intervals: " prefix
96✔
1010
        row_selector_type = RowSelectorType::IntervalBased;
96✔
1011

1012
        if (auto interval_source = data_manager_extension->getIntervalSource(source_name.toStdString())) {
96✔
1013
            result = interval_source;
96✔
1014
        }
96✔
1015
    } else if (data_source_string.startsWith("analog:")) {
348✔
1016
        QString source_name = data_source_string.mid(7);// Remove "analog:" prefix
168✔
1017
        row_selector_type = RowSelectorType::IntervalBased;
168✔
1018

1019
        if (auto analog_source = data_manager_extension->getAnalogSource(source_name.toStdString())) {
168✔
1020
            result = analog_source;
168✔
1021
        }
168✔
1022
    } else if (data_source_string.startsWith("lines:")) {
252✔
1023
        QString source_name = data_source_string.mid(6);// Remove "lines:" prefix
84✔
1024
        row_selector_type = RowSelectorType::Timestamp; // LineData computers work with timestamp row selectors
84✔
1025

1026
        if (auto line_source = data_manager_extension->getLineSource(source_name.toStdString())) {
84✔
1027
            result = line_source;
84✔
1028
        }
84✔
1029
    }
84✔
1030

1031
    return {result, row_selector_type};
1,722✔
1032
}
861✔
1033

1034
void TableDesignerWidget::updateRowInfoLabel(QString const & selected_source) {
47✔
1035
    if (selected_source.isEmpty()) {
47✔
1036
        ui->row_info_label->setText("No row source selected");
×
1037
        return;
×
1038
    }
1039

1040
    // Parse the selected source to get type and name
1041
    QString source_type;
47✔
1042
    QString source_name;
47✔
1043

1044
    if (selected_source.startsWith("TimeFrame: ")) {
47✔
1045
        source_type = "TimeFrame";
25✔
1046
        source_name = selected_source.mid(11);// Remove "TimeFrame: " prefix
25✔
1047
    } else if (selected_source.startsWith("Events: ")) {
22✔
1048
        source_type = "Events";
×
1049
        source_name = selected_source.mid(8);// Remove "Events: " prefix
×
1050
    } else if (selected_source.startsWith("Intervals: ")) {
22✔
1051
        source_type = "Intervals";
22✔
1052
        source_name = selected_source.mid(11);// Remove "Intervals: " prefix
22✔
1053
    }
1054

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

1058
    if (!_data_manager) {
47✔
1059
        ui->row_info_label->setText(info_text);
×
1060
        return;
×
1061
    }
1062

1063
    auto * reg3 = _data_manager->getTableRegistry();
47✔
1064
    auto data_manager_extension = reg3 ? reg3->getDataManagerExtension() : nullptr;
47✔
1065
    if (!data_manager_extension) {
47✔
1066
        ui->row_info_label->setText(info_text);
×
1067
        return;
×
1068
    }
1069

1070
    auto const source_name_str = source_name.toStdString();
47✔
1071

1072
    // Add specific information based on source type
1073
    if (source_type == "TimeFrame") {
47✔
1074
        auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
25✔
1075
        if (timeframe) {
25✔
1076
            info_text += QString(" - %1 time points").arg(timeframe->getTotalFrameCount());
25✔
1077
        }
1078
    } else if (source_type == "Events") {
47✔
1079
        auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
1080
        if (event_series) {
×
1081
            auto events = event_series->getEventSeries();
×
1082
            info_text += QString(" - %1 events").arg(events.size());
×
1083
        }
×
1084
    } else if (source_type == "Intervals") {
22✔
1085
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
22✔
1086
        if (interval_series) {
22✔
1087
            auto intervals = interval_series->getDigitalIntervalSeries();
22✔
1088
            info_text += QString(" - %1 intervals").arg(intervals.size());
22✔
1089

1090
            // Add capture range and interval setting information
1091
            if (isIntervalItselfSelected()) {
22✔
1092
                info_text += QString("\nUsing intervals as-is (no capture range)");
2✔
1093
            } else {
1094
                int capture_range = getCaptureRange();
20✔
1095
                QString interval_point = isIntervalBeginningSelected() ? "beginning" : "end";
20✔
1096
                info_text += QString("\nCapture range: ±%1 samples around %2 of intervals").arg(capture_range).arg(interval_point);
20✔
1097
            }
20✔
1098
        }
22✔
1099
    }
22✔
1100

1101
    ui->row_info_label->setText(info_text);
47✔
1102
}
47✔
1103

1104
std::unique_ptr<IRowSelector> TableDesignerWidget::createRowSelector(QString const & row_source) {
24✔
1105
    // Parse the row source to get type and name
1106
    QString source_type;
24✔
1107
    QString source_name;
24✔
1108

1109
    if (row_source.startsWith("TimeFrame: ")) {
24✔
1110
        source_type = "TimeFrame";
4✔
1111
        source_name = row_source.mid(11);// Remove "TimeFrame: " prefix
4✔
1112
    } else if (row_source.startsWith("Events: ")) {
20✔
1113
        source_type = "Events";
×
1114
        source_name = row_source.mid(8);// Remove "Events: " prefix
×
1115
    } else if (row_source.startsWith("Intervals: ")) {
20✔
1116
        source_type = "Intervals";
20✔
1117
        source_name = row_source.mid(11);// Remove "Intervals: " prefix
20✔
1118
    } else {
1119
        qDebug() << "Unknown row source format:" << row_source;
×
1120
        return nullptr;
×
1121
    }
1122

1123
    auto const source_name_str = source_name.toStdString();
24✔
1124

1125
    try {
1126
        if (source_type == "TimeFrame") {
24✔
1127
            // Create IntervalSelector using TimeFrame
1128
            auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
4✔
1129
            if (!timeframe) {
4✔
1130
                qDebug() << "TimeFrame not found:" << source_name;
×
1131
                return nullptr;
×
1132
            }
1133

1134
            // Use timestamps to select all rows
1135
            std::vector<TimeFrameIndex> timestamps;
4✔
1136
            for (int64_t i = 0; i < timeframe->getTotalFrameCount(); ++i) {
408✔
1137
                timestamps.push_back(TimeFrameIndex(i));
404✔
1138
            }
1139
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe);
4✔
1140

1141
        } else if (source_type == "Events") {
24✔
1142
            // Create TimestampSelector using DigitalEventSeries
1143
            auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
1144
            if (!event_series) {
×
1145
                qDebug() << "DigitalEventSeries not found:" << source_name;
×
1146
                return nullptr;
×
1147
            }
1148

1149
            auto events = event_series->getEventSeries();
×
1150
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
×
1151
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
×
1152
            if (!timeframe_obj) {
×
1153
                qDebug() << "TimeFrame not found for events:" << timeframe_key.str();
×
1154
                return nullptr;
×
1155
            }
1156

1157
            // Convert events to TimeFrameIndex
1158
            std::vector<TimeFrameIndex> timestamps;
×
1159
            for (auto const & event: events) {
×
1160
                timestamps.push_back(TimeFrameIndex(static_cast<int64_t>(event)));
×
1161
            }
1162

1163
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe_obj);
×
1164

1165
        } else if (source_type == "Intervals") {
20✔
1166
            // Create IntervalSelector using DigitalIntervalSeries with capture range
1167
            auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
20✔
1168
            if (!interval_series) {
20✔
1169
                qDebug() << "DigitalIntervalSeries not found:" << source_name;
×
1170
                return nullptr;
×
1171
            }
1172

1173
            auto intervals = interval_series->getDigitalIntervalSeries();
20✔
1174
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
20✔
1175
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
20✔
1176
            if (!timeframe_obj) {
20✔
1177
                qDebug() << "TimeFrame not found for intervals:" << timeframe_key.str();
×
1178
                return nullptr;
×
1179
            }
1180

1181
            // Get capture range and interval setting
1182
            int capture_range = getCaptureRange();
20✔
1183
            bool use_beginning = isIntervalBeginningSelected();
20✔
1184
            bool use_interval_itself = isIntervalItselfSelected();
20✔
1185

1186
            // Create intervals based on the selected option
1187
            std::vector<TimeFrameInterval> tf_intervals;
20✔
1188
            for (auto const & interval: intervals) {
99✔
1189
                if (use_interval_itself) {
79✔
1190
                    // Use the interval as-is
1191
                    tf_intervals.emplace_back(TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
3✔
1192
                } else {
1193
                    // Determine the reference point (beginning or end of interval)
1194
                    int64_t reference_point;
1195
                    if (use_beginning) {
76✔
1196
                        reference_point = interval.start;
76✔
1197
                    } else {
1198
                        reference_point = interval.end;
×
1199
                    }
1200

1201
                    // Create a new interval around the reference point
1202
                    int64_t start_point = reference_point - capture_range;
76✔
1203
                    int64_t end_point = reference_point + capture_range;
76✔
1204

1205
                    // Ensure bounds are within the timeframe
1206
                    start_point = std::max(start_point, int64_t(0));
76✔
1207
                    end_point = std::min(end_point, static_cast<int64_t>(timeframe_obj->getTotalFrameCount() - 1));
76✔
1208

1209
                    tf_intervals.emplace_back(TimeFrameIndex(start_point), TimeFrameIndex(end_point));
76✔
1210
                }
1211
            }
1212

1213
            return std::make_unique<IntervalSelector>(std::move(tf_intervals), timeframe_obj);
20✔
1214
        }
20✔
1215

1216
    } catch (std::exception const & e) {
×
1217
        qDebug() << "Exception creating row selector:" << e.what();
×
1218
        return nullptr;
×
1219
    }
×
1220

1221
    qDebug() << "Unsupported row source type:" << source_type;
×
1222
    return nullptr;
×
1223
}
24✔
1224

1225
bool TableDesignerWidget::addColumnToBuilder(TableViewBuilder & builder, ColumnInfo const & column_info) {
×
1226
    // Use the simplified TableRegistry method that handles all the type checking internally
1227
    auto * reg = _data_manager->getTableRegistry();
×
1228
    if (!reg) {
×
1229
        qDebug() << "TableRegistry not available";
×
1230
        return false;
×
1231
    }
1232

1233
    bool success = reg->addColumnToBuilder(builder, column_info);
×
1234
    if (!success) {
×
1235
        qDebug() << "Failed to add column to builder:" << QString::fromStdString(column_info.name);
×
1236
    }
1237

1238
    return success;
×
1239
}
1240

1241
void TableDesignerWidget::updateIntervalSettingsVisibility() {
47✔
1242
    if (!ui->interval_settings_group) {
47✔
1243
        return;
×
1244
    }
1245

1246
    QString selected_key = ui->row_data_source_combo->currentText();
47✔
1247
    if (selected_key.isEmpty()) {
47✔
1248
        ui->interval_settings_group->setVisible(false);
×
1249
        if (ui->capture_range_spinbox) {
×
1250
            ui->capture_range_spinbox->setEnabled(false);
×
1251
        }
1252
        return;
×
1253
    }
1254

1255
    if (!_data_manager) {
47✔
1256
        ui->interval_settings_group->setVisible(false);
×
1257
        if (ui->capture_range_spinbox) {
×
1258
            ui->capture_range_spinbox->setEnabled(false);
×
1259
        }
1260
        return;
×
1261
    }
1262

1263
    // Check if the selected source is an interval series
1264
    if (selected_key.startsWith("Intervals: ")) {
47✔
1265
        ui->interval_settings_group->setVisible(true);
22✔
1266

1267
        // Enable/disable capture range based on interval setting
1268
        if (ui->capture_range_spinbox) {
22✔
1269
            bool use_interval_itself = isIntervalItselfSelected();
22✔
1270
            ui->capture_range_spinbox->setEnabled(!use_interval_itself);
22✔
1271
        }
1272
    } else {
1273
        ui->interval_settings_group->setVisible(false);
25✔
1274
        if (ui->capture_range_spinbox) {
25✔
1275
            ui->capture_range_spinbox->setEnabled(false);
25✔
1276
        }
1277
    }
1278
}
47✔
1279

1280
int TableDesignerWidget::getCaptureRange() const {
40✔
1281
    if (ui->capture_range_spinbox) {
40✔
1282
        return ui->capture_range_spinbox->value();
40✔
1283
    }
1284
    return 30000;// Default value
×
1285
}
1286

1287
void TableDesignerWidget::setCaptureRange(int value) {
49✔
1288
    if (ui->capture_range_spinbox) {
49✔
1289
        ui->capture_range_spinbox->blockSignals(true);
49✔
1290
        ui->capture_range_spinbox->setValue(value);
49✔
1291
        ui->capture_range_spinbox->blockSignals(false);
49✔
1292
    }
1293
}
49✔
1294

1295
bool TableDesignerWidget::isIntervalBeginningSelected() const {
40✔
1296
    if (ui->interval_beginning_radio) {
40✔
1297
        return ui->interval_beginning_radio->isChecked();
40✔
1298
    }
1299
    return true;// Default to beginning
×
1300
}
1301

1302
bool TableDesignerWidget::isIntervalItselfSelected() const {
64✔
1303
    if (ui->interval_itself_radio) {
64✔
1304
        return ui->interval_itself_radio->isChecked();
64✔
1305
    }
1306
    return false;// Default to not selected
×
1307
}
1308

1309
void TableDesignerWidget::triggerPreviewDebounced() {
163✔
1310
    if (_preview_debounce_timer) _preview_debounce_timer->start();
163✔
1311
    // Also trigger an immediate rebuild to support non-interactive/test contexts
1312
    rebuildPreviewNow();
163✔
1313
}
163✔
1314

1315
void TableDesignerWidget::rebuildPreviewNow() {
163✔
1316
    if (!_data_manager || !_table_viewer) return;
163✔
1317
    if (_current_table_id.isEmpty()) {
163✔
1318
        _table_viewer->clearTable();
73✔
1319
        return;
73✔
1320
    }
1321

1322
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
90✔
1323
    if (row_source.isEmpty()) {
90✔
1324
        _table_viewer->clearTable();
24✔
1325
        return;
24✔
1326
    }
1327

1328
    // Get enabled column infos from the computers tree
1329
    auto column_infos = getEnabledColumnInfos();
66✔
1330
    if (column_infos.empty()) {
66✔
1331
        _table_viewer->clearTable();
46✔
1332
        return;
46✔
1333
    }
1334

1335
    // Create row selector for the entire dataset
1336
    auto selector = createRowSelector(row_source);
20✔
1337
    if (!selector) {
20✔
1338
        _table_viewer->clearTable();
×
1339
        return;
×
1340
    }
1341

1342
    // Apply any saved column order for this table id
1343
    auto desiredOrder = _table_column_order.value(_current_table_id);
20✔
1344
    if (!desiredOrder.isEmpty()) {
20✔
1345
        std::vector<ColumnInfo> reordered;
12✔
1346
        reordered.reserve(column_infos.size());
12✔
1347
        for (auto const & name: desiredOrder) {
39✔
1348
            auto it = std::find_if(column_infos.begin(), column_infos.end(), [&](ColumnInfo const & ci) { return QString::fromStdString(ci.name) == name; });
61✔
1349
            if (it != column_infos.end()) {
27✔
1350
                reordered.push_back(*it);
15✔
1351
            }
1352
        }
1353
        for (auto const & ci: column_infos) {
33✔
1354
            if (std::find_if(reordered.begin(), reordered.end(), [&](ColumnInfo const & x) { return x.name == ci.name; }) == reordered.end()) {
48✔
1355
                reordered.push_back(ci);
6✔
1356
            }
1357
        }
1358
        column_infos = std::move(reordered);
12✔
1359
    }
12✔
1360

1361
    // Set up the table viewer with pagination
1362
    _table_viewer->setTableConfiguration(
100✔
1363
            std::move(selector),
20✔
1364
            std::move(column_infos),
20✔
1365
            _data_manager,
20✔
1366
            QString("Preview: %1").arg(_current_table_id),
40✔
1367
            row_source);
1368

1369
    // Capture the current visual order from the viewer
1370
    QStringList currentOrder;
20✔
1371
    if (_table_viewer) {
20✔
1372
        auto * tv = _table_viewer->findChild<QTableView *>();
20✔
1373
        if (tv && tv->model()) {
20✔
1374
            auto * header = tv->horizontalHeader();
20✔
1375
            int cols = tv->model()->columnCount();
20✔
1376
            for (int v = 0; header && v < cols; ++v) {
64✔
1377
                int logical = header->logicalIndex(v);
44✔
1378
                auto name = tv->model()->headerData(logical, Qt::Horizontal, Qt::DisplayRole).toString();
44✔
1379
                currentOrder.push_back(name);
44✔
1380
            }
44✔
1381
        }
1382
    }
1383
    if (!currentOrder.isEmpty()) {
20✔
1384
        _table_column_order[_current_table_id] = currentOrder;
20✔
1385
    }
1386
}
136✔
1387

1388
void TableDesignerWidget::refreshComputersTree() {
84✔
1389
    if (!_data_manager) return;
84✔
1390

1391
    _updating_computers_tree = true;
84✔
1392

1393
    // Preserve previous checkbox states and custom column names
1394
    std::map<std::string, std::pair<Qt::CheckState, QString>> previous_states;
84✔
1395
    if (ui->computers_tree && ui->computers_tree->topLevelItemCount() > 0) {
84✔
1396
        for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
598✔
1397
            auto * top_level_item = ui->computers_tree->topLevelItem(i);
545✔
1398
            // Handle both grouped and individual modes
1399
            for (int j = 0; j < top_level_item->childCount(); ++j) {
2,303✔
1400
                auto * child_item = top_level_item->child(j);
1,758✔
1401
                if (child_item->childCount() > 0) {
1,758✔
1402
                    // This is a computer item under a group/data source
1403
                    for (int k = 0; k < child_item->childCount(); ++k) {
×
1404
                        auto * computer_item = child_item->child(k);
×
1405
                        QString ds = computer_item->data(0, Qt::UserRole).toString();
×
1406
                        QString cn = computer_item->data(1, Qt::UserRole).toString();
×
1407
                        std::string key = (ds + "||" + cn).toStdString();
×
1408
                        previous_states[key] = {computer_item->checkState(1), computer_item->text(2)};
×
1409
                    }
×
1410
                } else {
1411
                    // This is a computer item directly under data source (individual mode)
1412
                    QString ds = child_item->data(0, Qt::UserRole).toString();
1,758✔
1413
                    QString cn = child_item->data(1, Qt::UserRole).toString();
1,758✔
1414
                    std::string key = (ds + "||" + cn).toStdString();
1,758✔
1415
                    previous_states[key] = {child_item->checkState(1), child_item->text(2)};
1,758✔
1416
                }
1,758✔
1417
            }
1418
        }
1419
    }
1420

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

1424
    // Clean up parameter widgets
1425
    _computer_parameter_widgets.clear();
84✔
1426
    _parameter_controls.clear();
84✔
1427

1428
    auto * registry = _data_manager->getTableRegistry();
84✔
1429
    if (!registry) {
84✔
1430
        _updating_computers_tree = false;
×
1431
        return;
×
1432
    }
1433

1434
    auto data_manager_extension = registry->getDataManagerExtension();
84✔
1435
    if (!data_manager_extension) {
84✔
1436
        _updating_computers_tree = false;
×
1437
        return;
×
1438
    }
1439

1440
    auto & computer_registry = registry->getComputerRegistry();
84✔
1441

1442
    // Get available data sources
1443
    auto data_sources = getAvailableDataSources();
84✔
1444

1445
    if (_group_mode) {
84✔
1446
        // Group similar data sources together
1447
        std::map<std::string, QStringList> groups;
84✔
1448

1449
        // First pass: group data sources by their extracted group name
1450
        for (QString const & data_source: data_sources) {
945✔
1451
            std::string group_name = extractGroupName(data_source);
861✔
1452
            groups[group_name].append(data_source);
861✔
1453
        }
861✔
1454

1455
        // Second pass: create tree structure
1456
        for (auto const & [group_name, group_members]: groups) {
945✔
1457
            if (group_members.size() > 1) {
861✔
1458
                // Create group item
1459
                auto * group_item = new QTreeWidgetItem(ui->computers_tree);
×
1460
                group_item->setText(0, QString::fromStdString(group_name) + " (Group)");
×
1461
                group_item->setFlags(Qt::ItemIsEnabled);
×
NEW
1462
                group_item->setExpanded(false);// Start collapsed
×
1463

1464
                // Get computers available for this group (use first member to determine available computers)
1465
                auto [first_variant, row_selector_type] = createDataSourceVariant(group_members.first(), data_manager_extension);
×
1466
                if (!first_variant.has_value()) {
×
1467
                    continue;
×
1468
                }
1469

1470
                auto available_computers = computer_registry.getAvailableComputers(row_selector_type, first_variant.value());
×
1471

1472
                // Add computers as children of the group
NEW
1473
                for (auto const & computer_info: available_computers) {
×
1474
                    auto * computer_item = new QTreeWidgetItem(group_item);
×
1475
                    computer_item->setText(0, QString::fromStdString(computer_info.name));
×
1476
                    computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
×
1477
                    computer_item->setCheckState(1, Qt::Unchecked);
×
1478

1479
                    // Generate default column name using group name
1480
                    QString default_name = QString("%1_%2").arg(QString::fromStdString(group_name), QString::fromStdString(computer_info.name));
×
1481
                    computer_item->setText(2, default_name);
×
1482
                    computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
×
1483

1484
                    // Store group members and computer name for later use
NEW
1485
                    computer_item->setData(0, Qt::UserRole, group_members.join("||"));// Store all group members
×
1486
                    computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
×
NEW
1487
                    computer_item->setData(2, Qt::UserRole, true);// Mark as group computer
×
1488

1489
                    // Create parameter widget if computer has parameters
1490
                    if (!computer_info.parameterDescriptors.empty()) {
×
NEW
1491
                        auto * param_widget = createParameterWidget(QString::fromStdString(computer_info.name),
×
NEW
1492
                                                                    computer_info.parameterDescriptors);
×
1493
                        if (param_widget) {
×
1494
                            ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
×
1495
                            _computer_parameter_widgets[computer_item] = param_widget;
×
1496
                        }
1497
                    }
1498

1499
                    // Restore previous state if present (use first member for key)
1500
                    std::string prev_key = (group_members.first() + "||" + QString::fromStdString(computer_info.name)).toStdString();
×
1501
                    auto it_prev = previous_states.find(prev_key);
×
1502
                    if (it_prev != previous_states.end()) {
×
1503
                        computer_item->setCheckState(1, it_prev->second.first);
×
1504
                        if (!it_prev->second.second.isEmpty()) {
×
1505
                            computer_item->setText(2, it_prev->second.second);
×
1506
                        }
1507
                    }
1508
                }
×
1509
            } else {
×
1510
                // Single item - create as individual data source
1511
                QString const & data_source = group_members.first();
861✔
1512
                auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
861✔
1513
                data_source_item->setText(0, data_source);
861✔
1514
                data_source_item->setFlags(Qt::ItemIsEnabled);
861✔
1515
                data_source_item->setExpanded(false);
861✔
1516

1517
                auto [data_source_variant, row_selector_type] = createDataSourceVariant(data_source, data_manager_extension);
861✔
1518
                if (!data_source_variant.has_value()) {
861✔
1519
                    continue;
343✔
1520
                }
1521

1522
                auto available_computers = computer_registry.getAvailableComputers(row_selector_type, data_source_variant.value());
518✔
1523

1524
                for (auto const & computer_info: available_computers) {
3,296✔
1525
                    auto * computer_item = new QTreeWidgetItem(data_source_item);
2,778✔
1526
                    computer_item->setText(0, QString::fromStdString(computer_info.name));
2,778✔
1527
                    computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
2,778✔
1528
                    computer_item->setCheckState(1, Qt::Unchecked);
2,778✔
1529

1530
                    QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
2,778✔
1531
                    computer_item->setText(2, default_name);
2,778✔
1532
                    computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
2,778✔
1533

1534
                    computer_item->setData(0, Qt::UserRole, data_source);
2,778✔
1535
                    computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
2,778✔
1536
                    computer_item->setData(2, Qt::UserRole, false);// Mark as individual computer
2,778✔
1537

1538
                    // Create parameter widget if computer has parameters
1539
                    if (!computer_info.parameterDescriptors.empty()) {
2,778✔
1540
                        auto * param_widget = createParameterWidget(QString::fromStdString(computer_info.name),
254✔
1541
                                                                    computer_info.parameterDescriptors);
254✔
1542
                        if (param_widget) {
254✔
1543
                            ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
254✔
1544
                            _computer_parameter_widgets[computer_item] = param_widget;
254✔
1545
                        }
1546
                    }
1547

1548
                    std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
2,778✔
1549
                    auto it_prev = previous_states.find(prev_key);
2,778✔
1550
                    if (it_prev != previous_states.end()) {
2,778✔
1551
                        computer_item->setCheckState(1, it_prev->second.first);
1,755✔
1552
                        if (!it_prev->second.second.isEmpty()) {
1,755✔
1553
                            computer_item->setText(2, it_prev->second.second);
1,755✔
1554
                        }
1555
                    }
1556
                }
2,778✔
1557
            }
861✔
1558
        }
1559
    } else {
84✔
1560
        // Individual mode - create tree structure: Data Source -> Computers (original behavior)
1561
        for (QString const & data_source: data_sources) {
×
1562
            auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
×
1563
            data_source_item->setText(0, data_source);
×
1564
            data_source_item->setFlags(Qt::ItemIsEnabled);
×
1565
            data_source_item->setExpanded(false);// Start collapsed
×
1566

1567
            // Convert data source string to DataSourceVariant and determine RowSelectorType
1568
            auto [data_source_variant, row_selector_type] = createDataSourceVariant(data_source, data_manager_extension);
×
1569

1570
            if (!data_source_variant.has_value()) {
×
1571
                qDebug() << "Failed to create data source variant for:" << data_source;
×
1572
                continue;
×
1573
            }
1574

1575
            // Get available computers for this specific data source and row selector combination
1576
            auto available_computers = computer_registry.getAvailableComputers(row_selector_type, data_source_variant.value());
×
1577

1578
            // Add compatible computers as children
1579
            for (auto const & computer_info: available_computers) {
×
1580
                auto * computer_item = new QTreeWidgetItem(data_source_item);
×
1581
                computer_item->setText(0, QString::fromStdString(computer_info.name));
×
1582
                computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
×
1583
                computer_item->setCheckState(1, Qt::Unchecked);
×
1584

1585
                // Generate default column name
1586
                QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
×
1587
                computer_item->setText(2, default_name);
×
1588
                computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
×
1589

1590
                // Store data source and computer name for later use
1591
                computer_item->setData(0, Qt::UserRole, data_source);
×
1592
                computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
×
NEW
1593
                computer_item->setData(2, Qt::UserRole, false);// Mark as individual computer
×
1594

1595
                // Create parameter widget if computer has parameters
1596
                if (!computer_info.parameterDescriptors.empty()) {
×
NEW
1597
                    auto * param_widget = createParameterWidget(QString::fromStdString(computer_info.name),
×
NEW
1598
                                                                computer_info.parameterDescriptors);
×
1599
                    if (param_widget) {
×
1600
                        ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
×
1601
                        _computer_parameter_widgets[computer_item] = param_widget;
×
1602
                    }
1603
                }
1604

1605
                // Restore previous state if present
1606
                std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
×
1607
                auto it_prev = previous_states.find(prev_key);
×
1608
                if (it_prev != previous_states.end()) {
×
1609
                    computer_item->setCheckState(1, it_prev->second.first);
×
1610
                    if (!it_prev->second.second.isEmpty()) {
×
1611
                        computer_item->setText(2, it_prev->second.second);
×
1612
                    }
1613
                }
1614
            }
×
1615
        }
×
1616
    }
1617

1618
    // Resize columns to content
1619
    ui->computers_tree->resizeColumnToContents(0);
84✔
1620
    ui->computers_tree->resizeColumnToContents(1);
84✔
1621
    ui->computers_tree->resizeColumnToContents(2);
84✔
1622
    ui->computers_tree->resizeColumnToContents(3);
84✔
1623

1624
    _updating_computers_tree = false;
84✔
1625

1626
    // Update preview after refresh
1627
    triggerPreviewDebounced();
84✔
1628
}
168✔
1629

1630
void TableDesignerWidget::setJsonTemplateFromCurrentState() {
4✔
1631
    if (!_table_json_widget) return;
4✔
1632
    // Build a minimal JSON template representing current UI state
1633
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
4✔
1634
    auto columns = getEnabledColumnInfos();
4✔
1635
    if (row_source.isEmpty() && columns.empty()) {
4✔
1636
        _table_json_widget->setJsonText("{}");
×
1637
        return;
×
1638
    }
1639

1640
    QString row_type;
4✔
1641
    QString row_source_name;
4✔
1642
    if (row_source.startsWith("TimeFrame: ")) {
4✔
1643
        row_type = "timestamp";
1✔
1644
        row_source_name = row_source.mid(11);
1✔
1645
    } else if (row_source.startsWith("Events: ")) {
3✔
1646
        row_type = "timestamp";
×
1647
        row_source_name = row_source.mid(8);
×
1648
    } else if (row_source.startsWith("Intervals: ")) {
3✔
1649
        row_type = "interval";
3✔
1650
        row_source_name = row_source.mid(11);
3✔
1651
    }
1652

1653
    QStringList column_entries;
4✔
1654
    for (auto const & c: columns) {
10✔
1655
        // Strip any internal prefixes for JSON to keep schema user-friendly
1656
        QString ds = QString::fromStdString(c.dataSourceName);
6✔
1657
        if (ds.startsWith("events:")) ds = ds.mid(7);
6✔
1658
        else if (ds.startsWith("intervals:"))
4✔
1659
            ds = ds.mid(10);
3✔
1660
        else if (ds.startsWith("analog:"))
1✔
1661
            ds = ds.mid(7);
×
1662

1663
        QString entry = QString(
18✔
1664
                                "{\n  \"name\": \"%1\",\n  \"description\": \"%2\",\n  \"data_source\": \"%3\",\n  \"computer\": \"%4\"%5\n}")
1665
                                .arg(QString::fromStdString(c.name))
24✔
1666
                                .arg(QString::fromStdString(c.description))
24✔
1667
                                .arg(ds)
24✔
1668
                                .arg(QString::fromStdString(c.computerName))
24✔
1669
                                .arg(c.parameters.empty() ? QString() : QString(",\n  \"parameters\": {}"));
12✔
1670
        column_entries << entry;
6✔
1671
    }
6✔
1672

1673
    QString table_name = _table_info_widget ? _table_info_widget->getName() : _current_table_id;
4✔
1674
    QString json = QString(
12✔
1675
                           "{\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}")
1676
                           .arg(_current_table_id)
16✔
1677
                           .arg(table_name)
16✔
1678
                           .arg(row_type)
16✔
1679
                           .arg(row_source_name)
16✔
1680
                           .arg(column_entries.join(",\n"));
8✔
1681

1682
    _table_json_widget->setJsonText(json);
4✔
1683
}
4✔
1684

1685
void TableDesignerWidget::applyJsonTemplateToUI(QString const & jsonText) {
7✔
1686
    // Very light-weight parser using Qt to extract essential fields.
1687
    // Assumes a schema similar to tests under computers *.test.cpp.
1688
    QJsonParseError err;
7✔
1689
    QByteArray bytes = jsonText.toUtf8();
7✔
1690
    QJsonDocument doc = QJsonDocument::fromJson(bytes, &err);
7✔
1691
    if (err.error != QJsonParseError::NoError || !doc.isObject()) {
7✔
1692
        // Compute line/column from byte offset if possible
1693
        int64_t offset = static_cast<int64_t>(err.offset);
1✔
1694
        int line = 1;
1✔
1695
        int col = 1;
1✔
1696
        // Avoid operator[] ambiguity on some compilers by using qsizetype and at()
1697
        qsizetype len = std::min<qsizetype>(bytes.size(), static_cast<qsizetype>(offset));
1✔
1698
        for (qsizetype i = 0; i < len; ++i) {
149✔
1699
            char ch = bytes.at(i);
148✔
1700
            if (ch == '\n') {
148✔
1701
                ++line;
6✔
1702
                col = 1;
6✔
1703
            } else {
1704
                ++col;
142✔
1705
            }
1706
        }
1707
        QString detail = err.error != QJsonParseError::NoError
1✔
1708
                                 ? QString("%1 (line %2, column %3)").arg(err.errorString()).arg(line).arg(col)
3✔
1709
                                 : QString("JSON root must be an object");
2✔
1710
        auto * box = new QMessageBox(this);
1✔
1711
        box->setIcon(QMessageBox::Critical);
1✔
1712
        box->setWindowTitle("Invalid JSON");
1✔
1713
        box->setText(QString("JSON format is invalid: %1").arg(detail));
1✔
1714
        box->setAttribute(Qt::WA_DeleteOnClose);
1✔
1715
        box->show();
1✔
1716
        return;
1✔
1717
    }
1✔
1718
    auto obj = doc.object();
6✔
1719
    if (!obj.contains("tables") || !obj["tables"].isArray()) {
6✔
1720
        auto * box = new QMessageBox(this);
×
1721
        box->setIcon(QMessageBox::Critical);
×
1722
        box->setWindowTitle("Invalid JSON");
×
1723
        box->setText("Missing required key: tables (array)");
×
1724
        box->setAttribute(Qt::WA_DeleteOnClose);
×
1725
        box->show();
×
1726
        return;
×
1727
    }
1728
    auto tables = obj["tables"].toArray();
6✔
1729
    if (tables.isEmpty() || !tables[0].isObject()) return;
6✔
1730
    auto table = tables[0].toObject();
6✔
1731

1732
    // Row selector
1733
    QStringList errors;
6✔
1734
    QString rs_type;
6✔
1735
    QString rs_source;
6✔
1736
    if (table.contains("row_selector") && table["row_selector"].isObject()) {
6✔
1737
        auto rs = table["row_selector"].toObject();
5✔
1738
        rs_type = rs.value("type").toString();
5✔
1739
        rs_source = rs.value("source").toString();
5✔
1740
        if (rs_type.isEmpty() || rs_source.isEmpty()) {
5✔
1741
            errors << "Missing required keys in row_selector: 'type' and/or 'source'";
×
1742
        } else {
1743
            // Validate existence
1744
            bool source_ok = false;
5✔
1745
            if (rs_type == "interval") {
5✔
1746
                source_ok = (_data_manager && _data_manager->getData<DigitalIntervalSeries>(rs_source.toStdString()) != nullptr);
5✔
1747
            } else if (rs_type == "timestamp") {
×
1748
                source_ok = (_data_manager && (_data_manager->getTime(TimeKey(rs_source.toStdString())) != nullptr ||
×
1749
                                               _data_manager->getData<DigitalEventSeries>(rs_source.toStdString()) != nullptr));
×
1750
            } else {
1751
                errors << QString("Unsupported row_selector type: %1").arg(rs_type);
×
1752
            }
1753
            if (!source_ok) {
5✔
1754
                errors << QString("Row selector data key not found in DataManager: %1").arg(rs_source);
1✔
1755
            } else {
1756
                // Apply selection to UI
1757
                QString entry;
4✔
1758
                if (rs_type == "interval") {
4✔
1759
                    entry = QString("Intervals: %1").arg(rs_source);
4✔
1760
                } else if (rs_type == "timestamp") {
×
1761
                    // Prefer TimeFrame, fallback to Events
1762
                    entry = QString("TimeFrame: %1").arg(rs_source);
×
1763
                    int idx_tf = ui->row_data_source_combo->findText(entry);
×
1764
                    if (idx_tf < 0) entry = QString("Events: %1").arg(rs_source);
×
1765
                }
1766
                int idx = ui->row_data_source_combo->findText(entry);
4✔
1767
                if (idx >= 0) {
4✔
1768
                    ui->row_data_source_combo->setCurrentIndex(idx);
4✔
1769
                    // Ensure computers tree reflects this row selector before enabling columns
1770
                    refreshComputersTree();
4✔
1771
                } else {
1772
                    errors << QString("Row selector entry not available in UI: %1").arg(entry);
×
1773
                }
1774
            }
4✔
1775
        }
1776
    } else {
5✔
1777
        errors << "Missing required key: row_selector (object)";
1✔
1778
    }
1779

1780
    // Columns: enable matching computers and set column names
1781
    if (table.contains("columns") && table["columns"].isArray()) {
6✔
1782
        auto cols = table["columns"].toArray();
6✔
1783
        auto * tree = ui->computers_tree;
6✔
1784
        // Avoid recursive preview rebuilds while we toggle many items
1785
        bool prevBlocked = tree->blockSignals(true);
6✔
1786
        for (auto const & cval: cols) {
12✔
1787
            if (!cval.isObject()) continue;
6✔
1788
            auto cobj = cval.toObject();
6✔
1789
            QString data_source = cobj.value("data_source").toString();
6✔
1790
            QString computer = cobj.value("computer").toString();
6✔
1791
            QString name = cobj.value("name").toString();
6✔
1792
            if (data_source.isEmpty() || computer.isEmpty() || name.isEmpty()) {
6✔
1793
                errors << "Missing required keys in column: 'name', 'data_source', and 'computer'";
×
1794
                continue;
×
1795
            }
1796
            // Validate data source existence
1797
            bool has_ds = (_data_manager && (_data_manager->getData<DigitalEventSeries>(data_source.toStdString()) != nullptr ||
19✔
1798
                                             _data_manager->getData<DigitalIntervalSeries>(data_source.toStdString()) != nullptr ||
7✔
1799
                                             _data_manager->getData<AnalogTimeSeries>(data_source.toStdString()) != nullptr));
13✔
1800
            if (!has_ds) {
6✔
1801
                errors << QString("Data key not found in DataManager: %1").arg(data_source);
1✔
1802
            }
1803
            // Validate computer exists
1804
            bool computer_exists = false;
6✔
1805
            if (_data_manager) {
6✔
1806
                if (auto * reg = _data_manager->getTableRegistry()) {
6✔
1807
                    auto & cr = reg->getComputerRegistry();
6✔
1808
                    computer_exists = cr.findComputerInfo(computer.toStdString());
6✔
1809
                }
1810
            }
1811
            if (!computer_exists) {
6✔
1812
                errors << QString("Requested computer does not exist: %1").arg(computer);
2✔
1813
            }
1814
            // Validate compatibility (heuristic)
1815
            bool type_event = false, type_interval = false, type_analog = false;
6✔
1816
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
66✔
1817
                auto * ds_item = tree->topLevelItem(i);
60✔
1818
                QString ds_text = ds_item->text(0);
60✔
1819
                if (ds_text.contains(data_source)) {
60✔
1820
                    if (ds_text.startsWith("Events: ")) type_event = true;
5✔
1821
                    else if (ds_text.startsWith("Intervals: "))
×
1822
                        type_interval = true;
×
1823
                    else if (ds_text.startsWith("analog:"))
×
1824
                        type_analog = true;
×
1825
                }
1826
            }
60✔
1827
            QString ds_repr;
6✔
1828
            if (type_event) ds_repr = QString("Events: %1").arg(data_source);
6✔
1829
            else if (type_interval)
1✔
1830
                ds_repr = QString("Intervals: %1").arg(data_source);
×
1831
            else if (type_analog)
1✔
1832
                ds_repr = QString("analog:%1").arg(data_source);
×
1833
            if (!ds_repr.isEmpty() && !isComputerCompatibleWithDataSource(computer.toStdString(), ds_repr)) {
6✔
1834
                errors << QString("Computer '%1' is not valid for data source type requested (%2)").arg(computer, ds_repr);
2✔
1835
            }
1836

1837
            // Find matching tree item with strict preference
1838
            QString exact_events = QString("Events: %1").arg(data_source);
6✔
1839
            QString exact_intervals = QString("Intervals: %1").arg(data_source);
6✔
1840
            QString exact_analog = QString("analog:%1").arg(data_source);
6✔
1841
            QTreeWidgetItem * matched_ds = nullptr;
6✔
1842
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
31✔
1843
                auto * ds_item = tree->topLevelItem(i);
30✔
1844
                QString t = ds_item->text(0);
30✔
1845
                if (t == exact_events || t == exact_intervals || t == exact_analog) {
30✔
1846
                    matched_ds = ds_item;
5✔
1847
                    break;
5✔
1848
                }
1849
            }
30✔
1850
            if (!matched_ds) {
6✔
1851
                for (int i = 0; i < tree->topLevelItemCount(); ++i) {
11✔
1852
                    auto * ds_item = tree->topLevelItem(i);
10✔
1853
                    QString t = ds_item->text(0);
10✔
1854
                    if (t.contains(data_source) || t.endsWith(data_source)) {
10✔
1855
                        matched_ds = ds_item;
×
1856
                        break;
×
1857
                    }
1858
                }
10✔
1859
            }
1860
            if (matched_ds) {
6✔
1861
                for (int j = 0; j < matched_ds->childCount(); ++j) {
20✔
1862
                    auto * comp_item = matched_ds->child(j);
15✔
1863
                    QString comp_text = comp_item->text(0).trimmed();
15✔
1864
                    if (comp_text == computer || comp_text.contains(computer)) {
15✔
1865
                        comp_item->setCheckState(1, Qt::Checked);
3✔
1866
                        if (!name.isEmpty()) comp_item->setText(2, name);
3✔
1867
                    }
1868
                }
15✔
1869
            } else {
1870
                errors << QString("Data source not found in tree: %1").arg(data_source);
1✔
1871
            }
1872
        }
6✔
1873
        tree->blockSignals(prevBlocked);
6✔
1874
        if (!errors.isEmpty()) {
6✔
1875
            auto * box = new QMessageBox(this);
4✔
1876
            box->setIcon(QMessageBox::Critical);
4✔
1877
            box->setWindowTitle("Invalid Table JSON");
4✔
1878
            box->setText(errors.join("\n"));
4✔
1879
            box->setAttribute(Qt::WA_DeleteOnClose);
4✔
1880
            box->show();
4✔
1881
            return;
4✔
1882
        }
1883
        triggerPreviewDebounced();
2✔
1884
    }
6✔
1885
}
36✔
1886

1887
void TableDesignerWidget::onComputersTreeItemChanged() {
23,972✔
1888
    if (_updating_computers_tree) return;
23,972✔
1889

1890
    // Trigger preview update when checkbox states change
1891
    triggerPreviewDebounced();
16✔
1892
}
1893

1894
void TableDesignerWidget::onComputersTreeItemEdited(QTreeWidgetItem * item, int column) {
23,972✔
1895
    if (_updating_computers_tree) return;
23,972✔
1896

1897
    // Only respond to column name edits (column 2)
1898
    if (column == 2) {
16✔
1899
        // Column name was edited, trigger preview update
1900
        triggerPreviewDebounced();
1✔
1901
    }
1902
}
1903

1904
std::vector<ColumnInfo> TableDesignerWidget::getEnabledColumnInfos() const {
80✔
1905
    std::vector<ColumnInfo> column_infos;
80✔
1906

1907
    if (!ui->computers_tree) return column_infos;
80✔
1908

1909
    // Iterate through all data source items
1910
    for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
901✔
1911
        auto * data_source_item = ui->computers_tree->topLevelItem(i);
821✔
1912

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

1917
            // Check if this computer is enabled
1918
            if (computer_item->checkState(1) == Qt::Checked) {
2,658✔
1919
                QString data_source = computer_item->data(0, Qt::UserRole).toString();
50✔
1920
                QString computer_name = computer_item->data(1, Qt::UserRole).toString();
50✔
1921
                QString column_name = computer_item->text(2);
50✔
1922
                bool is_group_computer = computer_item->data(2, Qt::UserRole).toBool();
50✔
1923

1924
                // Get parameter values for this computer
1925
                std::map<std::string, std::string> parameters = getParameterValues(computer_name);
50✔
1926

1927
                if (is_group_computer) {
50✔
1928
                    // This is a group computer - create columns for all members
1929
                    QStringList group_members = data_source.split("||");
×
1930

NEW
1931
                    for (QString const & member: group_members) {
×
1932
                        // Generate individual column name (e.g., "spike_1_Mean", "spike_2_Mean")
1933
                        QString individual_column_name = generateDefaultColumnName(member, computer_name);
×
1934

1935
                        // Create ColumnInfo for each group member
1936
                        QString source_key = member;
×
1937
                        if (source_key.startsWith("Events: ")) {
×
1938
                            source_key = QString("events:%1").arg(source_key.mid(8));
×
1939
                        } else if (source_key.startsWith("Intervals: ")) {
×
1940
                            source_key = QString("intervals:%1").arg(source_key.mid(11));
×
1941
                        } else if (source_key.startsWith("analog:")) {
×
1942
                            source_key = source_key;// already prefixed
×
1943
                        } else if (source_key.startsWith("lines:")) {
×
1944
                            source_key = source_key;// already prefixed
×
1945
                        } else if (source_key.startsWith("TimeFrame: ")) {
×
1946
                            // TimeFrame used only for row selector; columns require concrete sources
1947
                            source_key = source_key.mid(11);
×
1948
                        }
1949

1950
                        ColumnInfo info(individual_column_name.toStdString(),
×
1951
                                        QString("Column from %1 using %2 (group applied)").arg(member, computer_name).toStdString(),
×
1952
                                        source_key.toStdString(),
×
1953
                                        computer_name.toStdString());
×
1954

1955
                        // Set parameters
1956
                        info.parameters = parameters;
×
1957

1958
                        // Set output type based on computer info
1959
                        if (auto * registry = _data_manager->getTableRegistry()) {
×
1960
                            auto & computer_registry = registry->getComputerRegistry();
×
1961
                            auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
×
1962
                            if (computer_info) {
×
1963
                                info.outputType = computer_info->outputType;
×
1964
                                info.outputTypeName = computer_info->outputTypeName;
×
1965
                                info.isVectorType = computer_info->isVectorType;
×
1966
                                if (info.isVectorType) {
×
1967
                                    info.elementType = computer_info->elementType;
×
1968
                                    info.elementTypeName = computer_info->elementTypeName;
×
1969
                                }
1970
                            }
1971
                        }
1972

1973
                        column_infos.push_back(std::move(info));
×
1974
                    }
×
1975
                } else {
×
1976
                    // Individual computer - original behavior
1977
                    if (column_name.isEmpty()) {
50✔
1978
                        // Clean the data source name before generating column name
1979
                        QString clean_data_source = data_source;
×
1980
                        if (clean_data_source.startsWith("lines:")) {
×
NEW
1981
                            clean_data_source = clean_data_source.mid(6);// Remove "lines:" prefix
×
1982
                        }
1983
                        column_name = generateDefaultColumnName(clean_data_source, computer_name);
×
1984
                    }
×
1985

1986
                    // Create ColumnInfo (use raw key without UI prefixes)
1987
                    QString source_key = data_source;
50✔
1988
                    if (source_key.startsWith("Events: ")) {
50✔
1989
                        source_key = QString("events:%1").arg(source_key.mid(8));
21✔
1990
                    } else if (source_key.startsWith("Intervals: ")) {
29✔
1991
                        source_key = QString("intervals:%1").arg(source_key.mid(11));
23✔
1992
                    } else if (source_key.startsWith("analog:")) {
6✔
1993
                        source_key = source_key;// already prefixed
×
1994
                    } else if (source_key.startsWith("lines:")) {
6✔
1995
                        source_key = source_key;// already prefixed
6✔
1996
                    } else if (source_key.startsWith("TimeFrame: ")) {
×
1997
                        // TimeFrame used only for row selector; columns require concrete sources
1998
                        source_key = source_key.mid(11);
×
1999
                    }
2000

2001
                    ColumnInfo info(column_name.toStdString(),
150✔
2002
                                    QString("Column from %1 using %2").arg(data_source, computer_name).toStdString(),
100✔
2003
                                    source_key.toStdString(),
100✔
2004
                                    computer_name.toStdString());
250✔
2005

2006
                    // Set parameters
2007
                    info.parameters = parameters;
50✔
2008

2009
                    // Set output type based on computer info
2010
                    if (auto * registry = _data_manager->getTableRegistry()) {
50✔
2011
                        auto & computer_registry = registry->getComputerRegistry();
50✔
2012
                        auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
50✔
2013
                        if (computer_info) {
50✔
2014
                            info.outputType = computer_info->outputType;
50✔
2015
                            info.outputTypeName = computer_info->outputTypeName;
50✔
2016
                            info.isVectorType = computer_info->isVectorType;
50✔
2017
                            if (info.isVectorType) {
50✔
2018
                                info.elementType = computer_info->elementType;
6✔
2019
                                info.elementTypeName = computer_info->elementTypeName;
6✔
2020
                            }
2021
                        }
2022
                    }
2023

2024
                    column_infos.push_back(std::move(info));
50✔
2025
                }
50✔
2026
            }
50✔
2027
        }
2028
    }
2029

2030
    return column_infos;
80✔
2031
}
×
2032

2033
bool TableDesignerWidget::isComputerCompatibleWithDataSource(std::string const & computer_name, QString const & data_source) const {
5✔
2034
    if (!_data_manager) return false;
5✔
2035

2036
    auto * registry = _data_manager->getTableRegistry();
5✔
2037
    if (!registry) return false;
5✔
2038

2039
    auto & computer_registry = registry->getComputerRegistry();
5✔
2040
    auto computer_info = computer_registry.findComputerInfo(computer_name);
5✔
2041
    if (!computer_info) return false;
5✔
2042

2043
    // Basic compatibility check based on data source type and common computer patterns
2044
    if (data_source.startsWith("Events: ")) {
3✔
2045
        // Event-based computers typically have "Event" in their name
2046
        return computer_name.find("Event") != std::string::npos;
3✔
2047
    } else if (data_source.startsWith("Intervals: ")) {
×
2048
        // Interval-based computers typically work with intervals or events
2049
        return computer_name.find("Event") != std::string::npos ||
×
2050
               computer_name.find("Interval") != std::string::npos;
×
2051
    } else if (data_source.startsWith("analog:")) {
×
2052
        // Analog-based computers typically have "Analog" in their name
2053
        return computer_name.find("Analog") != std::string::npos;
×
2054
    } else if (data_source.startsWith("TimeFrame: ")) {
×
2055
        // TimeFrame-based computers - generally most computers can work with timestamps
2056
        return computer_name.find("Timestamp") != std::string::npos ||
×
2057
               computer_name.find("Time") != std::string::npos;
×
2058
    }
2059

2060
    // Default: assume compatibility for unrecognized patterns
2061
    return true;
×
2062
}
2063

2064
QString TableDesignerWidget::generateDefaultColumnName(QString const & data_source, QString const & computer_name) const {
2,778✔
2065
    QString source_name = data_source;
2,778✔
2066

2067
    // Extract the actual name from prefixed data sources
2068
    if (source_name.startsWith("Events: ")) {
2,778✔
2069
        source_name = source_name.mid(8);
510✔
2070
    } else if (source_name.startsWith("Intervals: ")) {
2,268✔
2071
        source_name = source_name.mid(11);
672✔
2072
    } else if (source_name.startsWith("analog:")) {
1,596✔
2073
        source_name = source_name.mid(7);
1,344✔
2074
    } else if (source_name.startsWith("lines:")) {
252✔
2075
        source_name = source_name.mid(6);
252✔
2076
    } else if (source_name.startsWith("TimeFrame: ")) {
×
2077
        source_name = source_name.mid(11);
×
2078
    }
2079

2080
    // Create a concise name
2081
    return QString("%1_%2").arg(source_name, computer_name);
8,334✔
2082
}
2,778✔
2083

2084
std::string TableDesignerWidget::extractGroupName(QString const & data_source) const {
861✔
2085
    QString source_name = data_source;
861✔
2086

2087
    // Extract the actual name from prefixed data sources
2088
    if (source_name.startsWith("Events: ")) {
861✔
2089
        source_name = source_name.mid(8);
170✔
2090
    } else if (source_name.startsWith("Intervals: ")) {
691✔
2091
        source_name = source_name.mid(11);
96✔
2092
    } else if (source_name.startsWith("analog:")) {
595✔
2093
        source_name = source_name.mid(7);
168✔
2094
    } else if (source_name.startsWith("lines:")) {
427✔
2095
        source_name = source_name.mid(6);
84✔
2096
    } else if (source_name.startsWith("TimeFrame: ")) {
343✔
2097
        source_name = source_name.mid(11);
343✔
2098
    }
2099

2100
    // Use the same grouping pattern as Feature_Tree_Widget
2101
    std::regex const pattern{_grouping_pattern};
861✔
2102
    std::smatch matches{};
861✔
2103
    std::string key = source_name.toStdString();
861✔
2104

2105
    if (std::regex_search(key, matches, pattern) && matches.size() > 1) {
861✔
2106
        return matches[1].str();
×
2107
    }
2108

2109
    return key;// Return the key itself if no match
861✔
2110
}
861✔
2111

2112
void TableDesignerWidget::onGroupModeToggled(bool enabled) {
×
2113
    _group_mode = enabled;
×
2114

2115
    // Update button text to reflect current mode
2116
    if (enabled) {
×
2117
        ui->group_mode_toggle_btn->setText("Group Mode");
×
2118
        ui->computers_info_label->setText("Select computers by checking the boxes. Similar data will be grouped and transformed together.");
×
2119
    } else {
2120
        ui->group_mode_toggle_btn->setText("Individual Mode");
×
2121
        ui->computers_info_label->setText("Select computers by checking the boxes. Each data source will be handled individually.");
×
2122
    }
2123

2124
    // Refresh the tree to apply the new grouping mode
2125
    refreshComputersTree();
×
2126
}
×
2127

2128
QWidget * TableDesignerWidget::createParameterWidget(QString const & computer_name,
254✔
2129
                                                     std::vector<std::unique_ptr<IParameterDescriptor>> const & parameter_descriptors) {
2130
    if (parameter_descriptors.empty()) {
254✔
2131
        return nullptr;
×
2132
    }
2133

2134
    auto * widget = new QWidget();
254✔
2135
    auto * layout = new QHBoxLayout(widget);
254✔
2136
    layout->setContentsMargins(2, 2, 2, 2);
254✔
2137
    layout->setSpacing(4);
254✔
2138

2139
    for (auto const & param_desc: parameter_descriptors) {
508✔
2140
        QString param_name = QString::fromStdString(param_desc->getName());
254✔
2141
        QString param_key = computer_name + "::" + param_name;
254✔
2142

2143
        // Add parameter label
2144
        auto * label = new QLabel(QString::fromStdString(param_desc->getName()) + ":");
254✔
2145
        label->setToolTip(QString::fromStdString(param_desc->getDescription()));
254✔
2146
        layout->addWidget(label);
254✔
2147

2148
        if (param_desc->getUIHint() == "enum") {
254✔
2149
            // Create combo box for enum parameters
2150
            auto * combo = new QComboBox();
170✔
2151
            combo->setObjectName(param_key);// Store parameter key for retrieval
170✔
2152

2153
            auto ui_props = param_desc->getUIProperties();
170✔
2154
            QString options_str = QString::fromStdString(ui_props["options"]);
510✔
2155
            QString default_value = QString::fromStdString(ui_props["default"]);
510✔
2156

2157
            QStringList options = options_str.split(',', Qt::SkipEmptyParts);
170✔
2158
            combo->addItems(options);
170✔
2159

2160
            // Set default value
2161
            int default_index = combo->findText(default_value);
170✔
2162
            if (default_index >= 0) {
170✔
2163
                combo->setCurrentIndex(default_index);
170✔
2164
            }
2165

2166
            combo->setToolTip(QString::fromStdString(param_desc->getDescription()));
170✔
2167
            layout->addWidget(combo);
170✔
2168

2169
            // Store the widget for parameter retrieval
2170
            _parameter_controls[param_key.toStdString()] = combo;
170✔
2171

2172
        } else if (param_desc->getUIHint() == "number") {
254✔
2173
            // Create spin box for numeric parameters
2174
            auto * spinbox = new QSpinBox();
84✔
2175
            spinbox->setObjectName(param_key);
84✔
2176

2177
            auto ui_props = param_desc->getUIProperties();
84✔
2178
            QString default_str = QString::fromStdString(ui_props["default"]);
252✔
2179
            QString min_str = QString::fromStdString(ui_props["min"]);
252✔
2180
            QString max_str = QString::fromStdString(ui_props["max"]);
252✔
2181

2182
            if (!min_str.isEmpty()) spinbox->setMinimum(min_str.toInt());
84✔
2183
            if (!max_str.isEmpty()) spinbox->setMaximum(max_str.toInt());
84✔
2184
            if (!default_str.isEmpty()) spinbox->setValue(default_str.toInt());
84✔
2185

2186
            spinbox->setToolTip(QString::fromStdString(param_desc->getDescription()));
84✔
2187
            layout->addWidget(spinbox);
84✔
2188

2189
            _parameter_controls[param_key.toStdString()] = spinbox;
84✔
2190

2191
        } else {
84✔
2192
            // Default to text input
NEW
2193
            auto * lineedit = new QLineEdit();
×
2194
            lineedit->setObjectName(param_key);
×
2195

2196
            auto ui_props = param_desc->getUIProperties();
×
2197
            QString default_value = QString::fromStdString(ui_props["default"]);
×
2198
            lineedit->setText(default_value);
×
2199

2200
            lineedit->setToolTip(QString::fromStdString(param_desc->getDescription()));
×
2201
            layout->addWidget(lineedit);
×
2202

2203
            _parameter_controls[param_key.toStdString()] = lineedit;
×
2204
        }
×
2205
    }
254✔
2206

2207
    return widget;
254✔
2208
}
2209

2210
std::map<std::string, std::string> TableDesignerWidget::getParameterValues(QString const & computer_name) const {
50✔
2211
    std::map<std::string, std::string> parameters;
50✔
2212

2213
    // Look for parameter controls with this computer name prefix
2214
    QString prefix = computer_name + "::";
50✔
2215

2216
    for (auto const & [key, widget]: _parameter_controls) {
150✔
2217
        QString key_str = QString::fromStdString(key);
100✔
2218
        if (key_str.startsWith(prefix)) {
100✔
2219
            QString param_name = key_str.mid(prefix.length());
6✔
2220

2221
            if (auto * combo = qobject_cast<QComboBox *>(widget)) {
6✔
UNCOV
2222
                parameters[param_name.toStdString()] = combo->currentText().toStdString();
×
2223
            } else if (auto * spinbox = qobject_cast<QSpinBox *>(widget)) {
6✔
2224
                parameters[param_name.toStdString()] = QString::number(spinbox->value()).toStdString();
6✔
NEW
2225
            } else if (auto * lineedit = qobject_cast<QLineEdit *>(widget)) {
×
2226
                parameters[param_name.toStdString()] = lineedit->text().toStdString();
×
2227
            }
2228
        }
6✔
2229
    }
100✔
2230

2231
    return parameters;
50✔
2232
}
50✔
2233

2234
// Explicit template instantiations for formatVectorForCsv
2235
template void TableDesignerWidget::formatVectorForCsv<double>(std::ofstream & file, std::vector<double> const & values, int precision);
2236
template void TableDesignerWidget::formatVectorForCsv<int>(std::ofstream & file, std::vector<int> const & values, int precision);
2237
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