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

paulmthompson / WhiskerToolbox / 18050758459

26 Sep 2025 09:57PM UTC coverage: 69.825% (+0.1%) from 69.725%
18050758459

push

github

paulmthompson
multi computer tables show up now

192 of 242 new or added lines in 5 files covered. (79.34%)

22 existing lines in 3 files now uncovered.

43180 of 61840 relevant lines covered (69.83%)

1137.92 hits per line

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

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

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

27

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

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

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

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

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

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

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

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

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

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

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

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

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

103
    connectSignals();
18✔
104
    
105

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

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

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

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

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

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

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

170

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

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

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

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

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

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

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

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

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

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

260
void TableDesignerWidget::onCreateNewTable() {
×
261
    bool ok;
×
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

268
    auto * registry = _data_manager->getTableRegistry();
×
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
275
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
276
            if (ui->table_combo->itemData(i).toString().toStdString() == table_id) {
×
277
                ui->table_combo->setCurrentIndex(i);
×
278
                break;
×
279
            }
280
        }
281
    } else {
282
        QMessageBox::warning(this, "Error", "Failed to create table with ID: " + QString::fromStdString(table_id));
×
283
    }
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),
×
293
                                       QMessageBox::Yes | QMessageBox::No);
×
294

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

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

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

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

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

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

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

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

362
    QString row_source = ui->row_data_source_combo->currentText();
×
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
369
    auto column_infos = getEnabledColumnInfos();
×
370
    if (column_infos.empty()) {
×
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
377
        auto row_selector = createRowSelector(row_source);
×
378
        if (!row_selector) {
×
379
            updateBuildStatus("Failed to create row selector", true);
×
380
            return;
×
381
        }
382

383
        // Get the data manager extension
384
        auto * reg = _data_manager->getTableRegistry();
×
385
        auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
×
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;
×
397
        for (auto const & column_info: column_infos) {
×
398
            if (!reg->addColumnToBuilder(builder, column_info)) {
×
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) {
×
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
413
        if (reg) {
×
414
            // Update table info with current column configuration
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
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()));
×
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 {
427
                updateBuildStatus("Failed to store built table", true);
×
428
            }
429
        } else {
×
430
            updateBuildStatus("Registry unavailable", true);
×
431
        }
432

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

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

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

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

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

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

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

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

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

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

496
        // Store the built table in the TableManager and update table info with current columns
497
        if (reg) {
4✔
498
            // Update table info with current column configuration
499
            auto table_info = reg->getTableInfo(_current_table_id.toStdString());
4✔
500
            table_info.columns = column_infos;// Store the current enabled columns
4✔
501
            reg->updateTableInfo(_current_table_id.toStdString(),
4✔
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)))) {
4✔
507
                updateBuildStatus(QString("Table built successfully with %1 columns!").arg(column_infos.size()));
4✔
508
                qDebug() << "Successfully built table:" << _current_table_id << "with" << column_infos.size() << "columns";
4✔
509
                // Populate JSON widget with the current configuration as well
510
                setJsonTemplateFromCurrentState();
4✔
511
                return true;
4✔
512
            } else {
513
                updateBuildStatus("Failed to store built table", true);
×
514
                return false;
×
515
            }
516
        } else {
4✔
517
            updateBuildStatus("Registry unavailable", true);
×
518
            return false;
×
519
        }
520

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

528
void TableDesignerWidget::onApplyTransform() {
×
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
    }
540
    auto base_view = reg->getBuiltTable(_current_table_id.toStdString());
×
541
    if (!base_view) {
×
542
        updateBuildStatus("Build the base table first", true);
×
543
        return;
×
544
    }
545

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

553
    // Configure PCA
554
    PCAConfig cfg;
×
555
    cfg.center = _table_transform_widget && _table_transform_widget->isCenterEnabled();
×
556
    cfg.standardize = _table_transform_widget && _table_transform_widget->isStandardizeEnabled();
×
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
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);
×
571
        }
×
572

573
        std::string out_id = reg->generateUniqueTableId((_current_table_id + "_pca").toStdString());
×
574
        if (!reg->createTable(out_id, out_name.toStdString())) {
×
575
            reg->updateTableInfo(out_id, out_name.toStdString(), "");
×
576
        }
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)) {
×
591
        s = s.trimmed();
×
592
        if (!s.isEmpty()) out.push_back(s.toStdString());
×
593
    }
×
594
    return out;
×
595
}
×
596

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
    }
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 = ",";
×
625
    if (delimiter == "Space") delim = " ";
×
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());
×
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

639
        auto names = view->getColumnNames();
×
640
        if (includeHeader) {
×
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
        }
647
        size_t rows = view->getRowCount();
×
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()) {
×
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());
×
663
                        if (r < vals.size()) {
×
664
                            file << vals[r];
×
665
                            wrote = true;
×
666
                        }
667
                    } catch (...) {}
×
668
                }
669
                if (!wrote) {
×
670
                    try {
671
                        auto const & vals = view->getColumnValues<int64_t>(names[c].c_str());
×
672
                        if (r < vals.size()) {
×
673
                            file << vals[r];
×
674
                            wrote = true;
×
675
                        }
676
                    } catch (...) {}
×
677
                }
678
                if (!wrote) {
×
679
                    try {
680
                        auto const & vals = view->getColumnValues<bool>(names[c].c_str());
×
681
                        if (r < vals.size()) {
×
682
                            file << (vals[r] ? 1 : 0);
×
683
                            wrote = true;
×
684
                        }
685
                    } catch (...) {}
×
686
                }
687
                if (!wrote) file << "NaN";
×
688
            }
689
            file << eol;
×
690
        }
691
        file.close();
×
692
        updateBuildStatus(QString("Exported CSV: %1").arg(filename));
×
693
    } catch (std::exception const & e) {
×
694
        updateBuildStatus(QString("Export failed: %1").arg(e.what()), true);
×
695
    }
×
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()) {
×
704
        return;
×
705
    }
706

707
    QString name = _table_info_widget ? _table_info_widget->getName() : QString();
×
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");
×
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
720
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
721
            if (ui->table_combo->itemData(i).toString() == _current_table_id) {
×
722
                ui->table_combo->setCurrentIndex(i);
×
723
                break;
×
724
            }
725
        }
726
    } else {
727
        QMessageBox::warning(this, "Error", "Failed to save table information");
×
728
    }
729
}
×
730

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

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

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

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

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

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

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

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

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

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

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

790

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

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

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

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

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

822
            // Update interval settings visibility
823
            updateIntervalSettingsVisibility();
14✔
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();
27✔
833

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

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

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

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

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

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

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

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

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

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

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

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

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

