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

paulmthompson / WhiskerToolbox / 18008514883

25 Sep 2025 01:08PM UTC coverage: 68.869% (-0.05%) from 68.919%
18008514883

push

github

paulmthompson
dlc loader for csv files

92 of 102 new or added lines in 3 files covered. (90.2%)

680 existing lines in 10 files now uncovered.

41941 of 60900 relevant lines covered (68.87%)

1139.5 hits per line

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

60.04
/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)
16✔
63
    : QWidget(parent),
64
      ui(new Ui::TableDesignerWidget),
32✔
65
      _data_manager(std::move(data_manager)) {
48✔
66

67
    ui->setupUi(this);
16✔
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;
16✔
73
    _parameter_layout = nullptr;
16✔
74

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

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

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

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

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

96
    // Connect table viewer signals for better integration
97
    connect(_table_viewer, &TableViewerWidget::rowScrolled, this, [this](size_t row_index) {
16✔
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)
UNCOV
101
    });
×
102

103
    connectSignals();
16✔
104
    
105

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

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

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

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

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

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

155
TableDesignerWidget::~TableDesignerWidget() {
16✔
156
    delete ui;
16✔
157
}
16✔
158

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

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

170

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

180
    // Table info signals are connected via TableInfoWidget
181

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

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

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

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

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

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

243
    _current_table_id = table_id;
13✔
244
    loadTableInfo(table_id);
13✔
245

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

255
    updateBuildStatus("Table selected: " + table_id);
13✔
256

257
    qDebug() << "Selected table:" << table_id;
13✔
258
}
29✔
259

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

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

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

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

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

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

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

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

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

321
    // Update the info label
322
    updateRowInfoLabel(selected);
28✔
323

324
    // Update interval settings visibility
325
    updateIntervalSettingsVisibility();
28✔
326

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

330
    qDebug() << "Row data source changed to:" << selected;
28✔
331
    triggerPreviewDebounced();
28✔
332
}
44✔
333

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

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

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

355

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
639
        auto names = view->getColumnNames();
×
640
        if (includeHeader) {
×
UNCOV
641
            for (size_t i = 0; i < names.size(); ++i) {
×
642
                if (i > 0) file << delim;
×
643
                file << names[i];
×
644
            }
645
            file << eol;
×
646
        }
UNCOV
647
        size_t rows = view->getRowCount();
×
UNCOV
648
        for (size_t r = 0; r < rows; ++r) {
×
649
            for (size_t c = 0; c < names.size(); ++c) {
×
650
                if (c > 0) file << delim;
×
651
                bool wrote = false;
×
652
                // Try known scalar types in order
653
                try {
654
                    auto const & vals = view->getColumnValues<double>(names[c].c_str());
×
655
                    if (r < vals.size()) {
×
UNCOV
656
                        file << vals[r];
×
657
                        wrote = true;
×
658
                    }
659
                } catch (...) {}
×
660
                if (!wrote) {
×
661
                    try {
662
                        auto const & vals = view->getColumnValues<int>(names[c].c_str());
×
UNCOV
663
                        if (r < vals.size()) {
×
664
                            file << vals[r];
×
UNCOV
665
                            wrote = true;
×
666
                        }
667
                    } catch (...) {}
×
668
                }
669
                if (!wrote) {
×
670
                    try {
671
                        auto const & vals = view->getColumnValues<int64_t>(names[c].c_str());
×
UNCOV
672
                        if (r < vals.size()) {
×
673
                            file << vals[r];
×
UNCOV
674
                            wrote = true;
×
675
                        }
676
                    } catch (...) {}
×
677
                }
678
                if (!wrote) {
×
679
                    try {
680
                        auto const & vals = view->getColumnValues<bool>(names[c].c_str());
×
UNCOV
681
                        if (r < vals.size()) {
×
682
                            file << (vals[r] ? 1 : 0);
×
UNCOV
683
                            wrote = true;
×
684
                        }
UNCOV
685
                    } catch (...) {}
×
686
                }
687
                if (!wrote) file << "NaN";
×
688
            }
689
            file << eol;
×
690
        }
691
        file.close();
×
UNCOV
692
        updateBuildStatus(QString("Exported CSV: %1").arg(filename));
×
693
    } catch (std::exception const & e) {
×
694
        updateBuildStatus(QString("Export failed: %1").arg(e.what()), true);
×
UNCOV
695
    }
×
UNCOV
696
}
×
697

698
QString TableDesignerWidget::promptSaveCsvFilename() const {
×
699
    return QFileDialog::getSaveFileName(const_cast<TableDesignerWidget *>(this), "Export Table to CSV", QString(), "CSV Files (*.csv)");
×
700
}
701

702
void TableDesignerWidget::onSaveTableInfo() {
×
703
    if (_current_table_id.isEmpty()) {
×
UNCOV
704
        return;
×
705
    }
706

707
    QString name = _table_info_widget ? _table_info_widget->getName() : QString();
×
UNCOV
708
    QString description = _table_info_widget ? _table_info_widget->getDescription() : QString();
×
709

710
    if (name.isEmpty()) {
×
711
        QMessageBox::warning(this, "Error", "Table name cannot be empty");
×
UNCOV
712
        return;
×
713
    }
714

715
    if (auto * reg = _data_manager->getTableRegistry(); reg && reg->updateTableInfo(_current_table_id.toStdString(), name.toStdString(), description.toStdString())) {
×
716
        updateBuildStatus("Table information saved");
×
717
        // Refresh the combo to show updated name
718
        refreshTableCombo();
×
719
        // Restore selection
UNCOV
720
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
UNCOV
721
            if (ui->table_combo->itemData(i).toString() == _current_table_id) {
×
722
                ui->table_combo->setCurrentIndex(i);
×
UNCOV
723
                break;
×
724
            }
725
        }
726
    } else {
UNCOV
727
        QMessageBox::warning(this, "Error", "Failed to save table information");
×
728
    }
UNCOV
729
}
×
730

731
void TableDesignerWidget::onTableManagerTableCreated(QString const & table_id) {
12✔
732
    refreshTableCombo();
12✔
733
    qDebug() << "Table created signal received:" << table_id;
12✔
734
}
12✔
735

UNCOV
736
void TableDesignerWidget::onTableManagerTableRemoved(QString const & table_id) {
×
737
    refreshTableCombo();
×
738
    if (_current_table_id == table_id) {
×
UNCOV
739
        _current_table_id.clear();
×
UNCOV
740
        clearUI();
×
741
    }
UNCOV
742
    qDebug() << "Table removed signal received:" << table_id;
×
UNCOV
743
}
×
744

745
void TableDesignerWidget::onTableManagerTableInfoUpdated(QString const & table_id) {
11✔
746
    if (_current_table_id == table_id && !_loading_column_configuration) {
11✔
747
        loadTableInfo(table_id);
11✔
748
    }
749
    qDebug() << "Table info updated signal received:" << table_id;
11✔
750
}
11✔
751

752
void TableDesignerWidget::refreshTableCombo() {
28✔
753
    ui->table_combo->clear();
28✔
754

755
    auto * reg = _data_manager->getTableRegistry();
28✔
756
    auto table_infos = reg ? reg->getAllTableInfo() : std::vector<TableInfo>{};
28✔
757
    for (auto const & info: table_infos) {
43✔
758
        ui->table_combo->addItem(QString::fromStdString(info.name), QString::fromStdString(info.id));
15✔
759
    }
760

761
    if (ui->table_combo->count() == 0) {
28✔
762
        ui->table_combo->addItem("(No tables available)", "");
16✔
763
    }
764
}
56✔
765

766
void TableDesignerWidget::refreshRowDataSourceCombo() {
20✔
767
    ui->row_data_source_combo->clear();
20✔
768

769
    if (!_data_manager) {
20✔
UNCOV
770
        qDebug() << "refreshRowDataSourceCombo: No table manager";
×
UNCOV
771
        return;
×
772
    }
773

774
    auto data_sources = getAvailableDataSources();
20✔
775
    qDebug() << "refreshRowDataSourceCombo: Found" << data_sources.size() << "data sources:" << data_sources;
20✔
776

777
    for (QString const & source: data_sources) {
206✔
778
        // Only include valid row sources in this combo
779
        if (source.startsWith("TimeFrame: ") || source.startsWith("Events: ") || source.startsWith("Intervals: ")) {
186✔
780
            ui->row_data_source_combo->addItem(source);
146✔
781
        }
782
    }
783

784
    if (ui->row_data_source_combo->count() == 0) {
20✔
UNCOV
785
        ui->row_data_source_combo->addItem("(No data sources available)");
×
UNCOV
786
        qDebug() << "refreshRowDataSourceCombo: No data sources available";
×
787
    }
788
}
20✔
789

790

791
void TableDesignerWidget::loadTableInfo(QString const & table_id) {
24✔
792
    if (table_id.isEmpty() || !_data_manager) {
24✔
UNCOV
793
        clearUI();
×
UNCOV
794
        return;
×
795
    }
796

797
    auto * reg = _data_manager->getTableRegistry();
24✔
798
    auto info = reg ? reg->getTableInfo(table_id.toStdString()) : TableInfo{};
24✔
799
    if (info.id.empty()) {
24✔
UNCOV
800
        clearUI();
×
UNCOV
801
        return;
×
802
    }
803

804
    // Load table information
805
    if (_table_info_widget) {
24✔
806
        _table_info_widget->setName(QString::fromStdString(info.name));
24✔
807
        _table_info_widget->setDescription(QString::fromStdString(info.description));
24✔
808
    }
809

810
    // Load row source if available
811
    if (!info.rowSourceName.empty()) {
24✔
812
        int row_index = ui->row_data_source_combo->findText(QString::fromStdString(info.rowSourceName));
12✔
813
        if (row_index >= 0) {
12✔
814
            // Block signals to prevent circular dependency when loading table info
815
            ui->row_data_source_combo->blockSignals(true);
12✔
816
            ui->row_data_source_combo->setCurrentIndex(row_index);
12✔
817
            ui->row_data_source_combo->blockSignals(false);
12✔
818

819
            // Manually update the info label without triggering the signal handler
820
            updateRowInfoLabel(QString::fromStdString(info.rowSourceName));
12✔
821

822
            // Update interval settings visibility
823
            updateIntervalSettingsVisibility();
12✔
824

825
            // Since signals were blocked, this will ensure the tree is refreshed
826
            // when the computers tree is populated later in this function
827
        }
828
    }
829

830
    // Clear old column list (deprecated)
831
    // The computers tree will be populated based on available data sources
832
    refreshComputersTree();
24✔
833

834
    updateBuildStatus(QString("Loaded table: %1").arg(QString::fromStdString(info.name)));
24✔
835
    triggerPreviewDebounced();
24✔
836
}
24✔
837