905
    if (!_data_manager) {
106✔
906
        qDebug() << "getAvailableDataSources: No data manager";
×
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();
106✔
913
    qDebug() << "getAvailableDataSources: TimeFrame keys:" << timeframe_keys.size();
106✔
914
    for (auto const & key: timeframe_keys) {
539✔
915
        QString source = QString("TimeFrame: %1").arg(QString::fromStdString(key.str()));
433✔
916
        sources << source;
433✔
917
        qDebug() << "  Added TimeFrame:" << source;
433✔
918
    }
433✔
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>();
106✔
923
    qDebug() << "getAvailableDataSources: Event keys:" << event_keys.size();
106✔
924
    for (auto const & key: event_keys) {
321✔
925
        QString source = QString("Events: %1").arg(QString::fromStdString(key));
215✔
926
        sources << source;
215✔
927
        qDebug() << "  Added Events:" << source;
215✔
928
    }
215✔
929

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

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

949
    // Add LineData keys as data sources (for computers; not row selectors)
950
    auto line_keys = _data_manager->getKeys<LineData>();
106✔
951
    qDebug() << "getAvailableDataSources: Line keys:" << line_keys.size();
106✔
952
    for (auto const & key: line_keys) {
212✔
953
        QString source = QString("lines:%1").arg(QString::fromStdString(key));
106✔
954
        sources << source;
106✔
955
        qDebug() << "  Added Lines:" << source;
106✔
956
    }
106✔
957

958
    qDebug() << "getAvailableDataSources: Total sources found:" << sources.size();
106✔
959

960
    return sources;
106✔
961
}
106✔
962

963
std::pair<std::optional<DataSourceVariant>, RowSelectorType>
964
TableDesignerWidget::createDataSourceVariant(QString const & data_source_string,
861✔
965
                                             std::shared_ptr<DataManagerExtension> data_manager_extension) const {
966
    std::optional<DataSourceVariant> result;
861✔
967
    RowSelectorType row_selector_type = RowSelectorType::IntervalBased;
861✔
968

969
    if (data_source_string.startsWith("TimeFrame: ")) {
861✔
970
        // TimeFrames are used with TimestampSelector for rows; no concrete data source needed
971
        row_selector_type = RowSelectorType::Timestamp;
343✔
972

973
    } else if (data_source_string.startsWith("Events: ")) {
518✔
974
        QString source_name = data_source_string.mid(8);// Remove "Events: " prefix
170✔
975
        // Event-based computers in the registry operate with interval rows
976
        row_selector_type = RowSelectorType::IntervalBased;
170✔
977

978
        if (auto event_source = data_manager_extension->getEventSource(source_name.toStdString())) {
170✔
979
            result = event_source;
170✔
980
        }
170✔
981

982
    } else if (data_source_string.startsWith("Intervals: ")) {
518✔
983
        QString source_name = data_source_string.mid(11);// Remove "Intervals: " prefix
96✔
984
        row_selector_type = RowSelectorType::IntervalBased;
96✔
985

986
        if (auto interval_source = data_manager_extension->getIntervalSource(source_name.toStdString())) {
96✔
987
            result = interval_source;
96✔
988
        }
96✔
989
    } else if (data_source_string.startsWith("analog:")) {
348✔
990
        QString source_name = data_source_string.mid(7);// Remove "analog:" prefix
168✔
991
        row_selector_type = RowSelectorType::IntervalBased;
168✔
992

993
        if (auto analog_source = data_manager_extension->getAnalogSource(source_name.toStdString())) {
168✔
994
            result = analog_source;
168✔
995
        }
168✔
996
    } else if (data_source_string.startsWith("lines:")) {
252✔
997
        QString source_name = data_source_string.mid(6);// Remove "lines:" prefix
84✔
998
        row_selector_type = RowSelectorType::Timestamp; // LineData computers work with timestamp row selectors
84✔
999

1000
        if (auto line_source = data_manager_extension->getLineSource(source_name.toStdString())) {
84✔
1001
            result = line_source;
84✔
1002
        }
84✔
1003
    }
84✔
1004

1005
    return {result, row_selector_type};
1,722✔
1006
}
861✔
1007

1008
void TableDesignerWidget::updateRowInfoLabel(QString const & selected_source) {
47✔
1009
    if (selected_source.isEmpty()) {
47✔
1010
        ui->row_info_label->setText("No row source selected");
×
1011
        return;
×
1012
    }
1013

1014
    // Parse the selected source to get type and name
1015
    QString source_type;
47✔
1016
    QString source_name;
47✔
1017

1018
    if (selected_source.startsWith("TimeFrame: ")) {
47✔
1019
        source_type = "TimeFrame";
25✔
1020
        source_name = selected_source.mid(11);// Remove "TimeFrame: " prefix
25✔
1021
    } else if (selected_source.startsWith("Events: ")) {
22✔
1022
        source_type = "Events";
×
1023
        source_name = selected_source.mid(8);// Remove "Events: " prefix
×
1024
    } else if (selected_source.startsWith("Intervals: ")) {
22✔
1025
        source_type = "Intervals";
22✔
1026
        source_name = selected_source.mid(11);// Remove "Intervals: " prefix
22✔
1027
    }
1028

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

1032
    if (!_data_manager) {
47✔
1033
        ui->row_info_label->setText(info_text);
×
1034
        return;
×
1035
    }
1036

1037
    auto * reg3 = _data_manager->getTableRegistry();
47✔
1038
    auto data_manager_extension = reg3 ? reg3->getDataManagerExtension() : nullptr;
47✔
1039
    if (!data_manager_extension) {
47✔
1040
        ui->row_info_label->setText(info_text);
×
1041
        return;
×
1042
    }
1043

1044
    auto const source_name_str = source_name.toStdString();
47✔
1045

1046
    // Add specific information based on source type
1047
    if (source_type == "TimeFrame") {
47✔
1048
        auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
25✔
1049
        if (timeframe) {
25✔
1050
            info_text += QString(" - %1 time points").arg(timeframe->getTotalFrameCount());
25✔
1051
        }
1052
    } else if (source_type == "Events") {
47✔
1053
        auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
1054
        if (event_series) {
×
1055
            auto events = event_series->getEventSeries();
×
1056
            info_text += QString(" - %1 events").arg(events.size());
×
1057
        }
×
1058
    } else if (source_type == "Intervals") {
22✔
1059
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
22✔
1060
        if (interval_series) {
22✔
1061
            auto intervals = interval_series->getDigitalIntervalSeries();
22✔
1062
            info_text += QString(" - %1 intervals").arg(intervals.size());
22✔
1063

1064
            // Add capture range and interval setting information
1065
            if (isIntervalItselfSelected()) {
22✔
1066
                info_text += QString("\nUsing intervals as-is (no capture range)");
2✔
1067
            } else {
1068
                int capture_range = getCaptureRange();
20✔
1069
                QString interval_point = isIntervalBeginningSelected() ? "beginning" : "end";
20✔
1070
                info_text += QString("\nCapture range: ±%1 samples around %2 of intervals").arg(capture_range).arg(interval_point);
20✔
1071
            }
20✔
1072
        }
22✔
1073
    }
22✔
1074

1075
    ui->row_info_label->setText(info_text);
47✔
1076
}
47✔
1077

1078
std::unique_ptr<IRowSelector> TableDesignerWidget::createRowSelector(QString const & row_source) {
24✔
1079
    // Parse the row source to get type and name
1080
    QString source_type;
24✔
1081
    QString source_name;
24✔
1082

1083
    if (row_source.startsWith("TimeFrame: ")) {
24✔
1084
        source_type = "TimeFrame";
4✔
1085
        source_name = row_source.mid(11);// Remove "TimeFrame: " prefix
4✔
1086
    } else if (row_source.startsWith("Events: ")) {
20✔
1087
        source_type = "Events";
×
1088
        source_name = row_source.mid(8);// Remove "Events: " prefix
×
1089
    } else if (row_source.startsWith("Intervals: ")) {
20✔
1090
        source_type = "Intervals";
20✔
1091
        source_name = row_source.mid(11);// Remove "Intervals: " prefix
20✔
1092
    } else {
1093
        qDebug() << "Unknown row source format:" << row_source;
×
1094
        return nullptr;
×
1095
    }
1096

1097
    auto const source_name_str = source_name.toStdString();
24✔
1098

1099
    try {
1100
        if (source_type == "TimeFrame") {
24✔
1101
            // Create IntervalSelector using TimeFrame
1102
            auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
4✔
1103
            if (!timeframe) {
4✔
1104
                qDebug() << "TimeFrame not found:" << source_name;
×
1105
                return nullptr;
×
1106
            }
1107

1108
            // Use timestamps to select all rows
1109
            std::vector<TimeFrameIndex> timestamps;
4✔
1110
            for (int64_t i = 0; i < timeframe->getTotalFrameCount(); ++i) {
408✔
1111
                timestamps.push_back(TimeFrameIndex(i));
404✔
1112
            }
1113
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe);
4✔
1114

1115
        } else if (source_type == "Events") {
24✔
1116
            // Create TimestampSelector using DigitalEventSeries
1117
            auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
1118
            if (!event_series) {
×
1119
                qDebug() << "DigitalEventSeries not found:" << source_name;
×
1120
                return nullptr;
×
1121
            }
1122

1123
            auto events = event_series->getEventSeries();
×
1124
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
×
1125
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
×
1126
            if (!timeframe_obj) {
×
1127
                qDebug() << "TimeFrame not found for events:" << timeframe_key.str();
×
1128
                return nullptr;
×
1129
            }
1130

1131
            // Convert events to TimeFrameIndex
1132
            std::vector<TimeFrameIndex> timestamps;
×
1133
            for (auto const & event: events) {
×
1134
                timestamps.push_back(TimeFrameIndex(static_cast<int64_t>(event)));
×
1135
            }
1136

1137
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe_obj);
×
1138

1139
        } else if (source_type == "Intervals") {
20✔
1140
            // Create IntervalSelector using DigitalIntervalSeries with capture range
1141
            auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
20✔
1142
            if (!interval_series) {
20✔
1143
                qDebug() << "DigitalIntervalSeries not found:" << source_name;
×
1144
                return nullptr;
×
1145
            }
1146

1147
            auto intervals = interval_series->getDigitalIntervalSeries();
20✔
1148
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
20✔
1149
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
20✔
1150
            if (!timeframe_obj) {
20✔
1151
                qDebug() << "TimeFrame not found for intervals:" << timeframe_key.str();
×
1152
                return nullptr;
×
1153
            }
1154

1155
            // Get capture range and interval setting
1156
            int capture_range = getCaptureRange();
20✔
1157
            bool use_beginning = isIntervalBeginningSelected();
20✔
1158
            bool use_interval_itself = isIntervalItselfSelected();
20✔
1159

1160
            // Create intervals based on the selected option
1161
            std::vector<TimeFrameInterval> tf_intervals;
20✔
1162
            for (auto const & interval: intervals) {
99✔
1163
                if (use_interval_itself) {
79✔
1164
                    // Use the interval as-is
1165
                    tf_intervals.emplace_back(TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
3✔
1166
                } else {
1167
                    // Determine the reference point (beginning or end of interval)
1168
                    int64_t reference_point;
1169
                    if (use_beginning) {
76✔
1170
                        reference_point = interval.start;
76✔
1171
                    } else {
1172
                        reference_point = interval.end;
×
1173
                    }
1174

1175
                    // Create a new interval around the reference point
1176
                    int64_t start_point = reference_point - capture_range;
76✔
1177
                    int64_t end_point = reference_point + capture_range;
76✔
1178

1179
                    // Ensure bounds are within the timeframe
1180
                    start_point = std::max(start_point, int64_t(0));
76✔
1181
                    end_point = std::min(end_point, static_cast<int64_t>(timeframe_obj->getTotalFrameCount() - 1));
76✔
1182

1183
                    tf_intervals.emplace_back(TimeFrameIndex(start_point), TimeFrameIndex(end_point));
76✔
1184
                }
1185
            }
1186

1187
            return std::make_unique<IntervalSelector>(std::move(tf_intervals), timeframe_obj);
20✔
1188
        }
20✔
1189

1190
    } catch (std::exception const & e) {
×
1191
        qDebug() << "Exception creating row selector:" << e.what();
×
1192
        return nullptr;
×
1193
    }
×
1194

1195
    qDebug() << "Unsupported row source type:" << source_type;
×
1196
    return nullptr;
×
1197
}
24✔
1198

1199
bool TableDesignerWidget::addColumnToBuilder(TableViewBuilder & builder, ColumnInfo const & column_info) {
×
1200
    // Use the simplified TableRegistry method that handles all the type checking internally
1201
    auto * reg = _data_manager->getTableRegistry();
×
1202
    if (!reg) {
×
1203
        qDebug() << "TableRegistry not available";
×
1204
        return false;
×
1205
    }
1206

1207
    bool success = reg->addColumnToBuilder(builder, column_info);
×
1208
    if (!success) {
×
1209
        qDebug() << "Failed to add column to builder:" << QString::fromStdString(column_info.name);
×
1210
    }
1211

1212
    return success;
×
1213
}
1214

1215
void TableDesignerWidget::updateIntervalSettingsVisibility() {
47✔
1216
    if (!ui->interval_settings_group) {
47✔
1217
        return;
×
1218
    }
1219

1220
    QString selected_key = ui->row_data_source_combo->currentText();
47✔
1221
    if (selected_key.isEmpty()) {
47✔
1222
        ui->interval_settings_group->setVisible(false);
×
1223
        if (ui->capture_range_spinbox) {
×
1224
            ui->capture_range_spinbox->setEnabled(false);
×
1225
        }
1226
        return;
×
1227
    }
1228

1229
    if (!_data_manager) {
47✔
1230
        ui->interval_settings_group->setVisible(false);
×
1231
        if (ui->capture_range_spinbox) {
×
1232
            ui->capture_range_spinbox->setEnabled(false);
×
1233
        }
1234
        return;
×
1235
    }
1236

1237
    // Check if the selected source is an interval series
1238
    if (selected_key.startsWith("Intervals: ")) {
47✔
1239
        ui->interval_settings_group->setVisible(true);
22✔
1240

1241
        // Enable/disable capture range based on interval setting
1242
        if (ui->capture_range_spinbox) {
22✔
1243
            bool use_interval_itself = isIntervalItselfSelected();
22✔
1244
            ui->capture_range_spinbox->setEnabled(!use_interval_itself);
22✔
1245
        }
1246
    } else {
1247
        ui->interval_settings_group->setVisible(false);
25✔
1248
        if (ui->capture_range_spinbox) {
25✔
1249
            ui->capture_range_spinbox->setEnabled(false);
25✔
1250
        }
1251
    }
1252
}
47✔
1253