838
void TableDesignerWidget::clearUI() {
44✔
839
    _current_table_id.clear();
44✔
840

841
    // Clear table info
842
    if (_table_info_widget) {
44✔
843
        _table_info_widget->setName("");
44✔
844
        _table_info_widget->setDescription("");
44✔
845
    }
846

847
    // Clear row source
848
    ui->row_data_source_combo->setCurrentIndex(-1);
44✔
849
    ui->row_info_label->setText("No row source selected");
44✔
850

851
    // Reset capture range and interval settings
852
    setCaptureRange(30000);// Default value
44✔
853
    if (ui->interval_beginning_radio) {
44✔
854
        ui->interval_beginning_radio->setChecked(true);
44✔
855
    }
856
    if (ui->interval_itself_radio) {
44✔
857
        ui->interval_itself_radio->setChecked(false);
44✔
858
    }
859
    if (ui->interval_settings_group) {
44✔
860
        ui->interval_settings_group->setVisible(false);
44✔
861
    }
862

863
    // Clear computers tree
864
    if (ui->computers_tree) {
44✔
865
        ui->computers_tree->clear();
44✔
866
    }
867

868
    // Disable controls
869
    ui->delete_table_btn->setEnabled(false);
44✔
870
    // Table info section is controlled separately
871
    ui->build_table_btn->setEnabled(false);
44✔
872
    if (auto gb = this->findChild<QGroupBox *>("row_source_group")) gb->setEnabled(false);
88✔
873
    if (auto gb = this->findChild<QGroupBox *>("column_design_group")) gb->setEnabled(false);
88✔
874
    if (_table_info_section) _table_info_section->setEnabled(false);
44✔
875

876
    updateBuildStatus("No table selected");
44✔
877
    if (_table_viewer) _table_viewer->clearTable();
44✔
878
}
44✔
879

880
void TableDesignerWidget::updateBuildStatus(QString const & message, bool is_error) {
84✔
881
    ui->build_status_label->setText(message);
84✔
882

883
    if (is_error) {
84✔
UNCOV
884
        ui->build_status_label->setStyleSheet("QLabel { color: red; font-weight: bold; }");
×
885
    } else {
886
        ui->build_status_label->setStyleSheet("QLabel { color: green; }");
84✔
887
    }
888
}
84✔
889

890
QStringList TableDesignerWidget::getAvailableDataSources() const {
96✔
891
    QStringList sources;
96✔
892

893
    if (!_data_manager) {
96✔
UNCOV
894
        qDebug() << "getAvailableDataSources: No table manager";
×
UNCOV
895
        return sources;
×
896
    }
897

898
    auto * reg = _data_manager->getTableRegistry();
96✔
899
    auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
96✔
900
    if (!data_manager_extension) {
96✔
901
        qDebug() << "getAvailableDataSources: No data manager extension";
×
902
        return sources;
×
903
    }
904

905
    if (!_data_manager) {
96✔
UNCOV
906
        qDebug() << "getAvailableDataSources: No data manager";
×
UNCOV
907
        return sources;
×
908
    }
909

910
    // Add TimeFrame keys as potential row sources
911
    // TimeFrames can define intervals for analysis
912
    auto timeframe_keys = _data_manager->getTimeFrameKeys();
96✔
913
    qDebug() << "getAvailableDataSources: TimeFrame keys:" << timeframe_keys.size();
96✔
914
    for (auto const & key: timeframe_keys) {
489✔
915
        QString source = QString("TimeFrame: %1").arg(QString::fromStdString(key.str()));
393✔
916
        sources << source;
393✔
917
        qDebug() << "  Added TimeFrame:" << source;
393✔
918
    }
393✔
919

920
    // Add DigitalEventSeries keys as potential row sources
921
    // Events can be used to define analysis windows or timestamps
922
    auto event_keys = _data_manager->getKeys<DigitalEventSeries>();
96✔
923
    qDebug() << "getAvailableDataSources: Event keys:" << event_keys.size();
96✔
924
    for (auto const & key: event_keys) {
291✔
925
        QString source = QString("Events: %1").arg(QString::fromStdString(key));
195✔
926
        sources << source;
195✔
927
        qDebug() << "  Added Events:" << source;
195✔
928
    }
195✔
929

930
    // Add DigitalIntervalSeries keys as potential row sources
931
    // Intervals directly define analysis windows
932
    auto interval_keys = _data_manager->getKeys<DigitalIntervalSeries>();
96✔
933
    qDebug() << "getAvailableDataSources: Interval keys:" << interval_keys.size();
96✔
934
    for (auto const & key: interval_keys) {
207✔
935
        QString source = QString("Intervals: %1").arg(QString::fromStdString(key));
111✔
936
        sources << source;
111✔
937
        qDebug() << "  Added Intervals:" << source;
111✔
938
    }
111✔
939

940
    // Add AnalogTimeSeries keys as data sources (for computers; not row selectors)
941
    auto analog_keys = _data_manager->getKeys<AnalogTimeSeries>();
96✔
942
    qDebug() << "getAvailableDataSources: Analog keys:" << analog_keys.size();
96✔
943
    for (auto const & key: analog_keys) {
288✔
944
        QString source = QString("analog:%1").arg(QString::fromStdString(key));
192✔
945
        sources << source;
192✔
946
        qDebug() << "  Added Analog:" << source;
192✔
947
    }
192✔
948

949
    qDebug() << "getAvailableDataSources: Total sources found:" << sources.size();
96✔
950

951
    return sources;
96✔
952
}
96✔
953

954
std::pair<std::optional<DataSourceVariant>, RowSelectorType>
955
TableDesignerWidget::createDataSourceVariant(QString const & data_source_string,
705✔
956
                                             std::shared_ptr<DataManagerExtension> data_manager_extension) const {
957
    std::optional<DataSourceVariant> result;
705✔
958
    RowSelectorType row_selector_type = RowSelectorType::IntervalBased;
705✔
959

960
    if (data_source_string.startsWith("TimeFrame: ")) {
705✔
961
        // TimeFrames are used with TimestampSelector for rows; no concrete data source needed
962
        row_selector_type = RowSelectorType::Timestamp;
311✔
963

964
    } else if (data_source_string.startsWith("Events: ")) {
394✔
965
        QString source_name = data_source_string.mid(8);// Remove "Events: " prefix
154✔
966
        // Event-based computers in the registry operate with interval rows
967
        row_selector_type = RowSelectorType::IntervalBased;
154✔
968

969
        if (auto event_source = data_manager_extension->getEventSource(source_name.toStdString())) {
154✔
970
            result = event_source;
154✔
971
        }
154✔
972

973
    } else if (data_source_string.startsWith("Intervals: ")) {
394✔
974
        QString source_name = data_source_string.mid(11);// Remove "Intervals: " prefix
88✔
975
        row_selector_type = RowSelectorType::IntervalBased;
88✔
976

977
        if (auto interval_source = data_manager_extension->getIntervalSource(source_name.toStdString())) {
88✔
978
            result = interval_source;
88✔
979
        }
88✔
980
    } else if (data_source_string.startsWith("analog:")) {
240✔
981
        QString source_name = data_source_string.mid(7);// Remove "analog:" prefix
152✔
982
        row_selector_type = RowSelectorType::IntervalBased;
152✔
983

984
        if (auto analog_source = data_manager_extension->getAnalogSource(source_name.toStdString())) {
152✔
985
            result = analog_source;
152✔
986
        }
152✔
987
    }
152✔
988

989
    return {result, row_selector_type};
1,410✔
990
}
705✔
991