1254
int TableDesignerWidget::getCaptureRange() const {
40✔
1255
    if (ui->capture_range_spinbox) {
40✔
1256
        return ui->capture_range_spinbox->value();
40✔
1257
    }
1258
    return 30000;// Default value
×
1259
}
1260

1261
void TableDesignerWidget::setCaptureRange(int value) {
49✔
1262
    if (ui->capture_range_spinbox) {
49✔
1263
        ui->capture_range_spinbox->blockSignals(true);
49✔
1264
        ui->capture_range_spinbox->setValue(value);
49✔
1265
        ui->capture_range_spinbox->blockSignals(false);
49✔
1266
    }
1267
}
49✔
1268

1269
bool TableDesignerWidget::isIntervalBeginningSelected() const {
40✔
1270
    if (ui->interval_beginning_radio) {
40✔
1271
        return ui->interval_beginning_radio->isChecked();
40✔
1272
    }
1273
    return true;// Default to beginning
×
1274
}
1275

1276
bool TableDesignerWidget::isIntervalItselfSelected() const {
64✔
1277
    if (ui->interval_itself_radio) {
64✔
1278
        return ui->interval_itself_radio->isChecked();
64✔
1279
    }
1280
    return false;// Default to not selected
×
1281
}
1282

1283
void TableDesignerWidget::triggerPreviewDebounced() {
163✔
1284
    if (_preview_debounce_timer) _preview_debounce_timer->start();
163✔
1285
    // Also trigger an immediate rebuild to support non-interactive/test contexts
1286
    rebuildPreviewNow();
163✔
1287
}
163✔
1288

1289
void TableDesignerWidget::rebuildPreviewNow() {
163✔
1290
    if (!_data_manager || !_table_viewer) return;
163✔
1291
    if (_current_table_id.isEmpty()) {
163✔
1292
        _table_viewer->clearTable();
73✔
1293
        return;
73✔
1294
    }
1295

1296
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
90✔
1297
    if (row_source.isEmpty()) {
90✔
1298
        _table_viewer->clearTable();
24✔
1299
        return;
24✔
1300
    }
1301

1302
    // Get enabled column infos from the computers tree
1303
    auto column_infos = getEnabledColumnInfos();
66✔
1304
    if (column_infos.empty()) {
66✔
1305
        _table_viewer->clearTable();
46✔
1306
        return;
46✔
1307
    }
1308

1309
    // Create row selector for the entire dataset
1310
    auto selector = createRowSelector(row_source);
20✔
1311
    if (!selector) {
20✔
1312
        _table_viewer->clearTable();
×
1313
        return;
×
1314
    }
1315

1316
    // Apply any saved column order for this table id
1317
    auto desiredOrder = _table_column_order.value(_current_table_id);
20✔
1318
    if (!desiredOrder.isEmpty()) {
20✔
1319
        std::vector<ColumnInfo> reordered;
12✔
1320
        reordered.reserve(column_infos.size());
12✔
1321
        for (auto const & name: desiredOrder) {
39✔
1322
            auto it = std::find_if(column_infos.begin(), column_infos.end(), [&](ColumnInfo const & ci) { return QString::fromStdString(ci.name) == name; });
61✔
1323
            if (it != column_infos.end()) {
27✔
1324
                reordered.push_back(*it);
15✔
1325
            }
1326
        }
1327
        for (auto const & ci: column_infos) {
33✔
1328
            if (std::find_if(reordered.begin(), reordered.end(), [&](ColumnInfo const & x) { return x.name == ci.name; }) == reordered.end()) {
48✔
1329
                reordered.push_back(ci);
6✔
1330
            }
1331
        }
1332
        column_infos = std::move(reordered);
12✔
1333
    }
12✔
1334

1335
    // Set up the table viewer with pagination
1336
    _table_viewer->setTableConfiguration(
100✔
1337
            std::move(selector),
20✔
1338
            std::move(column_infos),
20✔
1339
            _data_manager,
20✔
1340
            QString("Preview: %1").arg(_current_table_id),
40✔
1341
            row_source);
1342

1343
    // Capture the current visual order from the viewer
1344
    QStringList currentOrder;
20✔
1345
    if (_table_viewer) {
20✔
1346
        auto * tv = _table_viewer->findChild<QTableView *>();
20✔
1347
        if (tv && tv->model()) {
20✔
1348
            auto * header = tv->horizontalHeader();
20✔
1349
            int cols = tv->model()->columnCount();
20✔
1350
            for (int v = 0; header && v < cols; ++v) {
64✔
1351
                int logical = header->logicalIndex(v);
44✔
1352
                auto name = tv->model()->headerData(logical, Qt::Horizontal, Qt::DisplayRole).toString();
44✔
1353
                currentOrder.push_back(name);
44✔
1354
            }
44✔
1355
        }
1356
    }
1357
    if (!currentOrder.isEmpty()) {
20✔
1358
        _table_column_order[_current_table_id] = currentOrder;
20✔
1359
    }
1360
}
136✔
1361

1362
void TableDesignerWidget::refreshComputersTree() {
84✔
1363
    if (!_data_manager) return;
84✔
1364

1365
    _updating_computers_tree = true;
84✔
1366

1367
    // Preserve previous checkbox states and custom column names
1368
    std::map<std::string, std::pair<Qt::CheckState, QString>> previous_states;
84✔
1369
    if (ui->computers_tree && ui->computers_tree->topLevelItemCount() > 0) {
84✔
1370
        for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
598✔
1371
            auto * top_level_item = ui->computers_tree->topLevelItem(i);
545✔
1372
            // Handle both grouped and individual modes
1373
            for (int j = 0; j < top_level_item->childCount(); ++j) {
2,197✔
1374
                auto * child_item = top_level_item->child(j);
1,652✔
1375
                if (child_item->childCount() > 0) {
1,652✔
1376
                    // This is a computer item under a group/data source
1377
                    for (int k = 0; k < child_item->childCount(); ++k) {
×
1378
                        auto * computer_item = child_item->child(k);
×
1379
                        QString ds = computer_item->data(0, Qt::UserRole).toString();
×
1380
                        QString cn = computer_item->data(1, Qt::UserRole).toString();
×
1381
                        std::string key = (ds + "||" + cn).toStdString();
×
1382
                        previous_states[key] = {computer_item->checkState(1), computer_item->text(2)};
×
1383
                    }
×
1384
                } else {
1385
                    // This is a computer item directly under data source (individual mode)
1386
                    QString ds = child_item->data(0, Qt::UserRole).toString();
1,652✔
1387
                    QString cn = child_item->data(1, Qt::UserRole).toString();
1,652✔
1388
                    std::string key = (ds + "||" + cn).toStdString();
1,652✔
1389
                    previous_states[key] = {child_item->checkState(1), child_item->text(2)};
1,652✔
1390
                }
1,652✔
1391
            }
1392
        }
1393
    }
1394

1395
    ui->computers_tree->clear();
84✔
1396
    ui->computers_tree->setHeaderLabels({"Data Source / Computer", "Enabled", "Column Name", "Parameters"});
504✔
1397
    
1398
    // Clean up parameter widgets
1399
    _computer_parameter_widgets.clear();
84✔
1400
    _parameter_controls.clear();
84✔
1401

1402
    auto * registry = _data_manager->getTableRegistry();
84✔
1403
    if (!registry) {
84✔
1404
        _updating_computers_tree = false;
×
1405
        return;
×
1406
    }
1407

1408
    auto data_manager_extension = registry->getDataManagerExtension();
84✔
1409
    if (!data_manager_extension) {
84✔
1410
        _updating_computers_tree = false;
×
1411
        return;
×
1412
    }
1413

1414
    auto & computer_registry = registry->getComputerRegistry();
84✔
1415

1416
    // Get available data sources
1417
    auto data_sources = getAvailableDataSources();
84✔
1418

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

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

1544
            if (!data_source_variant.has_value()) {
×
1545
                qDebug() << "Failed to create data source variant for:" << data_source;
×
1546
                continue;
×
1547
            }
1548

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

1552
            // Add compatible computers as children
1553
            for (auto const & computer_info: available_computers) {
×
1554
                auto * computer_item = new QTreeWidgetItem(data_source_item);
×
1555
                computer_item->setText(0, QString::fromStdString(computer_info.name));
×
1556
                computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
×
1557
                computer_item->setCheckState(1, Qt::Unchecked);
×
1558

1559
                // Generate default column name
1560
                QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
×
1561
                computer_item->setText(2, default_name);
×
1562
                computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
×
1563

1564
                // Store data source and computer name for later use
1565
                computer_item->setData(0, Qt::UserRole, data_source);
×
1566
                computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
×
1567
                computer_item->setData(2, Qt::UserRole, false); // Mark as individual computer
×
1568

1569
                // Create parameter widget if computer has parameters
1570
                if (!computer_info.parameterDescriptors.empty()) {
×
1571
                    auto* param_widget = createParameterWidget(QString::fromStdString(computer_info.name), 
×
1572
                                                               computer_info.parameterDescriptors);
×
1573
                    if (param_widget) {
×
1574
                        ui->computers_tree->setItemWidget(computer_item, 3, param_widget);
×
1575
                        _computer_parameter_widgets[computer_item] = param_widget;
×
1576
                    }
1577
                }
1578

1579
                // Restore previous state if present
1580
                std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
×
1581
                auto it_prev = previous_states.find(prev_key);
×
1582
                if (it_prev != previous_states.end()) {
×
1583
                    computer_item->setCheckState(1, it_prev->second.first);
×
1584
                    if (!it_prev->second.second.isEmpty()) {
×
1585
                        computer_item->setText(2, it_prev->second.second);
×
1586
                    }
1587
                }
1588
            }
×
1589
        }
×
1590
    }
1591

1592
    // Resize columns to content
1593
    ui->computers_tree->resizeColumnToContents(0);
84✔
1594
    ui->computers_tree->resizeColumnToContents(1);
84✔
1595
    ui->computers_tree->resizeColumnToContents(2);
84✔
1596
    ui->computers_tree->resizeColumnToContents(3);
84✔
1597

1598
    _updating_computers_tree = false;
84✔
1599

1600
    // Update preview after refresh
1601
    triggerPreviewDebounced();
84✔
1602
}
168✔
1603

1604
void TableDesignerWidget::setJsonTemplateFromCurrentState() {
4✔
1605
    if (!_table_json_widget) return;
4✔
1606
    // Build a minimal JSON template representing current UI state
1607
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
4✔
1608
    auto columns = getEnabledColumnInfos();
4✔
1609
    if (row_source.isEmpty() && columns.empty()) {
4✔
1610
        _table_json_widget->setJsonText("{}");
×
1611
        return;
×
1612
    }
1613

1614
    QString row_type;
4✔
1615
    QString row_source_name;
4✔
1616
    if (row_source.startsWith("TimeFrame: ")) {
4✔
1617
        row_type = "timestamp";
1✔
1618
        row_source_name = row_source.mid(11);
1✔
1619
    } else if (row_source.startsWith("Events: ")) {
3✔
1620
        row_type = "timestamp";
×
1621
        row_source_name = row_source.mid(8);
×
1622
    } else if (row_source.startsWith("Intervals: ")) {
3✔
1623
        row_type = "interval";
3✔
1624
        row_source_name = row_source.mid(11);
3✔
1625
    }
1626

1627
    QStringList column_entries;
4✔
1628
    for (auto const & c: columns) {
10✔
1629
        // Strip any internal prefixes for JSON to keep schema user-friendly
1630
        QString ds = QString::fromStdString(c.dataSourceName);
6✔
1631
        if (ds.startsWith("events:")) ds = ds.mid(7);
6✔
1632
        else if (ds.startsWith("intervals:"))
4✔
1633
            ds = ds.mid(10);
3✔
1634
        else if (ds.startsWith("analog:"))
1✔
1635
            ds = ds.mid(7);
×
1636

1637
        QString entry = QString(
18✔
1638
                                "{\n  \"name\": \"%1\",\n  \"description\": \"%2\",\n  \"data_source\": \"%3\",\n  \"computer\": \"%4\"%5\n}")
1639
                                .arg(QString::fromStdString(c.name))
24✔
1640
                                .arg(QString::fromStdString(c.description))
24✔
1641
                                .arg(ds)
24✔
1642
                                .arg(QString::fromStdString(c.computerName))
24✔
1643
                                .arg(c.parameters.empty() ? QString() : QString(",\n  \"parameters\": {}"));
12✔
1644
        column_entries << entry;
6✔
1645
    }
6✔
1646

1647
    QString table_name = _table_info_widget ? _table_info_widget->getName() : _current_table_id;
4✔
1648
    QString json = QString(
12✔
1649
                           "{\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}")
1650
                           .arg(_current_table_id)
16✔
1651
                           .arg(table_name)
16✔
1652
                           .arg(row_type)
16✔
1653
                           .arg(row_source_name)
16✔
1654
                           .arg(column_entries.join(",\n"));
8✔
1655

1656
    _table_json_widget->setJsonText(json);
4✔
1657
}
4✔
1658

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

1706
    // Row selector
1707
    QStringList errors;
6✔
1708
    QString rs_type;
6✔
1709
    QString rs_source;