992
void TableDesignerWidget::updateRowInfoLabel(QString const & selected_source) {
42✔
993
    if (selected_source.isEmpty()) {
42✔
UNCOV
994
        ui->row_info_label->setText("No row source selected");
×
UNCOV
995
        return;
×
996
    }
997

998
    // Parse the selected source to get type and name
999
    QString source_type;
42✔
1000
    QString source_name;
42✔
1001

1002
    if (selected_source.startsWith("TimeFrame: ")) {
42✔
1003
        source_type = "TimeFrame";
20✔
1004
        source_name = selected_source.mid(11);// Remove "TimeFrame: " prefix
20✔
1005
    } else if (selected_source.startsWith("Events: ")) {
22✔
UNCOV
1006
        source_type = "Events";
×
UNCOV
1007
        source_name = selected_source.mid(8);// Remove "Events: " prefix
×
1008
    } else if (selected_source.startsWith("Intervals: ")) {
22✔
1009
        source_type = "Intervals";
22✔
1010
        source_name = selected_source.mid(11);// Remove "Intervals: " prefix
22✔
1011
    }
1012

1013
    // Get additional information about the selected source
1014
    QString info_text = QString("Selected: %1 (%2)").arg(source_name, source_type);
42✔
1015

1016
    if (!_data_manager) {
42✔
UNCOV
1017
        ui->row_info_label->setText(info_text);
×
UNCOV
1018
        return;
×
1019
    }
1020

1021
    auto * reg3 = _data_manager->getTableRegistry();
42✔
1022
    auto data_manager_extension = reg3 ? reg3->getDataManagerExtension() : nullptr;
42✔
1023
    if (!data_manager_extension) {
42✔
UNCOV
1024
        ui->row_info_label->setText(info_text);
×
UNCOV
1025
        return;
×
1026
    }
1027

1028
    auto const source_name_str = source_name.toStdString();
42✔
1029

1030
    // Add specific information based on source type
1031
    if (source_type == "TimeFrame") {
42✔
1032
        auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
20✔
1033
        if (timeframe) {
20✔
1034
            info_text += QString(" - %1 time points").arg(timeframe->getTotalFrameCount());
20✔
1035
        }
1036
    } else if (source_type == "Events") {
42✔
UNCOV
1037
        auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
UNCOV
1038
        if (event_series) {
×
UNCOV
1039
            auto events = event_series->getEventSeries();
×
UNCOV
1040
            info_text += QString(" - %1 events").arg(events.size());
×
UNCOV
1041
        }
×
1042
    } else if (source_type == "Intervals") {
22✔
1043
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
22✔
1044
        if (interval_series) {
22✔
1045
            auto intervals = interval_series->getDigitalIntervalSeries();
22✔
1046
            info_text += QString(" - %1 intervals").arg(intervals.size());
22✔
1047

1048
            // Add capture range and interval setting information
1049
            if (isIntervalItselfSelected()) {
22✔
1050
                info_text += QString("\nUsing intervals as-is (no capture range)");
2✔
1051
            } else {
1052
                int capture_range = getCaptureRange();
20✔
1053
                QString interval_point = isIntervalBeginningSelected() ? "beginning" : "end";
20✔
1054
                info_text += QString("\nCapture range: ±%1 samples around %2 of intervals").arg(capture_range).arg(interval_point);
20✔
1055
            }
20✔
1056
        }
22✔
1057
    }
22✔
1058

1059
    ui->row_info_label->setText(info_text);
42✔
1060
}
42✔
1061

1062
std::unique_ptr<IRowSelector> TableDesignerWidget::createRowSelector(QString const & row_source) {
20✔
1063
    // Parse the row source to get type and name
1064
    QString source_type;
20✔
1065
    QString source_name;
20✔
1066

1067
    if (row_source.startsWith("TimeFrame: ")) {
20✔
UNCOV
1068
        source_type = "TimeFrame";
×
UNCOV
1069
        source_name = row_source.mid(11);// Remove "TimeFrame: " prefix
×
1070
    } else if (row_source.startsWith("Events: ")) {
20✔
UNCOV
1071
        source_type = "Events";
×
1072
        source_name = row_source.mid(8);// Remove "Events: " prefix
×
1073
    } else if (row_source.startsWith("Intervals: ")) {
20✔
1074
        source_type = "Intervals";
20✔
1075
        source_name = row_source.mid(11);// Remove "Intervals: " prefix
20✔
1076
    } else {
UNCOV
1077
        qDebug() << "Unknown row source format:" << row_source;
×
UNCOV
1078
        return nullptr;
×
1079
    }
1080

1081
    auto const source_name_str = source_name.toStdString();
20✔
1082

1083
    try {
1084
        if (source_type == "TimeFrame") {
20✔
1085
            // Create IntervalSelector using TimeFrame
UNCOV
1086
            auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
×
UNCOV
1087
            if (!timeframe) {
×
1088
                qDebug() << "TimeFrame not found:" << source_name;
×
1089
                return nullptr;
×
1090
            }
1091

1092
            // Use timestamps to select all rows
UNCOV
1093
            std::vector<TimeFrameIndex> timestamps;
×
UNCOV
1094
            for (int64_t i = 0; i < timeframe->getTotalFrameCount(); ++i) {
×
UNCOV
1095
                timestamps.push_back(TimeFrameIndex(i));
×
1096
            }
1097
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe);
×
1098

1099
        } else if (source_type == "Events") {
20✔
1100
            // Create TimestampSelector using DigitalEventSeries
UNCOV
1101
            auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
1102
            if (!event_series) {
×
1103
                qDebug() << "DigitalEventSeries not found:" << source_name;
×
1104
                return nullptr;
×
1105
            }
1106

1107
            auto events = event_series->getEventSeries();
×
UNCOV
1108
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
×
UNCOV
1109
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
×
UNCOV
1110
            if (!timeframe_obj) {
×
1111
                qDebug() << "TimeFrame not found for events:" << timeframe_key.str();
×
1112
                return nullptr;
×
1113
            }
1114

1115
            // Convert events to TimeFrameIndex
1116
            std::vector<TimeFrameIndex> timestamps;
×
UNCOV
1117
            for (auto const & event: events) {
×
UNCOV
1118
                timestamps.push_back(TimeFrameIndex(static_cast<int64_t>(event)));
×
1119
            }
1120

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

1123
        } else if (source_type == "Intervals") {
20✔
1124
            // Create IntervalSelector using DigitalIntervalSeries with capture range
1125
            auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
20✔
1126
            if (!interval_series) {
20✔
UNCOV
1127
                qDebug() << "DigitalIntervalSeries not found:" << source_name;
×
UNCOV
1128
                return nullptr;
×
1129
            }
1130

1131
            auto intervals = interval_series->getDigitalIntervalSeries();
20✔
1132
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
20✔
1133
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
20✔
1134
            if (!timeframe_obj) {
20✔
UNCOV
1135
                qDebug() << "TimeFrame not found for intervals:" << timeframe_key.str();
×
UNCOV
1136
                return nullptr;
×
1137
            }
1138

1139
            // Get capture range and interval setting
1140
            int capture_range = getCaptureRange();
20✔
1141
            bool use_beginning = isIntervalBeginningSelected();
20✔
1142
            bool use_interval_itself = isIntervalItselfSelected();
20✔
1143

1144
            // Create intervals based on the selected option
1145
            std::vector<TimeFrameInterval> tf_intervals;
20✔
1146
            for (auto const & interval: intervals) {
99✔
1147
                if (use_interval_itself) {
79✔
1148
                    // Use the interval as-is
1149
                    tf_intervals.emplace_back(TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
3✔
1150
                } else {
1151
                    // Determine the reference point (beginning or end of interval)
1152
                    int64_t reference_point;
1153
                    if (use_beginning) {
76✔
1154
                        reference_point = interval.start;
76✔
1155
                    } else {
UNCOV
1156
                        reference_point = interval.end;
×
1157
                    }
1158

1159
                    // Create a new interval around the reference point
1160
                    int64_t start_point = reference_point - capture_range;
76✔
1161
                    int64_t end_point = reference_point + capture_range;
76✔
1162

1163
                    // Ensure bounds are within the timeframe
1164
                    start_point = std::max(start_point, int64_t(0));
76✔
1165
                    end_point = std::min(end_point, static_cast<int64_t>(timeframe_obj->getTotalFrameCount() - 1));
76✔
1166

1167
                    tf_intervals.emplace_back(TimeFrameIndex(start_point), TimeFrameIndex(end_point));
76✔
1168
                }
1169
            }
1170

1171
            return std::make_unique<IntervalSelector>(std::move(tf_intervals), timeframe_obj);
20✔
1172
        }
20✔
1173

1174
    } catch (std::exception const & e) {
×
1175
        qDebug() << "Exception creating row selector:" << e.what();
×
UNCOV
1176
        return nullptr;
×
UNCOV
1177
    }
×
1178

UNCOV
1179
    qDebug() << "Unsupported row source type:" << source_type;
×
1180
    return nullptr;
×
1181
}
20✔
1182

1183
bool TableDesignerWidget::addColumnToBuilder(TableViewBuilder & builder, ColumnInfo const & column_info) {
×
1184
    // Use the simplified TableRegistry method that handles all the type checking internally
UNCOV
1185
    auto * reg = _data_manager->getTableRegistry();
×
1186
    if (!reg) {
×
1187
        qDebug() << "TableRegistry not available";
×
1188
        return false;
×
1189
    }
1190

1191
    bool success = reg->addColumnToBuilder(builder, column_info);
×
UNCOV
1192
    if (!success) {
×
UNCOV
1193
        qDebug() << "Failed to add column to builder:" << QString::fromStdString(column_info.name);
×
1194
    }
1195

1196
    return success;
×
1197
}
1198

1199
void TableDesignerWidget::updateIntervalSettingsVisibility() {
42✔
1200
    if (!ui->interval_settings_group) {
42✔
1201
        return;
×
1202
    }
1203

1204
    QString selected_key = ui->row_data_source_combo->currentText();
42✔
1205
    if (selected_key.isEmpty()) {
42✔
UNCOV
1206
        ui->interval_settings_group->setVisible(false);
×
UNCOV
1207
        if (ui->capture_range_spinbox) {
×
UNCOV
1208
            ui->capture_range_spinbox->setEnabled(false);
×
1209
        }
1210
        return;
×
1211
    }
1212

1213
    if (!_data_manager) {
42✔
UNCOV
1214
        ui->interval_settings_group->setVisible(false);
×
UNCOV
1215
        if (ui->capture_range_spinbox) {
×
UNCOV
1216
            ui->capture_range_spinbox->setEnabled(false);
×
1217
        }
UNCOV
1218
        return;
×
1219
    }
1220

1221
    // Check if the selected source is an interval series
1222
    if (selected_key.startsWith("Intervals: ")) {
42✔
1223
        ui->interval_settings_group->setVisible(true);
22✔
1224

1225
        // Enable/disable capture range based on interval setting
1226
        if (ui->capture_range_spinbox) {
22✔
1227
            bool use_interval_itself = isIntervalItselfSelected();
22✔
1228
            ui->capture_range_spinbox->setEnabled(!use_interval_itself);
22✔
1229
        }
1230
    } else {
1231
        ui->interval_settings_group->setVisible(false);
20✔
1232
        if (ui->capture_range_spinbox) {
20✔
1233
            ui->capture_range_spinbox->setEnabled(false);
20✔
1234
        }
1235
    }
1236
}
42✔
1237

1238
int TableDesignerWidget::getCaptureRange() const {
40✔
1239
    if (ui->capture_range_spinbox) {
40✔
1240
        return ui->capture_range_spinbox->value();
40✔
1241
    }
UNCOV
1242
    return 30000;// Default value
×
1243
}
1244

1245
void TableDesignerWidget::setCaptureRange(int value) {
44✔
1246
    if (ui->capture_range_spinbox) {
44✔
1247
        ui->capture_range_spinbox->blockSignals(true);
44✔
1248
        ui->capture_range_spinbox->setValue(value);
44✔
1249
        ui->capture_range_spinbox->blockSignals(false);
44✔
1250
    }
1251
}
44✔
1252

1253
bool TableDesignerWidget::isIntervalBeginningSelected() const {
40✔
1254
    if (ui->interval_beginning_radio) {
40✔
1255
        return ui->interval_beginning_radio->isChecked();
40✔
1256
    }
UNCOV
1257
    return true;// Default to beginning
×
1258
}
1259

1260
bool TableDesignerWidget::isIntervalItselfSelected() const {
64✔
1261
    if (ui->interval_itself_radio) {
64✔
1262
        return ui->interval_itself_radio->isChecked();
64✔
1263
    }
UNCOV
1264
    return false;// Default to not selected
×
1265
}
1266

1267
void TableDesignerWidget::triggerPreviewDebounced() {
148✔
1268
    if (_preview_debounce_timer) _preview_debounce_timer->start();
148✔
1269
    // Also trigger an immediate rebuild to support non-interactive/test contexts
1270
    rebuildPreviewNow();
148✔
1271
}
148✔
1272

1273
void TableDesignerWidget::rebuildPreviewNow() {
148✔
1274
    if (!_data_manager || !_table_viewer) return;
148✔
1275
    if (_current_table_id.isEmpty()) {
148✔
1276
        _table_viewer->clearTable();
67✔
1277
        return;
67✔
1278
    }
1279

1280
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
81✔
1281
    if (row_source.isEmpty()) {
81✔
1282
        _table_viewer->clearTable();
22✔
1283
        return;
22✔
1284
    }
1285

1286
    // Get enabled column infos from the computers tree
1287
    auto column_infos = getEnabledColumnInfos();
59✔
1288
    if (column_infos.empty()) {
59✔
1289
        _table_viewer->clearTable();
42✔
1290
        return;
42✔
1291
    }
1292

1293
    // Create row selector for the entire dataset
1294
    auto selector = createRowSelector(row_source);
17✔
1295
    if (!selector) {
17✔
UNCOV
1296
        _table_viewer->clearTable();
×
UNCOV
1297
        return;
×
1298
    }
1299

1300
    // Apply any saved column order for this table id
1301
    auto desiredOrder = _table_column_order.value(_current_table_id);
17✔
1302
    if (!desiredOrder.isEmpty()) {
17✔
1303
        std::vector<ColumnInfo> reordered;
10✔
1304
        reordered.reserve(column_infos.size());
10✔
1305
        for (auto const & name: desiredOrder) {
25✔
1306
            auto it = std::find_if(column_infos.begin(), column_infos.end(), [&](ColumnInfo const & ci) { return QString::fromStdString(ci.name) == name; });
37✔
1307
            if (it != column_infos.end()) {
15✔
1308
                reordered.push_back(*it);
15✔
1309
            }
1310
        }
1311
        for (auto const & ci: column_infos) {
29✔
1312
            if (std::find_if(reordered.begin(), reordered.end(), [&](ColumnInfo const & x) { return x.name == ci.name; }) == reordered.end()) {
46✔
1313
                reordered.push_back(ci);
4✔
1314
            }
1315
        }
1316
        column_infos = std::move(reordered);
10✔
1317
    }
10✔
1318

1319
    // Set up the table viewer with pagination
1320
    _table_viewer->setTableConfiguration(
85✔
1321
            std::move(selector),
17✔
1322
            std::move(column_infos),
17✔
1323
            _data_manager,
17✔
1324
            QString("Preview: %1").arg(_current_table_id));
34✔
1325

1326
    // Capture the current visual order from the viewer
1327
    QStringList currentOrder;
17✔
1328
    if (_table_viewer) {
17✔
1329
        auto * tv = _table_viewer->findChild<QTableView *>();
17✔
1330
        if (tv && tv->model()) {
17✔
1331
            auto * header = tv->horizontalHeader();
17✔
1332
            int cols = tv->model()->columnCount();
17✔
1333
            for (int v = 0; header && v < cols; ++v) {
43✔
1334
                int logical = header->logicalIndex(v);
26✔
1335
                auto name = tv->model()->headerData(logical, Qt::Horizontal, Qt::DisplayRole).toString();
26✔
1336
                currentOrder.push_back(name);
26✔
1337
            }
26✔
1338
        }
1339
    }
1340
    if (!currentOrder.isEmpty()) {
17✔
1341
        _table_column_order[_current_table_id] = currentOrder;
17✔
1342
    }
1343
}
123✔
1344

1345
void TableDesignerWidget::refreshComputersTree() {
76✔
1346
    if (!_data_manager) return;
76✔
1347

1348
    _updating_computers_tree = true;
76✔
1349

1350
    // Preserve previous checkbox states and custom column names
1351
    std::map<std::string, std::pair<Qt::CheckState, QString>> previous_states;
76✔
1352
    if (ui->computers_tree && ui->computers_tree->topLevelItemCount() > 0) {
76✔
1353
        for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
495✔
1354
            auto * top_level_item = ui->computers_tree->topLevelItem(i);
447✔
1355
            // Handle both grouped and individual modes
1356
            for (int j = 0; j < top_level_item->childCount(); ++j) {
1,901✔
1357
                auto * child_item = top_level_item->child(j);
1,454✔
1358
                if (child_item->childCount() > 0) {
1,454✔
1359
                    // This is a computer item under a group/data source
UNCOV
1360
                    for (int k = 0; k < child_item->childCount(); ++k) {
×
UNCOV
1361
                        auto * computer_item = child_item->child(k);
×
UNCOV
1362
                        QString ds = computer_item->data(0, Qt::UserRole).toString();
×
UNCOV
1363
                        QString cn = computer_item->data(1, Qt::UserRole).toString();
×
UNCOV
1364
                        std::string key = (ds + "||" + cn).toStdString();
×
1365
                        previous_states[key] = {computer_item->checkState(1), computer_item->text(2)};
×
1366
                    }
×
1367
                } else {
1368
                    // This is a computer item directly under data source (individual mode)
1369
                    QString ds = child_item->data(0, Qt::UserRole).toString();
1,454✔
1370
                    QString cn = child_item->data(1, Qt::UserRole).toString();
1,454✔
1371
                    std::string key = (ds + "||" + cn).toStdString();
1,454✔
1372
                    previous_states[key] = {child_item->checkState(1), child_item->text(2)};
1,454✔
1373
                }
1,454✔
1374
            }
1375
        }
1376
    }
1377

1378
    ui->computers_tree->clear();
76✔
1379
    ui->computers_tree->setHeaderLabels({"Data Source / Computer", "Enabled", "Column Name", "Parameters"});
456✔
1380
    
1381
    // Clean up parameter widgets
1382
    _computer_parameter_widgets.clear();
76✔
1383
    _parameter_controls.clear();
76✔
1384

1385
    auto * registry = _data_manager->getTableRegistry();
76✔
1386
    if (!registry) {
76✔
UNCOV
1387
        _updating_computers_tree = false;
×
UNCOV
1388
        return;
×
1389
    }
1390

1391
    auto data_manager_extension = registry->getDataManagerExtension();
76✔
1392
    if (!data_manager_extension) {
76✔
UNCOV
1393
        _updating_computers_tree = false;
×
UNCOV
1394
        return;
×
1395
    }
1396

1397
    auto & computer_registry = registry->getComputerRegistry();
76✔
1398

1399
    // Get available data sources
1400
    auto data_sources = getAvailableDataSources();
76✔
1401