6✔
1710
    if (table.contains("row_selector") && table["row_selector"].isObject()) {
6✔
1711
        auto rs = table["row_selector"].toObject();
5✔
1712
        rs_type = rs.value("type").toString();
5✔
1713
        rs_source = rs.value("source").toString();
5✔
1714
        if (rs_type.isEmpty() || rs_source.isEmpty()) {
5✔
1715
            errors << "Missing required keys in row_selector: 'type' and/or 'source'";
×
1716
        } else {
1717
            // Validate existence
1718
            bool source_ok = false;
5✔
1719
            if (rs_type == "interval") {
5✔
1720
                source_ok = (_data_manager && _data_manager->getData<DigitalIntervalSeries>(rs_source.toStdString()) != nullptr);
5✔
1721
            } else if (rs_type == "timestamp") {
×
1722
                source_ok = (_data_manager && (_data_manager->getTime(TimeKey(rs_source.toStdString())) != nullptr ||
×
1723
                                               _data_manager->getData<DigitalEventSeries>(rs_source.toStdString()) != nullptr));
×
1724
            } else {
1725
                errors << QString("Unsupported row_selector type: %1").arg(rs_type);
×
1726
            }
1727
            if (!source_ok) {
5✔
1728
                errors << QString("Row selector data key not found in DataManager: %1").arg(rs_source);
1✔
1729
            } else {
1730
                // Apply selection to UI
1731
                QString entry;
4✔
1732
                if (rs_type == "interval") {
4✔
1733
                    entry = QString("Intervals: %1").arg(rs_source);
4✔
1734
                } else if (rs_type == "timestamp") {
×
1735
                    // Prefer TimeFrame, fallback to Events
1736
                    entry = QString("TimeFrame: %1").arg(rs_source);
×
1737
                    int idx_tf = ui->row_data_source_combo->findText(entry);
×
1738
                    if (idx_tf < 0) entry = QString("Events: %1").arg(rs_source);
×
1739
                }
1740
                int idx = ui->row_data_source_combo->findText(entry);
4✔
1741
                if (idx >= 0) {
4✔
1742
                    ui->row_data_source_combo->setCurrentIndex(idx);
4✔
1743
                    // Ensure computers tree reflects this row selector before enabling columns
1744
                    refreshComputersTree();
4✔
1745
                } else {
1746
                    errors << QString("Row selector entry not available in UI: %1").arg(entry);
×
1747
                }
1748
            }
4✔
1749
        }
1750
    } else {
5✔
1751
        errors << "Missing required key: row_selector (object)";
1✔
1752
    }
1753

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

1811
            // Find matching tree item with strict preference
1812
            QString exact_events = QString("Events: %1").arg(data_source);
6✔
1813
            QString exact_intervals = QString("Intervals: %1").arg(data_source);
6✔
1814
            QString exact_analog = QString("analog:%1").arg(data_source);
6✔
1815
            QTreeWidgetItem * matched_ds = nullptr;
6✔
1816
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
31✔
1817
                auto * ds_item = tree->topLevelItem(i);
30✔
1818
                QString t = ds_item->text(0);
30✔
1819
                if (t == exact_events || t == exact_intervals || t == exact_analog) {
30✔
1820
                    matched_ds = ds_item;
5✔
1821
                    break;
5✔
1822
                }
1823
            }
30✔
1824
            if (!matched_ds) {
6✔
1825
                for (int i = 0; i < tree->topLevelItemCount(); ++i) {
11✔
1826
                    auto * ds_item = tree->topLevelItem(i);
10✔
1827
                    QString t = ds_item->text(0);
10✔
1828
                    if (t.contains(data_source) || t.endsWith(data_source)) {
10✔
1829
                        matched_ds = ds_item;
×
1830
                        break;
×
1831
                    }
1832
                }
10✔
1833
            }
1834
            if (matched_ds) {
6✔
1835
                for (int j = 0; j < matched_ds->childCount(); ++j) {
20✔
1836
                    auto * comp_item = matched_ds->child(j);
15✔
1837
                    QString comp_text = comp_item->text(0).trimmed();
15✔
1838
                    if (comp_text == computer || comp_text.contains(computer)) {
15✔
1839
                        comp_item->setCheckState(1, Qt::Checked);
3✔
1840
                        if (!name.isEmpty()) comp_item->setText(2, name);
3✔
1841
                    }
1842
                }
15✔
1843
            } else {
1844
                errors << QString("Data source not found in tree: %1").arg(data_source);
1✔
1845
            }
1846
        }
6✔
1847
        tree->blockSignals(prevBlocked);
6✔
1848
        if (!errors.isEmpty()) {
6✔
1849
            auto * box = new QMessageBox(this);
4✔
1850
            box->setIcon(QMessageBox::Critical);
4✔
1851
            box->setWindowTitle("Invalid Table JSON");
4✔
1852
            box->setText(errors.join("\n"));
4✔
1853
            box->setAttribute(Qt::WA_DeleteOnClose);
4✔
1854
            box->show();
4✔
1855
            return;
4✔
1856
        }
1857
        triggerPreviewDebounced();
2✔
1858
    }
6✔
1859
}
36✔
1860

1861
void TableDesignerWidget::onComputersTreeItemChanged() {
22,628✔
1862
    if (_updating_computers_tree) return;
22,628✔
1863

1864
    // Trigger preview update when checkbox states change
1865
    triggerPreviewDebounced();
16✔
1866
}
1867

1868
void TableDesignerWidget::onComputersTreeItemEdited(QTreeWidgetItem * item, int column) {
22,628✔
1869
    if (_updating_computers_tree) return;
22,628✔
1870

1871
    // Only respond to column name edits (column 2)
1872
    if (column == 2) {
16✔
1873
        // Column name was edited, trigger preview update
1874
        triggerPreviewDebounced();
1✔
1875
    }
1876
}
1877