1402
    if (_group_mode) {
76✔
1403
        // Group similar data sources together
1404
        std::map<std::string, QStringList> groups;
76✔
1405
        
1406
        // First pass: group data sources by their extracted group name
1407
        for (QString const & data_source : data_sources) {
781✔
1408
            std::string group_name = extractGroupName(data_source);
705✔
1409
            groups[group_name].append(data_source);
705✔
1410
        }
705✔
1411
        
1412
        // Second pass: create tree structure
1413
        for (auto const & [group_name, group_members] : groups) {
781✔
1414
            if (group_members.size() > 1) {
705✔
1415
                // Create group item
UNCOV
1416
                auto * group_item = new QTreeWidgetItem(ui->computers_tree);
×
UNCOV
1417
                group_item->setText(0, QString::fromStdString(group_name) + " (Group)");
×
UNCOV
1418
                group_item->setFlags(Qt::ItemIsEnabled);
×
UNCOV
1419
                group_item->setExpanded(false); // Start collapsed
×
1420
                
1421
                // Get computers available for this group (use first member to determine available computers)
UNCOV
1422
                auto [first_variant, row_selector_type] = createDataSourceVariant(group_members.first(), data_manager_extension);
×
UNCOV
1423
                if (!first_variant.has_value()) {
×
UNCOV
1424
                    continue;
×
1425
                }
1426
                
UNCOV
1427
                auto available_computers = computer_registry.getAvailableComputers(row_selector_type, first_variant.value());
×
1428
                
1429
                // Add computers as children of the group
UNCOV
1430
                for (auto const & computer_info : available_computers) {
×
UNCOV
1431
                    auto * computer_item = new QTreeWidgetItem(group_item);
×
UNCOV
1432
                    computer_item->setText(0, QString::fromStdString(computer_info.name));
×
UNCOV
1433
                    computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
×
UNCOV
1434
                    computer_item->setCheckState(1, Qt::Unchecked);
×
1435
                    
1436
                    // Generate default column name using group name
UNCOV
1437
                    QString default_name = QString("%1_%2").arg(QString::fromStdString(group_name), QString::fromStdString(computer_info.name));
×
UNCOV
1438
                    computer_item->setText(2, default_name);
×
UNCOV
1439
                    computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
×
1440
                    
1441
                    // Store group members and computer name for later use
UNCOV
1442
                    computer_item->setData(0, Qt::UserRole, group_members.join("||")); // Store all group members
×
1443
                    computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
×
1444
                    computer_item->setData(2, Qt::UserRole, true); // Mark as group computer
×
1445
                    
1446
                    // Create parameter widget if computer has parameters
UNCOV
1447
                    if (!computer_info.parameterDescriptors.empty()) {
×
UNCOV
1448
                        auto* param_widget = createParameterWidget(QString::fromStdString(computer_info.name), 
×
UNCOV
1449
                                                                   computer_info.parameterDescriptors);
×
1450
                        if (param_widget) {
×
1451
                            ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
×
UNCOV
1452
                            _computer_parameter_widgets[computer_item] = param_widget;
×
1453
                        }
1454
                    }
1455
                    
1456
                    // Restore previous state if present (use first member for key)
UNCOV
1457
                    std::string prev_key = (group_members.first() + "||" + QString::fromStdString(computer_info.name)).toStdString();
×
UNCOV
1458
                    auto it_prev = previous_states.find(prev_key);
×
UNCOV
1459
                    if (it_prev != previous_states.end()) {
×
UNCOV
1460
                        computer_item->setCheckState(1, it_prev->second.first);
×
UNCOV
1461
                        if (!it_prev->second.second.isEmpty()) {
×
UNCOV
1462
                            computer_item->setText(2, it_prev->second.second);
×
1463
                        }
1464
                    }
1465
                }
×
1466
            } else {
×
1467
                // Single item - create as individual data source
1468
                QString const & data_source = group_members.first();
705✔
1469
                auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
705✔
1470
                data_source_item->setText(0, data_source);
705✔
1471
                data_source_item->setFlags(Qt::ItemIsEnabled);
705✔
1472
                data_source_item->setExpanded(false);
705✔
1473
                
1474
                auto [data_source_variant, row_selector_type] = createDataSourceVariant(data_source, data_manager_extension);
705✔
1475
                if (!data_source_variant.has_value()) {
705✔
1476
                    continue;
311✔
1477
                }
1478
                
1479
                auto available_computers = computer_registry.getAvailableComputers(row_selector_type, data_source_variant.value());
394✔
1480
                
1481
                for (auto const & computer_info : available_computers) {
2,688✔
1482
                    auto * computer_item = new QTreeWidgetItem(data_source_item);
2,294✔
1483
                    computer_item->setText(0, QString::fromStdString(computer_info.name));
2,294✔
1484
                    computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
2,294✔
1485
                    computer_item->setCheckState(1, Qt::Unchecked);
2,294✔
1486
                    
1487
                    QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
2,294✔
1488
                    computer_item->setText(2, default_name);
2,294✔
1489
                    computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
2,294✔
1490
                    
1491
                    computer_item->setData(0, Qt::UserRole, data_source);
2,294✔
1492
                    computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
2,294✔
1493
                    computer_item->setData(2, Qt::UserRole, false); // Mark as individual computer
2,294✔
1494
                    
1495
                    // Create parameter widget if computer has parameters
1496
                    if (!computer_info.parameterDescriptors.empty()) {
2,294✔
1497
                        auto* param_widget = createParameterWidget(QString::fromStdString(computer_info.name), 
154✔
1498
                                                                   computer_info.parameterDescriptors);
154✔
1499
                        if (param_widget) {
154✔
1500
                            ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
154✔
1501
                            _computer_parameter_widgets[computer_item] = param_widget;
154✔
1502
                        }
1503
                    }
1504
                    
1505
                    std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
2,294✔
1506
                    auto it_prev = previous_states.find(prev_key);
2,294✔
1507
                    if (it_prev != previous_states.end()) {
2,294✔
1508
                        computer_item->setCheckState(1, it_prev->second.first);
1,451✔
1509
                        if (!it_prev->second.second.isEmpty()) {
1,451✔
1510
                            computer_item->setText(2, it_prev->second.second);
1,451✔
1511
                        }
1512
                    }
1513
                }
2,294✔
1514
            }
705✔
1515
        }
1516
    } else {
76✔
1517
        // Individual mode - create tree structure: Data Source -> Computers (original behavior)
UNCOV
1518
        for (QString const & data_source: data_sources) {
×
UNCOV
1519
            auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
×
UNCOV
1520
            data_source_item->setText(0, data_source);
×
UNCOV
1521
            data_source_item->setFlags(Qt::ItemIsEnabled);
×
UNCOV
1522
            data_source_item->setExpanded(false);// Start collapsed
×
1523

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

1527
            if (!data_source_variant.has_value()) {
×
1528
                qDebug() << "Failed to create data source variant for:" << data_source;
×
1529
                continue;
×
1530
            }
1531

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

1535
            // Add compatible computers as children
UNCOV
1536
            for (auto const & computer_info: available_computers) {
×
UNCOV
1537
                auto * computer_item = new QTreeWidgetItem(data_source_item);
×
UNCOV
1538
                computer_item->setText(0, QString::fromStdString(computer_info.name));
×
UNCOV
1539
                computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
×
UNCOV
1540
                computer_item->setCheckState(1, Qt::Unchecked);
×
1541

1542
                // Generate default column name
UNCOV
1543
                QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
×
UNCOV
1544
                computer_item->setText(2, default_name);
×
UNCOV
1545
                computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
×
1546

1547
                // Store data source and computer name for later use
1548
                computer_item->setData(0, Qt::UserRole, data_source);
×
UNCOV
1549
                computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
×
UNCOV
1550
                computer_item->setData(2, Qt::UserRole, false); // Mark as individual computer
×
1551

1552
                // Create parameter widget if computer has parameters
UNCOV
1553
                if (!computer_info.parameterDescriptors.empty()) {
×
1554
                    auto* param_widget = createParameterWidget(QString::fromStdString(computer_info.name), 
×
1555
                                                               computer_info.parameterDescriptors);
×
1556
                    if (param_widget) {
×
UNCOV
1557
                        ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
×
1558
                        _computer_parameter_widgets[computer_item] = param_widget;
×
1559
                    }
1560
                }
1561

1562
                // Restore previous state if present
UNCOV
1563
                std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
×
UNCOV
1564
                auto it_prev = previous_states.find(prev_key);
×
UNCOV
1565
                if (it_prev != previous_states.end()) {
×
UNCOV
1566
                    computer_item->setCheckState(1, it_prev->second.first);
×
1567
                    if (!it_prev->second.second.isEmpty()) {
×
UNCOV
1568
                        computer_item->setText(2, it_prev->second.second);
×
1569
                    }
1570
                }
1571
            }
×
UNCOV
1572
        }
×
1573
    }
1574

1575
    // Resize columns to content
1576
    ui->computers_tree->resizeColumnToContents(0);
76✔
1577
    ui->computers_tree->resizeColumnToContents(1);
76✔
1578
    ui->computers_tree->resizeColumnToContents(2);
76✔
1579
    ui->computers_tree->resizeColumnToContents(3);
76✔
1580

1581
    _updating_computers_tree = false;
76✔
1582

1583
    // Update preview after refresh
1584
    triggerPreviewDebounced();
76✔
1585
}
152✔
1586

1587
void TableDesignerWidget::setJsonTemplateFromCurrentState() {
3✔
1588
    if (!_table_json_widget) return;
3✔
1589
    // Build a minimal JSON template representing current UI state
1590
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
3✔
1591
    auto columns = getEnabledColumnInfos();
3✔
1592
    if (row_source.isEmpty() && columns.empty()) {
3✔
UNCOV
1593
        _table_json_widget->setJsonText("{}");
×
UNCOV
1594
        return;
×
1595
    }
1596

1597
    QString row_type;
3✔
1598
    QString row_source_name;
3✔
1599
    if (row_source.startsWith("TimeFrame: ")) {
3✔
1600
        row_type = "timestamp";
×
1601
        row_source_name = row_source.mid(11);
×
1602
    } else if (row_source.startsWith("Events: ")) {
3✔
UNCOV
1603
        row_type = "timestamp";
×
UNCOV
1604
        row_source_name = row_source.mid(8);
×
1605
    } else if (row_source.startsWith("Intervals: ")) {
3✔
1606
        row_type = "interval";
3✔
1607
        row_source_name = row_source.mid(11);
3✔
1608
    }
1609

1610
    QStringList column_entries;
3✔
1611
    for (auto const & c: columns) {
8✔
1612
        // Strip any internal prefixes for JSON to keep schema user-friendly
1613
        QString ds = QString::fromStdString(c.dataSourceName);
5✔
1614
        if (ds.startsWith("events:")) ds = ds.mid(7);
5✔
1615
        else if (ds.startsWith("intervals:"))
3✔
1616
            ds = ds.mid(10);
3✔
UNCOV
1617
        else if (ds.startsWith("analog:"))
×
UNCOV
1618
            ds = ds.mid(7);
×
1619

1620
        QString entry = QString(
15✔
1621
                                "{\n  \"name\": \"%1\",\n  \"description\": \"%2\",\n  \"data_source\": \"%3\",\n  \"computer\": \"%4\"%5\n}")
1622
                                .arg(QString::fromStdString(c.name))
20✔
1623
                                .arg(QString::fromStdString(c.description))
20✔
1624
                                .arg(ds)
20✔
1625
                                .arg(QString::fromStdString(c.computerName))
20✔
1626
                                .arg(c.parameters.empty() ? QString() : QString(",\n  \"parameters\": {}"));
10✔
1627
        column_entries << entry;
5✔
1628
    }
5✔
1629

1630
    QString table_name = _table_info_widget ? _table_info_widget->getName() : _current_table_id;
3✔
1631
    QString json = QString(
9✔
1632
                           "{\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}")
1633
                           .arg(_current_table_id)
12✔
1634
                           .arg(table_name)
12✔
1635
                           .arg(row_type)
12✔
1636
                           .arg(row_source_name)
12✔
1637
                           .arg(column_entries.join(",\n"));
6✔
1638

1639
    _table_json_widget->setJsonText(json);
3✔
1640
}
3✔
1641

1642
void TableDesignerWidget::applyJsonTemplateToUI(QString const & jsonText) {
7✔
1643
    // Very light-weight parser using Qt to extract essential fields.
1644
    // Assumes a schema similar to tests under computers *.test.cpp.
1645
    QJsonParseError err;
7✔
1646
    QByteArray bytes = jsonText.toUtf8();
7✔
1647
    QJsonDocument doc = QJsonDocument::fromJson(bytes, &err);
7✔
1648
    if (err.error != QJsonParseError::NoError || !doc.isObject()) {
7✔
1649
        // Compute line/column from byte offset if possible
1650
        int64_t offset = static_cast<int64_t>(err.offset);
1✔
1651
        int line = 1;
1✔
1652
        int col = 1;
1✔
1653
        // Avoid operator[] ambiguity on some compilers by using qsizetype and at()
1654
        qsizetype len = std::min<qsizetype>(bytes.size(), static_cast<qsizetype>(offset));
1✔
1655
        for (qsizetype i = 0; i < len; ++i) {
149✔
1656
            char ch = bytes.at(i);
148✔
1657
            if (ch == '\n') {
148✔
1658
                ++line;
6✔
1659
                col = 1;
6✔
1660
            } else {
1661
                ++col;
142✔
1662
            }
1663
        }
1664
        QString detail = err.error != QJsonParseError::NoError
1✔
1665
                                 ? QString("%1 (line %2, column %3)").arg(err.errorString()).arg(line).arg(col)
3✔
1666
                                 : QString("JSON root must be an object");
2✔
1667
        auto * box = new QMessageBox(this);
1✔
1668
        box->setIcon(QMessageBox::Critical);
1✔
1669
        box->setWindowTitle("Invalid JSON");
1✔
1670
        box->setText(QString("JSON format is invalid: %1").arg(detail));
1✔
1671
        box->setAttribute(Qt::WA_DeleteOnClose);
1✔
1672
        box->show();
1✔
1673
        return;
1✔
1674
    }
1✔
1675
    auto obj = doc.object();
6✔
1676
    if (!obj.contains("tables") || !obj["tables"].isArray()) {
6✔
UNCOV
1677
        auto * box = new QMessageBox(this);
×
UNCOV
1678
        box->setIcon(QMessageBox::Critical);
×
UNCOV
1679
        box->setWindowTitle("Invalid JSON");
×
UNCOV
1680
        box->setText("Missing required key: tables (array)");
×
UNCOV
1681
        box->setAttribute(Qt::WA_DeleteOnClose);
×
UNCOV
1682
        box->show();
×
UNCOV
1683
        return;
×
1684
    }
1685
    auto tables = obj["tables"].toArray();
6✔
1686
    if (tables.isEmpty() || !tables[0].isObject()) return;
6✔
1687
    auto table = tables[0].toObject();
6✔
1688

1689
    // Row selector
1690
    QStringList errors;
6✔
1691
    QString rs_type;
6✔
1692
    QString rs_source;
6✔
1693
    if (table.contains("row_selector") && table["row_selector"].isObject()) {
6✔
1694
        auto rs = table["row_selector"].toObject();
5✔
1695
        rs_type = rs.value("type").toString();
5✔
1696
        rs_source = rs.value("source").toString();
5✔
1697
        if (rs_type.isEmpty() || rs_source.isEmpty()) {
5✔
UNCOV
1698
            errors << "Missing required keys in row_selector: 'type' and/or 'source'";
×
1699
        } else {
1700
            // Validate existence
1701
            bool source_ok = false;
5✔
1702
            if (rs_type == "interval") {
5✔
1703
                source_ok = (_data_manager && _data_manager->getData<DigitalIntervalSeries>(rs_source.toStdString()) != nullptr);
5✔
UNCOV
1704
            } else if (rs_type == "timestamp") {
×
UNCOV
1705
                source_ok = (_data_manager && (_data_manager->getTime(TimeKey(rs_source.toStdString())) != nullptr ||
×
UNCOV
1706
                                               _data_manager->getData<DigitalEventSeries>(rs_source.toStdString()) != nullptr));
×
1707
            } else {
UNCOV
1708
                errors << QString("Unsupported row_selector type: %1").arg(rs_type);
×
1709
            }
1710
            if (!source_ok) {
5✔
1711
                errors << QString("Row selector data key not found in DataManager: %1").arg(rs_source);
1✔
1712
            } else {
1713
                // Apply selection to UI
1714
                QString entry;
4✔
1715
                if (rs_type == "interval") {
4✔
1716
                    entry = QString("Intervals: %1").arg(rs_source);
4✔
UNCOV
1717
                } else if (rs_type == "timestamp") {
×
1718
                    // Prefer TimeFrame, fallback to Events
UNCOV
1719
                    entry = QString("TimeFrame: %1").arg(rs_source);
×
UNCOV
1720
                    int idx_tf = ui->row_data_source_combo->findText(entry);
×
UNCOV
1721
                    if (idx_tf < 0) entry = QString("Events: %1").arg(rs_source);
×
1722
                }
1723
                int idx = ui->row_data_source_combo->findText(entry);
4✔
1724
                if (idx >= 0) {
4✔
1725
                    ui->row_data_source_combo->setCurrentIndex(idx);
4✔
1726
                    // Ensure computers tree reflects this row selector before enabling columns
1727
                    refreshComputersTree();
4✔
1728
                } else {
UNCOV
1729
                    errors << QString("Row selector entry not available in UI: %1").arg(entry);
×
1730
                }
1731
            }
4✔
1732
        }
1733
    } else {
5✔
1734
        errors << "Missing required key: row_selector (object)";
1✔
1735
    }
1736

1737
    // Columns: enable matching computers and set column names
1738
    if (table.contains("columns") && table["columns"].isArray()) {
6✔
1739
        auto cols = table["columns"].toArray();
6✔
1740
        auto * tree = ui->computers_tree;
6✔
1741
        // Avoid recursive preview rebuilds while we toggle many items
1742
        bool prevBlocked = tree->blockSignals(true);
6✔
1743
        for (auto const & cval: cols) {
12✔
1744
            if (!cval.isObject()) continue;
6✔
1745
            auto cobj = cval.toObject();
6✔
1746
            QString data_source = cobj.value("data_source").toString();
6✔
1747
            QString computer = cobj.value("computer").toString();
6✔
1748
            QString name = cobj.value("name").toString();
6✔
1749
            if (data_source.isEmpty() || computer.isEmpty() || name.isEmpty()) {
6✔
UNCOV
1750
                errors << "Missing required keys in column: 'name', 'data_source', and 'computer'";
×
UNCOV
1751
                continue;
×
1752
            }
1753
            // Validate data source existence
1754
            bool has_ds = (_data_manager && (_data_manager->getData<DigitalEventSeries>(data_source.toStdString()) != nullptr ||
19✔
1755
                                             _data_manager->getData<DigitalIntervalSeries>(data_source.toStdString()) != nullptr ||
7✔
1756
                                             _data_manager->getData<AnalogTimeSeries>(data_source.toStdString()) != nullptr));
13✔
1757
            if (!has_ds) {
6✔
1758
                errors << QString("Data key not found in DataManager: %1").arg(data_source);
1✔
1759
            }
1760
            // Validate computer exists
1761
            bool computer_exists = false;
6✔
1762
            if (_data_manager) {
6✔
1763
                if (auto * reg = _data_manager->getTableRegistry()) {
6✔
1764
                    auto & cr = reg->getComputerRegistry();
6✔
1765
                    computer_exists = cr.findComputerInfo(computer.toStdString());
6✔
1766
                }
1767
            }
1768
            if (!computer_exists) {
6✔
1769
                errors << QString("Requested computer does not exist: %1").arg(computer);
2✔
1770
            }
1771
            // Validate compatibility (heuristic)
1772
            bool type_event = false, type_interval = false, type_analog = false;
6✔
1773
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
60✔
1774
                auto * ds_item = tree->topLevelItem(i);
54✔
1775
                QString ds_text = ds_item->text(0);
54✔
1776
                if (ds_text.contains(data_source)) {
54✔
1777
                    if (ds_text.startsWith("Events: ")) type_event = true;
5✔
UNCOV
1778
                    else if (ds_text.startsWith("Intervals: "))
×
UNCOV
1779
                        type_interval = true;
×
UNCOV
1780
                    else if (ds_text.startsWith("analog:"))
×
UNCOV
1781
                        type_analog = true;
×
1782
                }
1783
            }
54✔
1784
            QString ds_repr;
6✔
1785
            if (type_event) ds_repr = QString("Events: %1").arg(data_source);
6✔
1786
            else if (type_interval)
1✔
UNCOV
1787
                ds_repr = QString("Intervals: %1").arg(data_source);
×
1788
            else if (type_analog)
1✔
1789
                ds_repr = QString("analog:%1").arg(data_source);
×
1790
            if (!ds_repr.isEmpty() && !isComputerCompatibleWithDataSource(computer.toStdString(), ds_repr)) {
6✔
1791
                errors << QString("Computer '%1' is not valid for data source type requested (%2)").arg(computer, ds_repr);
2✔
1792
            }
1793

1794
            // Find matching tree item with strict preference
1795
            QString exact_events = QString("Events: %1").arg(data_source);
6✔
1796
            QString exact_intervals = QString("Intervals: %1").arg(data_source);
6✔
1797
            QString exact_analog = QString("analog:%1").arg(data_source);
6✔
1798
            QTreeWidgetItem * matched_ds = nullptr;
6✔
1799
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
30✔
1800
                auto * ds_item = tree->topLevelItem(i);
29✔
1801
                QString t = ds_item->text(0);
29✔
1802
                if (t == exact_events || t == exact_intervals || t == exact_analog) {
29✔
1803
                    matched_ds = ds_item;
5✔
1804
                    break;
5✔
1805
                }
1806
            }
29✔
1807
            if (!matched_ds) {
6✔
1808
                for (int i = 0; i < tree->topLevelItemCount(); ++i) {
10✔
1809
                    auto * ds_item = tree->topLevelItem(i);
9✔
1810
                    QString t = ds_item->text(0);
9✔
1811
                    if (t.contains(data_source) || t.endsWith(data_source)) {
9✔
UNCOV
1812
                        matched_ds = ds_item;
×
UNCOV
1813
                        break;
×
1814
                    }
1815
                }
9✔
1816
            }
1817
            if (matched_ds) {
6✔
1818
                for (int j = 0; j < matched_ds->childCount(); ++j) {
20✔
1819
                    auto * comp_item = matched_ds->child(j);
15✔
1820
                    QString comp_text = comp_item->text(0).trimmed();
15✔
1821
                    if (comp_text == computer || comp_text.contains(computer)) {
15✔
1822
                        comp_item->setCheckState(1, Qt::Checked);
3✔
1823
                        if (!name.isEmpty()) comp_item->setText(2, name);
3✔
1824
                    }
1825
                }
15✔
1826
            } else {
1827
                errors << QString("Data source not found in tree: %1").arg(data_source);
1✔
1828
            }
1829
        }