1878
std::vector<ColumnInfo> TableDesignerWidget::getEnabledColumnInfos() const {
80✔
1879
    std::vector<ColumnInfo> column_infos;
80✔
1880

1881
    if (!ui->computers_tree) return column_infos;
80✔
1882

1883
    // Iterate through all data source items
1884
    for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
901✔
1885
        auto * data_source_item = ui->computers_tree->topLevelItem(i);
821✔
1886

1887
        // Iterate through computer items under each data source
1888
        for (int j = 0; j < data_source_item->childCount(); ++j) {
3,319✔
1889
            auto * computer_item = data_source_item->child(j);
2,498✔
1890

1891
            // Check if this computer is enabled
1892
            if (computer_item->checkState(1) == Qt::Checked) {
2,498✔
1893
                QString data_source = computer_item->data(0, Qt::UserRole).toString();
50✔
1894
                QString computer_name = computer_item->data(1, Qt::UserRole).toString();
50✔
1895
                QString column_name = computer_item->text(2);
50✔
1896
                bool is_group_computer = computer_item->data(2, Qt::UserRole).toBool();
50✔
1897
                
1898
                // Get parameter values for this computer
1899
                std::map<std::string, std::string> parameters = getParameterValues(computer_name);
50✔
1900

1901
                if (is_group_computer) {
50✔
1902
                    // This is a group computer - create columns for all members
1903
                    QStringList group_members = data_source.split("||");
×
1904
                    
1905
                    for (QString const & member : group_members) {
×
1906
                        // Generate individual column name (e.g., "spike_1_Mean", "spike_2_Mean")
1907
                        QString individual_column_name = generateDefaultColumnName(member, computer_name);
×
1908

1909
                        // Create ColumnInfo for each group member
1910
                        QString source_key = member;
×
1911
                        if (source_key.startsWith("Events: ")) {
×
1912
                            source_key = QString("events:%1").arg(source_key.mid(8));
×
1913
                        } else if (source_key.startsWith("Intervals: ")) {
×
1914
                            source_key = QString("intervals:%1").arg(source_key.mid(11));
×
1915
                        } else if (source_key.startsWith("analog:")) {
×
1916
                            source_key = source_key;// already prefixed
×
NEW
1917
                        } else if (source_key.startsWith("lines:")) {
×
NEW
1918
                            source_key = source_key;// already prefixed
×
UNCOV
1919
                        } else if (source_key.startsWith("TimeFrame: ")) {
×
1920
                            // TimeFrame used only for row selector; columns require concrete sources
1921
                            source_key = source_key.mid(11);
×
1922
                        }
1923

1924
                        ColumnInfo info(individual_column_name.toStdString(),
×
1925
                                        QString("Column from %1 using %2 (group applied)").arg(member, computer_name).toStdString(),
×
1926
                                        source_key.toStdString(),
×
1927
                                        computer_name.toStdString());
×
1928

1929
                        // Set parameters
1930
                        info.parameters = parameters;
×
1931

1932
                        // Set output type based on computer info
1933
                        if (auto * registry = _data_manager->getTableRegistry()) {
×
1934
                            auto & computer_registry = registry->getComputerRegistry();
×
1935
                            auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
×
1936
                            if (computer_info) {
×
1937
                                info.outputType = computer_info->outputType;
×
1938
                                info.outputTypeName = computer_info->outputTypeName;
×
1939
                                info.isVectorType = computer_info->isVectorType;
×
1940
                                if (info.isVectorType) {
×
1941
                                    info.elementType = computer_info->elementType;
×
1942
                                    info.elementTypeName = computer_info->elementTypeName;
×
1943
                                }
1944
                            }
1945
                        }
1946

1947
                        column_infos.push_back(std::move(info));
×
1948
                    }
×
1949
                } else {
×
1950
                    // Individual computer - original behavior
1951
                    if (column_name.isEmpty()) {
50✔
1952
                        // Clean the data source name before generating column name
NEW
1953
                        QString clean_data_source = data_source;
×
NEW
1954
                        if (clean_data_source.startsWith("lines:")) {
×
NEW
1955
                            clean_data_source = clean_data_source.mid(6); // Remove "lines:" prefix
×
1956
                        }
NEW
1957
                        column_name = generateDefaultColumnName(clean_data_source, computer_name);
×
UNCOV
1958
                    }
×
1959

1960
                    // Create ColumnInfo (use raw key without UI prefixes)
1961
                    QString source_key = data_source;
50✔
1962
                    if (source_key.startsWith("Events: ")) {
50✔
1963
                        source_key = QString("events:%1").arg(source_key.mid(8));
21✔
1964
                    } else if (source_key.startsWith("Intervals: ")) {
29✔
1965
                        source_key = QString("intervals:%1").arg(source_key.mid(11));
23✔
1966
                    } else if (source_key.startsWith("analog:")) {
6✔
1967
                        source_key = source_key;// already prefixed
×
1968
                    } else if (source_key.startsWith("lines:")) {
6✔
1969
                        source_key = source_key;// already prefixed
6✔
UNCOV
1970
                    } else if (source_key.startsWith("TimeFrame: ")) {
×
1971
                        // TimeFrame used only for row selector; columns require concrete sources
1972
                        source_key = source_key.mid(11);
×
1973
                    }
1974

1975
                    ColumnInfo info(column_name.toStdString(),
150✔
1976
                                    QString("Column from %1 using %2").arg(data_source, computer_name).toStdString(),
100✔
1977
                                    source_key.toStdString(),
100✔
1978
                                    computer_name.toStdString());
250✔
1979

1980
                    // Set parameters
1981
                    info.parameters = parameters;
50✔
1982

1983
                    // Set output type based on computer info
1984
                    if (auto * registry = _data_manager->getTableRegistry()) {
50✔
1985
                        auto & computer_registry = registry->getComputerRegistry();
50✔
1986
                        auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
50✔
1987
                        if (computer_info) {
50✔
1988
                            info.outputType = computer_info->outputType;
50✔
1989
                            info.outputTypeName = computer_info->outputTypeName;
50✔
1990
                            info.isVectorType = computer_info->isVectorType;
50✔
1991
                            if (info.isVectorType) {
50✔
1992
                                info.elementType = computer_info->elementType;
6✔
1993
                                info.elementTypeName = computer_info->elementTypeName;
6✔
1994
                            }
1995
                        }
1996
                    }
1997

1998
                    column_infos.push_back(std::move(info));
50✔
1999
                }
50✔
2000
            }
50✔
2001
        }
2002
    }
2003

2004
    return column_infos;
80✔
2005
}
×
2006

2007
bool TableDesignerWidget::isComputerCompatibleWithDataSource(std::string const & computer_name, QString const & data_source) const {
5✔
2008
    if (!_data_manager) return false;
5✔
2009

2010
    auto * registry = _data_manager->getTableRegistry();
5✔
2011
    if (!registry) return false;
5✔
2012

2013
    auto & computer_registry = registry->getComputerRegistry();
5✔
2014
    auto computer_info = computer_registry.findComputerInfo(computer_name);
5✔
2015
    if (!computer_info) return false;
5✔
2016

2017
    // Basic compatibility check based on data source type and common computer patterns
2018
    if (data_source.startsWith("Events: ")) {
3✔
2019
        // Event-based computers typically have "Event" in their name
2020
        return computer_name.find("Event") != std::string::npos;
3✔
2021
    } else if (data_source.startsWith("Intervals: ")) {
×
2022
        // Interval-based computers typically work with intervals or events
2023
        return computer_name.find("Event") != std::string::npos ||
×
2024
               computer_name.find("Interval") != std::string::npos;
×
2025
    } else if (data_source.startsWith("analog:")) {
×
2026
        // Analog-based computers typically have "Analog" in their name
2027
        return computer_name.find("Analog") != std::string::npos;
×
2028
    } else if (data_source.startsWith("TimeFrame: ")) {
×
2029
        // TimeFrame-based computers - generally most computers can work with timestamps
2030
        return computer_name.find("Timestamp") != std::string::npos ||
×
2031
               computer_name.find("Time") != std::string::npos;
×
2032
    }
2033

2034
    // Default: assume compatibility for unrecognized patterns
2035
    return true;
×
2036
}
2037

2038
QString TableDesignerWidget::generateDefaultColumnName(QString const & data_source, QString const & computer_name) const {
2,610✔
2039
    QString source_name = data_source;
2,610✔
2040

2041
    // Extract the actual name from prefixed data sources
2042
    if (source_name.startsWith("Events: ")) {
2,610✔
2043
        source_name = source_name.mid(8);
510✔
2044
    } else if (source_name.startsWith("Intervals: ")) {
2,100✔
2045
        source_name = source_name.mid(11);
672✔
2046
    } else if (source_name.startsWith("analog:")) {
1,428✔
2047
        source_name = source_name.mid(7);
1,344✔
2048
    } else if (source_name.startsWith("lines:")) {
84✔
2049
        source_name = source_name.mid(6);
84✔
2050
    } else if (source_name.startsWith("TimeFrame: ")) {
×
2051
        source_name = source_name.mid(11);
×
2052
    }
2053

2054
    // Create a concise name
2055
    return QString("%1_%2").arg(source_name, computer_name);
7,830✔
2056
}
2,610✔
2057