6✔
1830
        tree->blockSignals(prevBlocked);
6✔
1831
        if (!errors.isEmpty()) {
6✔
1832
            auto * box = new QMessageBox(this);
4✔
1833
            box->setIcon(QMessageBox::Critical);
4✔
1834
            box->setWindowTitle("Invalid Table JSON");
4✔
1835
            box->setText(errors.join("\n"));
4✔
1836
            box->setAttribute(Qt::WA_DeleteOnClose);
4✔
1837
            box->show();
4✔
1838
            return;
4✔
1839
        }
1840
        triggerPreviewDebounced();
2✔
1841
    }
6✔
1842
}
36✔
1843

1844
void TableDesignerWidget::onComputersTreeItemChanged() {
19,786✔
1845
    if (_updating_computers_tree) return;
19,786✔
1846

1847
    // Trigger preview update when checkbox states change
1848
    triggerPreviewDebounced();
15✔
1849
}
1850

1851
void TableDesignerWidget::onComputersTreeItemEdited(QTreeWidgetItem * item, int column) {
19,786✔
1852
    if (_updating_computers_tree) return;
19,786✔
1853

1854
    // Only respond to column name edits (column 2)
1855
    if (column == 2) {
15✔
1856
        // Column name was edited, trigger preview update
1857
        triggerPreviewDebounced();
1✔
1858
    }
1859
}
1860

1861
std::vector<ColumnInfo> TableDesignerWidget::getEnabledColumnInfos() const {
70✔
1862
    std::vector<ColumnInfo> column_infos;
70✔
1863

1864
    if (!ui->computers_tree) return column_infos;
70✔
1865

1866
    // Iterate through all data source items
1867
    for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
721✔
1868
        auto * data_source_item = ui->computers_tree->topLevelItem(i);
651✔
1869

1870
        // Iterate through computer items under each data source
1871
        for (int j = 0; j < data_source_item->childCount(); ++j) {
2,779✔
1872
            auto * computer_item = data_source_item->child(j);
2,128✔
1873

1874
            // Check if this computer is enabled
1875
            if (computer_item->checkState(1) == Qt::Checked) {
2,128✔
1876
                QString data_source = computer_item->data(0, Qt::UserRole).toString();
44✔
1877
                QString computer_name = computer_item->data(1, Qt::UserRole).toString();
44✔
1878
                QString column_name = computer_item->text(2);
44✔
1879
                bool is_group_computer = computer_item->data(2, Qt::UserRole).toBool();
44✔
1880
                
1881
                // Get parameter values for this computer
1882
                std::map<std::string, std::string> parameters = getParameterValues(computer_name);
44✔
1883

1884
                if (is_group_computer) {
44✔
1885
                    // This is a group computer - create columns for all members
UNCOV
1886
                    QStringList group_members = data_source.split("||");
×
1887
                    
UNCOV
1888
                    for (QString const & member : group_members) {
×
1889
                        // Generate individual column name (e.g., "spike_1_Mean", "spike_2_Mean")
UNCOV
1890
                        QString individual_column_name = generateDefaultColumnName(member, computer_name);
×
1891

1892
                        // Create ColumnInfo for each group member
UNCOV
1893
                        QString source_key = member;
×
UNCOV
1894
                        if (source_key.startsWith("Events: ")) {
×
UNCOV
1895
                            source_key = QString("events:%1").arg(source_key.mid(8));
×
UNCOV
1896
                        } else if (source_key.startsWith("Intervals: ")) {
×
UNCOV
1897
                            source_key = QString("intervals:%1").arg(source_key.mid(11));
×
UNCOV
1898
                        } else if (source_key.startsWith("analog:")) {
×
UNCOV
1899
                            source_key = source_key;// already prefixed
×
UNCOV
1900
                        } else if (source_key.startsWith("TimeFrame: ")) {
×
1901
                            // TimeFrame used only for row selector; columns require concrete sources
UNCOV
1902
                            source_key = source_key.mid(11);
×
1903
                        }
1904

UNCOV
1905
                        ColumnInfo info(individual_column_name.toStdString(),
×
UNCOV
1906
                                        QString("Column from %1 using %2 (group applied)").arg(member, computer_name).toStdString(),
×
UNCOV
1907
                                        source_key.toStdString(),
×
UNCOV
1908
                                        computer_name.toStdString());
×
1909

1910
                        // Set parameters
UNCOV
1911
                        info.parameters = parameters;
×
1912

1913
                        // Set output type based on computer info
UNCOV
1914
                        if (auto * registry = _data_manager->getTableRegistry()) {
×
UNCOV
1915
                            auto & computer_registry = registry->getComputerRegistry();
×
UNCOV
1916
                            auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
×
UNCOV
1917
                            if (computer_info) {
×
UNCOV
1918
                                info.outputType = computer_info->outputType;
×
UNCOV
1919
                                info.outputTypeName = computer_info->outputTypeName;
×
UNCOV
1920
                                info.isVectorType = computer_info->isVectorType;
×
UNCOV
1921
                                if (info.isVectorType) {
×
UNCOV
1922
                                    info.elementType = computer_info->elementType;
×
UNCOV
1923
                                    info.elementTypeName = computer_info->elementTypeName;
×
1924
                                }
1925
                            }
1926
                        }
1927

UNCOV
1928
                        column_infos.push_back(std::move(info));
×
UNCOV
1929
                    }
×
UNCOV
1930
                } else {
×
1931
                    // Individual computer - original behavior
1932
                    if (column_name.isEmpty()) {
44✔
UNCOV
1933
                        column_name = generateDefaultColumnName(data_source, computer_name);
×
1934
                    }
1935

1936
                    // Create ColumnInfo (use raw key without UI prefixes)
1937
                    QString source_key = data_source;
44✔
1938
                    if (source_key.startsWith("Events: ")) {
44✔
1939
                        source_key = QString("events:%1").arg(source_key.mid(8));
21✔
1940
                    } else if (source_key.startsWith("Intervals: ")) {
23✔
1941
                        source_key = QString("intervals:%1").arg(source_key.mid(11));
23✔
UNCOV
1942
                    } else if (source_key.startsWith("analog:")) {
×
UNCOV
1943
                        source_key = source_key;// already prefixed
×
UNCOV
1944
                    } else if (source_key.startsWith("TimeFrame: ")) {
×
1945
                        // TimeFrame used only for row selector; columns require concrete sources
UNCOV
1946
                        source_key = source_key.mid(11);
×
1947
                    }
1948

1949
                    ColumnInfo info(column_name.toStdString(),
132✔
1950
                                    QString("Column from %1 using %2").arg(data_source, computer_name).toStdString(),
88✔
1951
                                    source_key.toStdString(),
88✔
1952
                                    computer_name.toStdString());
220✔
1953

1954
                    // Set parameters
1955
                    info.parameters = parameters;
44✔
1956

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

1972
                    column_infos.push_back(std::move(info));
44✔
1973
                }
44✔
1974
            }
44✔
1975
        }
1976
    }
1977

1978
    return column_infos;
70✔
UNCOV
1979
}
×
1980