2058
std::string TableDesignerWidget::extractGroupName(const QString& data_source) const {
861✔
2059
    QString source_name = data_source;
861✔
2060

2061
    // Extract the actual name from prefixed data sources
2062
    if (source_name.startsWith("Events: ")) {
861✔
2063
        source_name = source_name.mid(8);
170✔
2064
    } else if (source_name.startsWith("Intervals: ")) {
691✔
2065
        source_name = source_name.mid(11);
96✔
2066
    } else if (source_name.startsWith("analog:")) {
595✔
2067
        source_name = source_name.mid(7);
168✔
2068
    } else if (source_name.startsWith("lines:")) {
427✔
2069
        source_name = source_name.mid(6);
84✔
2070
    } else if (source_name.startsWith("TimeFrame: ")) {
343✔
2071
        source_name = source_name.mid(11);
343✔
2072
    }
2073

2074
    // Use the same grouping pattern as Feature_Tree_Widget
2075
    std::regex const pattern{_grouping_pattern};
861✔
2076
    std::smatch matches{};
861✔
2077
    std::string key = source_name.toStdString();
861✔
2078

2079
    if (std::regex_search(key, matches, pattern) && matches.size() > 1) {
861✔
2080
        return matches[1].str();
×
2081
    }
2082

2083
    return key; // Return the key itself if no match
861✔
2084
}
861✔
2085

2086
void TableDesignerWidget::onGroupModeToggled(bool enabled) {
×
2087
    _group_mode = enabled;
×
2088
    
2089
    // Update button text to reflect current mode
2090
    if (enabled) {
×
2091
        ui->group_mode_toggle_btn->setText("Group Mode");
×
2092
        ui->computers_info_label->setText("Select computers by checking the boxes. Similar data will be grouped and transformed together.");
×
2093
    } else {
2094
        ui->group_mode_toggle_btn->setText("Individual Mode");
×
2095
        ui->computers_info_label->setText("Select computers by checking the boxes. Each data source will be handled individually.");
×
2096
    }
2097
    
2098
    // Refresh the tree to apply the new grouping mode
2099
    refreshComputersTree();
×
2100
}
×
2101

2102
QWidget* TableDesignerWidget::createParameterWidget(const QString& computer_name, 
254✔
2103
                                                     const std::vector<std::unique_ptr<IParameterDescriptor>>& parameter_descriptors) {
2104
    if (parameter_descriptors.empty()) {
254✔
2105
        return nullptr;
×
2106
    }
2107
    
2108
    auto* widget = new QWidget();
254✔
2109
    auto* layout = new QHBoxLayout(widget);
254✔
2110
    layout->setContentsMargins(2, 2, 2, 2);
254✔
2111
    layout->setSpacing(4);
254✔
2112
    
2113
    for (const auto& param_desc : parameter_descriptors) {
508✔
2114
        QString param_name = QString::fromStdString(param_desc->getName());
254✔
2115
        QString param_key = computer_name + "::" + param_name;
254✔
2116
        
2117
        // Add parameter label
2118
        auto* label = new QLabel(QString::fromStdString(param_desc->getName()) + ":");
254✔
2119
        label->setToolTip(QString::fromStdString(param_desc->getDescription()));
254✔
2120
        layout->addWidget(label);
254✔
2121
        
2122
        if (param_desc->getUIHint() == "enum") {
254✔
2123
            // Create combo box for enum parameters
2124
            auto* combo = new QComboBox();
170✔
2125
            combo->setObjectName(param_key); // Store parameter key for retrieval
170✔
2126
            
2127
            auto ui_props = param_desc->getUIProperties();
170✔
2128
            QString options_str = QString::fromStdString(ui_props["options"]);
510✔
2129
            QString default_value = QString::fromStdString(ui_props["default"]);
510✔
2130
            
2131
            QStringList options = options_str.split(',', Qt::SkipEmptyParts);
170✔
2132
            combo->addItems(options);
170✔
2133
            
2134
            // Set default value
2135
            int default_index = combo->findText(default_value);
170✔
2136
            if (default_index >= 0) {
170✔
2137
                combo->setCurrentIndex(default_index);
170✔
2138
            }
2139
            
2140
            combo->setToolTip(QString::fromStdString(param_desc->getDescription()));
170✔
2141
            layout->addWidget(combo);
170✔
2142
            
2143
            // Store the widget for parameter retrieval
2144
            _parameter_controls[param_key.toStdString()] = combo;
170✔
2145
            
2146
        } else if (param_desc->getUIHint() == "number") {
254✔
2147
            // Create spin box for numeric parameters
2148
            auto* spinbox = new QSpinBox();
84✔
2149
            spinbox->setObjectName(param_key);
84✔
2150
            
2151
            auto ui_props = param_desc->getUIProperties();
84✔
2152
            QString default_str = QString::fromStdString(ui_props["default"]);
252✔
2153
            QString min_str = QString::fromStdString(ui_props["min"]);
252✔
2154
            QString max_str = QString::fromStdString(ui_props["max"]);
252✔
2155
            
2156
            if (!min_str.isEmpty()) spinbox->setMinimum(min_str.toInt());
84✔
2157
            if (!max_str.isEmpty()) spinbox->setMaximum(max_str.toInt());
84✔
2158
            if (!default_str.isEmpty()) spinbox->setValue(default_str.toInt());
84✔
2159
            
2160
            spinbox->setToolTip(QString::fromStdString(param_desc->getDescription()));
84✔
2161
            layout->addWidget(spinbox);
84✔
2162
            
2163
            _parameter_controls[param_key.toStdString()] = spinbox;
84✔
2164
            
2165
        } else {
84✔
2166
            // Default to text input
2167
            auto* lineedit = new QLineEdit();
×
2168
            lineedit->setObjectName(param_key);
×
2169
            
2170
            auto ui_props = param_desc->getUIProperties();
×
2171
            QString default_value = QString::fromStdString(ui_props["default"]);
×
2172
            lineedit->setText(default_value);
×
2173
            
2174
            lineedit->setToolTip(QString::fromStdString(param_desc->getDescription()));
×
2175
            layout->addWidget(lineedit);
×
2176
            
2177
            _parameter_controls[param_key.toStdString()] = lineedit;
×
2178
        }
×
2179
    }
254✔
2180
    
2181
    return widget;
254✔
2182
}
2183

2184
std::map<std::string, std::string> TableDesignerWidget::getParameterValues(const QString& computer_name) const {
50✔
2185
    std::map<std::string, std::string> parameters;
50✔
2186
    
2187
    // Look for parameter controls with this computer name prefix
2188
    QString prefix = computer_name + "::";
50✔
2189
    
2190
    for (const auto& [key, widget] : _parameter_controls) {
150✔
2191
        QString key_str = QString::fromStdString(key);
100✔
2192
        if (key_str.startsWith(prefix)) {
100✔
2193
            QString param_name = key_str.mid(prefix.length());
6✔
2194
            
2195
            if (auto* combo = qobject_cast<QComboBox*>(widget)) {
6✔
2196
                parameters[param_name.toStdString()] = combo->currentText().toStdString();
×
2197
            } else if (auto* spinbox = qobject_cast<QSpinBox*>(widget)) {
6✔
2198
                parameters[param_name.toStdString()] = QString::number(spinbox->value()).toStdString();
6✔
2199
            } else if (auto* lineedit = qobject_cast<QLineEdit*>(widget)) {
×
2200
                parameters[param_name.toStdString()] = lineedit->text().toStdString();
×
2201
            }
2202
        }
6✔
2203
    }
100✔
2204
    
2205
    return parameters;
50✔
2206
}
50✔
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