1981
bool TableDesignerWidget::isComputerCompatibleWithDataSource(std::string const & computer_name, QString const & data_source) const {
5✔
1982
    if (!_data_manager) return false;
5✔
1983

1984
    auto * registry = _data_manager->getTableRegistry();
5✔
1985
    if (!registry) return false;
5✔
1986

1987
    auto & computer_registry = registry->getComputerRegistry();
5✔
1988
    auto computer_info = computer_registry.findComputerInfo(computer_name);
5✔
1989
    if (!computer_info) return false;
5✔
1990

1991
    // Basic compatibility check based on data source type and common computer patterns
1992
    if (data_source.startsWith("Events: ")) {
3✔
1993
        // Event-based computers typically have "Event" in their name
1994
        return computer_name.find("Event") != std::string::npos;
3✔
UNCOV
1995
    } else if (data_source.startsWith("Intervals: ")) {
×
1996
        // Interval-based computers typically work with intervals or events
UNCOV
1997
        return computer_name.find("Event") != std::string::npos ||
×
UNCOV
1998
               computer_name.find("Interval") != std::string::npos;
×
UNCOV
1999
    } else if (data_source.startsWith("analog:")) {
×
2000
        // Analog-based computers typically have "Analog" in their name
UNCOV
2001
        return computer_name.find("Analog") != std::string::npos;
×
UNCOV
2002
    } else if (data_source.startsWith("TimeFrame: ")) {
×
2003
        // TimeFrame-based computers - generally most computers can work with timestamps
UNCOV
2004
        return computer_name.find("Timestamp") != std::string::npos ||
×
UNCOV
2005
               computer_name.find("Time") != std::string::npos;
×
2006
    }
2007

2008
    // Default: assume compatibility for unrecognized patterns
UNCOV
2009
    return true;
×
2010
}
2011

2012
QString TableDesignerWidget::generateDefaultColumnName(QString const & data_source, QString const & computer_name) const {
2,294✔
2013
    QString source_name = data_source;
2,294✔
2014

2015
    // Extract the actual name from prefixed data sources
2016
    if (source_name.startsWith("Events: ")) {
2,294✔
2017
        source_name = source_name.mid(8);
462✔
2018
    } else if (source_name.startsWith("Intervals: ")) {
1,832✔
2019
        source_name = source_name.mid(11);
616✔
2020
    } else if (source_name.startsWith("analog:")) {
1,216✔
2021
        source_name = source_name.mid(7);
1,216✔
UNCOV
2022
    } else if (source_name.startsWith("TimeFrame: ")) {
×
UNCOV
2023
        source_name = source_name.mid(11);
×
2024
    }
2025

2026
    // Create a concise name
2027
    return QString("%1_%2").arg(source_name, computer_name);
6,882✔
2028
}
2,294✔
2029

2030
std::string TableDesignerWidget::extractGroupName(const QString& data_source) const {
705✔
2031
    QString source_name = data_source;
705✔
2032

2033
    // Extract the actual name from prefixed data sources
2034
    if (source_name.startsWith("Events: ")) {
705✔
2035
        source_name = source_name.mid(8);
154✔
2036
    } else if (source_name.startsWith("Intervals: ")) {
551✔
2037
        source_name = source_name.mid(11);
88✔
2038
    } else if (source_name.startsWith("analog:")) {
463✔
2039
        source_name = source_name.mid(7);
152✔
2040
    } else if (source_name.startsWith("TimeFrame: ")) {
311✔
2041
        source_name = source_name.mid(11);
311✔
2042
    }
2043

2044
    // Use the same grouping pattern as Feature_Tree_Widget
2045
    std::regex const pattern{_grouping_pattern};
705✔
2046
    std::smatch matches{};
705✔
2047
    std::string key = source_name.toStdString();
705✔
2048

2049
    if (std::regex_search(key, matches, pattern) && matches.size() > 1) {
705✔
UNCOV
2050
        return matches[1].str();
×
2051
    }
2052

2053
    return key; // Return the key itself if no match
705✔
2054
}
705✔
2055

UNCOV
2056
void TableDesignerWidget::onGroupModeToggled(bool enabled) {
×
UNCOV
2057
    _group_mode = enabled;
×
2058
    
2059
    // Update button text to reflect current mode
UNCOV
2060
    if (enabled) {
×
UNCOV
2061
        ui->group_mode_toggle_btn->setText("Group Mode");
×
UNCOV
2062
        ui->computers_info_label->setText("Select computers by checking the boxes. Similar data will be grouped and transformed together.");
×
2063
    } else {
UNCOV
2064
        ui->group_mode_toggle_btn->setText("Individual Mode");
×
UNCOV
2065
        ui->computers_info_label->setText("Select computers by checking the boxes. Each data source will be handled individually.");
×
2066
    }
2067
    
2068
    // Refresh the tree to apply the new grouping mode
UNCOV
2069
    refreshComputersTree();
×
UNCOV
2070
}
×
2071

2072
QWidget* TableDesignerWidget::createParameterWidget(const QString& computer_name, 
154✔
2073
                                                     const std::vector<std::unique_ptr<IParameterDescriptor>>& parameter_descriptors) {
2074
    if (parameter_descriptors.empty()) {
154✔
UNCOV
2075
        return nullptr;
×
2076
    }
2077
    
2078
    auto* widget = new QWidget();
154✔
2079
    auto* layout = new QHBoxLayout(widget);
154✔
2080
    layout->setContentsMargins(2, 2, 2, 2);
154✔
2081
    layout->setSpacing(4);
154✔
2082
    
2083
    for (const auto& param_desc : parameter_descriptors) {
308✔
2084
        QString param_name = QString::fromStdString(param_desc->getName());
154✔
2085
        QString param_key = computer_name + "::" + param_name;
154✔
2086
        
2087
        // Add parameter label
2088
        auto* label = new QLabel(QString::fromStdString(param_desc->getName()) + ":");
154✔
2089
        label->setToolTip(QString::fromStdString(param_desc->getDescription()));
154✔
2090
        layout->addWidget(label);
154✔
2091
        
2092
        if (param_desc->getUIHint() == "enum") {
154✔
2093
            // Create combo box for enum parameters
2094
            auto* combo = new QComboBox();
154✔
2095
            combo->setObjectName(param_key); // Store parameter key for retrieval
154✔
2096
            
2097
            auto ui_props = param_desc->getUIProperties();
154✔
2098
            QString options_str = QString::fromStdString(ui_props["options"]);
462✔
2099
            QString default_value = QString::fromStdString(ui_props["default"]);
462✔
2100
            
2101
            QStringList options = options_str.split(',', Qt::SkipEmptyParts);
154✔
2102
            combo->addItems(options);
154✔
2103
            
2104
            // Set default value
2105
            int default_index = combo->findText(default_value);
154✔
2106
            if (default_index >= 0) {
154✔
2107
                combo->setCurrentIndex(default_index);
154✔
2108
            }
2109
            
2110
            combo->setToolTip(QString::fromStdString(param_desc->getDescription()));
154✔
2111
            layout->addWidget(combo);
154✔
2112
            
2113
            // Store the widget for parameter retrieval
2114
            _parameter_controls[param_key.toStdString()] = combo;
154✔
2115
            
2116
        } else if (param_desc->getUIHint() == "number") {
154✔
2117
            // Create spin box for numeric parameters
UNCOV
2118
            auto* spinbox = new QSpinBox();
×
UNCOV
2119
            spinbox->setObjectName(param_key);
×
2120
            
UNCOV
2121
            auto ui_props = param_desc->getUIProperties();
×
UNCOV
2122
            QString default_str = QString::fromStdString(ui_props["default"]);
×
UNCOV
2123
            QString min_str = QString::fromStdString(ui_props["min"]);
×
UNCOV
2124
            QString max_str = QString::fromStdString(ui_props["max"]);
×
2125
            
UNCOV
2126
            if (!min_str.isEmpty()) spinbox->setMinimum(min_str.toInt());
×
UNCOV
2127
            if (!max_str.isEmpty()) spinbox->setMaximum(max_str.toInt());
×
UNCOV
2128
            if (!default_str.isEmpty()) spinbox->setValue(default_str.toInt());
×
2129
            
UNCOV
2130
            spinbox->setToolTip(QString::fromStdString(param_desc->getDescription()));
×
UNCOV
2131
            layout->addWidget(spinbox);
×
2132
            
UNCOV
2133
            _parameter_controls[param_key.toStdString()] = spinbox;
×
2134
            
UNCOV
2135
        } else {
×
2136
            // Default to text input
UNCOV
2137
            auto* lineedit = new QLineEdit();
×
UNCOV
2138
            lineedit->setObjectName(param_key);
×
2139
            
UNCOV
2140
            auto ui_props = param_desc->getUIProperties();
×
UNCOV
2141
            QString default_value = QString::fromStdString(ui_props["default"]);
×
UNCOV
2142
            lineedit->setText(default_value);
×
2143
            
UNCOV
2144
            lineedit->setToolTip(QString::fromStdString(param_desc->getDescription()));
×
UNCOV
2145
            layout->addWidget(lineedit);
×
2146
            
UNCOV
2147
            _parameter_controls[param_key.toStdString()] = lineedit;
×
UNCOV
2148
        }
×
2149
    }
154✔
2150
    
2151
    return widget;
154✔
2152
}
2153

2154
std::map<std::string, std::string> TableDesignerWidget::getParameterValues(const QString& computer_name) const {
44✔
2155
    std::map<std::string, std::string> parameters;
44✔
2156
    
2157
    // Look for parameter controls with this computer name prefix
2158
    QString prefix = computer_name + "::";
44✔
2159
    
2160
    for (const auto& [key, widget] : _parameter_controls) {
88✔
2161
        QString key_str = QString::fromStdString(key);
44✔
2162
        if (key_str.startsWith(prefix)) {
44✔
UNCOV
2163
            QString param_name = key_str.mid(prefix.length());
×
2164
            
UNCOV
2165
            if (auto* combo = qobject_cast<QComboBox*>(widget)) {
×
UNCOV
2166
                parameters[param_name.toStdString()] = combo->currentText().toStdString();
×
UNCOV
2167
            } else if (auto* spinbox = qobject_cast<QSpinBox*>(widget)) {
×
UNCOV
2168
                parameters[param_name.toStdString()] = QString::number(spinbox->value()).toStdString();
×
UNCOV
2169
            } else if (auto* lineedit = qobject_cast<QLineEdit*>(widget)) {
×
UNCOV
2170
                parameters[param_name.toStdString()] = lineedit->text().toStdString();
×
2171
            }
UNCOV
2172
        }
×
2173
    }
44✔
2174
    
2175
    return parameters;
44✔
2176
}
44✔
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