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

paulmthompson / WhiskerToolbox / 17621333818

10 Sep 2025 02:29PM UTC coverage: 71.842% (+0.04%) from 71.805%
17621333818

push

github

paulmthompson
Merge branch 'main' of https://github.com/paulmthompson/WhiskerToolbox

510 of 568 new or added lines in 5 files covered. (89.79%)

224 existing lines in 3 files now uncovered.

37074 of 51605 relevant lines covered (71.84%)

1307.88 hits per line

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

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

4
#include "DataManager/AnalogTimeSeries/Analog_Time_Series.hpp"
5
#include "DataManager/DataManager.hpp"
6
#include "DataManager/DigitalTimeSeries/Digital_Event_Series.hpp"
7
#include "DataManager/DigitalTimeSeries/Digital_Interval_Series.hpp"
8
#include "DataManager/Lines/Line_Data.hpp"
9
#include "DataManager/Points/Point_Data.hpp"
10
#include "DataManager/utils/TableView/ComputerRegistry.hpp"
11
#include "DataManager/utils/TableView/TableEvents.hpp"
12
#include "DataManager/utils/TableView/TableRegistry.hpp"
13
#include "DataManager/utils/TableView/adapters/DataManagerExtension.h"
14
#include "DataManager/utils/TableView/computers/EventInIntervalComputer.h"
15
#include "DataManager/utils/TableView/computers/IntervalReductionComputer.h"
16
#include "DataManager/utils/TableView/core/TableViewBuilder.h"
17
#include "DataManager/utils/TableView/interfaces/IColumnComputer.h"
18
#include "DataManager/utils/TableView/interfaces/IRowSelector.h"
19
#include "DataManager/utils/TableView/transforms/PCATransform.hpp"
20
#include "TableInfoWidget.hpp"
21
#include "Collapsible_Widget/Section.hpp"
22
#include "TableViewerWidget/TableViewerWidget.hpp"
23
#include "TableTransformWidget.hpp"
24
#include "TableExportWidget.hpp"
25
#include "TableJSONWidget.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 <QTreeWidget>
39
#include <QTreeWidgetItem>
40
#include <QVBoxLayout>
41
#include <QTableView>
42

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

51
#include <algorithm>
52
#include <fstream>
53
#include <iomanip>
54
#include <limits>
55
#include <tuple>
56
#include <typeindex>
57
#include <vector>
58

59
TableDesignerWidget::TableDesignerWidget(std::shared_ptr<DataManager> data_manager, QWidget * parent)
16✔
60
    : QWidget(parent),
61
      ui(new Ui::TableDesignerWidget),
32✔
62
      _data_manager(std::move(data_manager)) {
16✔
63

64
    ui->setupUi(this);
16✔
65
    
66
    _parameter_widget = nullptr;
16✔
67
    _parameter_layout = nullptr;
16✔
68
    
69
    // Initialize table viewer widget for preview
70
    _table_viewer = new TableViewerWidget(this);
16✔
71
    
72
    // Add the table viewer widget to the preview layout
73
    ui->preview_layout->addWidget(_table_viewer);
16✔
74
    
75
    _preview_debounce_timer = new QTimer(this);
16✔
76
    _preview_debounce_timer->setSingleShot(true);
16✔
77
    _preview_debounce_timer->setInterval(150);
16✔
78
    connect(_preview_debounce_timer, &QTimer::timeout, this, &TableDesignerWidget::rebuildPreviewNow);
16✔
79

80
    _table_info_widget = new TableInfoWidget(this);
16✔
81
    _table_info_section = new Section(this, "Table Information");
16✔
82
    _table_info_section->setContentLayout(*new QVBoxLayout());
16✔
83
    _table_info_section->layout()->addWidget(_table_info_widget);
16✔
84
    _table_info_section->autoSetContentLayout();
16✔
85
    ui->main_layout->insertWidget(1, _table_info_section);
16✔
86

87
    // Hook save from table info widget
88
    connect(_table_info_widget, &TableInfoWidget::saveClicked, this, &TableDesignerWidget::onSaveTableInfo);
16✔
89

90
    // Connect table viewer signals for better integration
91
    connect(_table_viewer, &TableViewerWidget::rowScrolled, this, [this](size_t row_index) {
16✔
92
        // Optional: Could emit a signal or update status when user scrolls preview
93
        // For now, just ensure the table viewer is working as expected
94
        Q_UNUSED(row_index)
UNCOV
95
    });
×
96

97
    connectSignals();
16✔
98
    // Initialize UI to a clean state, then populate controls
99
    clearUI();
16✔
100
    refreshTableCombo();
16✔
101
    refreshRowDataSourceCombo();
16✔
102
    refreshComputersTree();
16✔
103

104
    // Insert Transform section
105
    _table_transform_widget = new TableTransformWidget(this);
16✔
106
    _table_transform_section = new Section(this, "Transforms");
16✔
107
    _table_transform_section->setContentLayout(*new QVBoxLayout());
16✔
108
    _table_transform_section->layout()->addWidget(_table_transform_widget);
16✔
109
    _table_transform_section->autoSetContentLayout();
16✔
110
    // Place after build_group (after preview)
111
    ui->main_layout->insertWidget( ui->main_layout->indexOf(ui->build_group) + 1, _table_transform_section );
16✔
112
    connect(_table_transform_widget, &TableTransformWidget::applyTransformClicked,
48✔
113
            this, &TableDesignerWidget::onApplyTransform);
32✔
114

115
    // Insert Export section
116
    _table_export_widget = new TableExportWidget(this);
16✔
117
    _table_export_section = new Section(this, "Export");
16✔
118
    _table_export_section->setContentLayout(*new QVBoxLayout());
16✔
119
    _table_export_section->layout()->addWidget(_table_export_widget);
16✔
120
    _table_export_section->autoSetContentLayout();
16✔
121
    ui->main_layout->insertWidget( ui->main_layout->indexOf(ui->build_group) + 2, _table_export_section );
16✔
122
    connect(_table_export_widget, &TableExportWidget::exportClicked,
48✔
123
            this, &TableDesignerWidget::onExportCsv);
32✔
124

125
    // Insert JSON section
126
    _table_json_widget = new TableJSONWidget(this);
16✔
127
    _table_json_section = new Section(this, "Table JSON Template");
16✔
128
    _table_json_section->setContentLayout(*new QVBoxLayout());
16✔
129
    _table_json_section->layout()->addWidget(_table_json_widget);
16✔
130
    _table_json_section->autoSetContentLayout();
16✔
131
    ui->main_layout->insertWidget( ui->main_layout->indexOf(ui->build_group) + 3, _table_json_section );
16✔
132
    connect(_table_json_widget, &TableJSONWidget::updateRequested, this, [this](QString const & jsonText){
16✔
133
        applyJsonTemplateToUI(jsonText);
7✔
134
    });
7✔
135

136
    // Add observer to automatically refresh dropdowns when DataManager changes
137
    if (_data_manager) {
16✔
138
        _data_manager->addObserver([this]() {
16✔
139
            refreshAllDataSources();
4✔
140
        });
4✔
141
    }
142

143
    qDebug() << "TableDesignerWidget initialized with TableViewerWidget for efficient pagination";
16✔
144
}
16✔
145

146
TableDesignerWidget::~TableDesignerWidget() {
16✔
147
    delete ui;
16✔
148
}
16✔
149

150
void TableDesignerWidget::refreshAllDataSources() {
4✔
151
    qDebug() << "Manually refreshing all data sources...";
4✔
152
    refreshRowDataSourceCombo();
4✔
153
    refreshComputersTree();
4✔
154

155
    // If we have a selected table, refresh its info
156
    if (!_current_table_id.isEmpty()) {
4✔
UNCOV
157
        loadTableInfo(_current_table_id);
×
158
    }
159
}
4✔
160

161

162
void TableDesignerWidget::connectSignals() {
16✔
163
    // Table selection signals
164
    connect(ui->table_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
48✔
165
            this, &TableDesignerWidget::onTableSelectionChanged);
32✔
166
    connect(ui->new_table_btn, &QPushButton::clicked,
48✔
167
            this, &TableDesignerWidget::onCreateNewTable);
32✔
168
    connect(ui->delete_table_btn, &QPushButton::clicked,
48✔
169
            this, &TableDesignerWidget::onDeleteTable);
32✔
170

171
    // Table info signals are connected via TableInfoWidget
172

173
    // Row source signals
174
    connect(ui->row_data_source_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
48✔
175
            this, &TableDesignerWidget::onRowDataSourceChanged);
32✔
176
    connect(ui->capture_range_spinbox, QOverload<int>::of(&QSpinBox::valueChanged),
48✔
177
            this, &TableDesignerWidget::onCaptureRangeChanged);
32✔
178
    connect(ui->interval_beginning_radio, &QRadioButton::toggled,
48✔
179
            this, &TableDesignerWidget::onIntervalSettingChanged);
32✔
180
    connect(ui->interval_end_radio, &QRadioButton::toggled,
48✔
181
            this, &TableDesignerWidget::onIntervalSettingChanged);
32✔
182
    connect(ui->interval_itself_radio, &QRadioButton::toggled,
48✔
183
            this, &TableDesignerWidget::onIntervalSettingChanged);
32✔
184

185
    // Column design signals (tree-based)
186
    connect(ui->computers_tree, &QTreeWidget::itemChanged,
48✔
187
            this, &TableDesignerWidget::onComputersTreeItemChanged);
32✔
188
    connect(ui->computers_tree, &QTreeWidget::itemChanged,
48✔
189
            this, &TableDesignerWidget::onComputersTreeItemEdited);
32✔
190

191
    // Build signals
192
    connect(ui->build_table_btn, &QPushButton::clicked,
48✔
193
            this, &TableDesignerWidget::onBuildTable);
32✔
194
    // Transform apply handled via TableTransformWidget
195
    // Export handled via TableExportWidget
196

197
    // Subscribe to DataManager table observer
198
    if (_data_manager) {
16✔
199
        auto token = _data_manager->addTableObserver([this](TableEvent const & ev) {
32✔
200
            switch (ev.type) {
26✔
201
                case TableEventType::Created:
12✔
202
                    this->onTableManagerTableCreated(QString::fromStdString(ev.tableId));
12✔
203
                    break;
12✔
UNCOV
204
                case TableEventType::Removed:
×
UNCOV
205
                    this->onTableManagerTableRemoved(QString::fromStdString(ev.tableId));
×
UNCOV
206
                    break;
×
207
                case TableEventType::InfoUpdated:
11✔
208
                    this->onTableManagerTableInfoUpdated(QString::fromStdString(ev.tableId));
11✔
209
                    break;
11✔
210
                case TableEventType::DataChanged:
3✔
211
                    // No direct UI change needed here for designer list
212
                    break;
3✔
213
            }
214
        });
42✔
215
        (void) token;// Optionally store and remove on dtor
216
    }
217
}
16✔
218

219
void TableDesignerWidget::onTableSelectionChanged() {
41✔
220
    int current_index = ui->table_combo->currentIndex();
41✔
221
    if (current_index < 0) {
41✔
222
        clearUI();
12✔
223
        return;
12✔
224
    }
225

226
    QString table_id = ui->table_combo->itemData(current_index).toString();
29✔
227
    if (table_id.isEmpty()) {
29✔
228
        clearUI();
16✔
229
        return;
16✔
230
    }
231

232
    _current_table_id = table_id;
13✔
233
    loadTableInfo(table_id);
13✔
234

235
    // Enable/disable controls
236
    ui->delete_table_btn->setEnabled(true);
13✔
237
    // Table info section is controlled separately
238
    ui->build_table_btn->setEnabled(true);
13✔
239
    if (auto gb = this->findChild<QGroupBox*>("row_source_group")) gb->setEnabled(true);
26✔
240
    if (auto gb = this->findChild<QGroupBox*>("column_design_group")) gb->setEnabled(true);
26✔
241
    // Enable save info within TableInfoWidget
242
    if (_table_info_section) _table_info_section->setEnabled(true);
13✔
243

244
    updateBuildStatus("Table selected: " + table_id);
13✔
245

246
    qDebug() << "Selected table:" << table_id;
13✔
247
}
29✔
248

UNCOV
249
void TableDesignerWidget::onCreateNewTable() {
×
UNCOV
250
    bool ok;
×
UNCOV
251
    QString name = QInputDialog::getText(this, "New Table", "Enter table name:", QLineEdit::Normal, "New Table", &ok);
×
252

UNCOV
253
    if (!ok || name.isEmpty()) {
×
UNCOV
254
        return;
×
255
    }
256

UNCOV
257
    auto * registry = _data_manager->getTableRegistry();
×
UNCOV
258
    if (!registry) { return; }
×
UNCOV
259
    auto table_id = registry->generateUniqueTableId("Table");
×
260

UNCOV
261
    if (registry->createTable(table_id, name.toStdString())) {
×
262
        // The combo will be refreshed by the signal handler
263
        // Set the new table as selected
UNCOV
264
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
265
            if (ui->table_combo->itemData(i).toString().toStdString() == table_id) {
×
266
                ui->table_combo->setCurrentIndex(i);
×
267
                break;
×
268
            }
269
        }
270
    } else {
UNCOV
271
        QMessageBox::warning(this, "Error", "Failed to create table with ID: " + QString::fromStdString(table_id));
×
272
    }
273
}
×
274

275
void TableDesignerWidget::onDeleteTable() {
×
UNCOV
276
    if (_current_table_id.isEmpty()) {
×
277
        return;
×
278
    }
279

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

UNCOV
284
    if (reply == QMessageBox::Yes) {
×
UNCOV
285
        auto * registry = _data_manager->getTableRegistry();
×
UNCOV
286
        if (registry && registry->removeTable(_current_table_id.toStdString())) {
×
287
            // The combo will be refreshed by the signal handler
UNCOV
288
            clearUI();
×
289
        } else {
UNCOV
290
            QMessageBox::warning(this, "Error", "Failed to delete table: " + _current_table_id);
×
291
        }
292
    }
293
}
294

295
void TableDesignerWidget::onRowDataSourceChanged() {
44✔
296
    QString selected = ui->row_data_source_combo->currentText();
44✔
297
    if (selected.isEmpty()) {
44✔
298
        ui->row_info_label->setText("No row source selected");
16✔
299
        return;
16✔
300
    }
301

302
    // Save the row source selection to the current table
303
    // Only save if we have a current table and we're not loading table info
304
    if (!_current_table_id.isEmpty() && _data_manager) {
28✔
305
        if (auto * reg = _data_manager->getTableRegistry()) {
8✔
306
            reg->updateTableRowSource(_current_table_id.toStdString(), selected.toStdString());
8✔
307
        }
308
    }
309

310
    // Update the info label
311
    updateRowInfoLabel(selected);
28✔
312

313
    // Update interval settings visibility
314
    updateIntervalSettingsVisibility();
28✔
315

316
    // Refresh computers tree since available computers depend on row selector type
317
    refreshComputersTree();
28✔
318

319
    qDebug() << "Row data source changed to:" << selected;
28✔
320
    triggerPreviewDebounced();
28✔
321
}
44✔
322

UNCOV
323
void TableDesignerWidget::onCaptureRangeChanged() {
×
324
    // Update the info label to reflect the new capture range
UNCOV
325
    QString selected = ui->row_data_source_combo->currentText();
×
UNCOV
326
    if (!selected.isEmpty()) {
×
UNCOV
327
        updateRowInfoLabel(selected);
×
328
    }
UNCOV
329
    triggerPreviewDebounced();
×
UNCOV
330
}
×
331

332
void TableDesignerWidget::onIntervalSettingChanged() {
2✔
333
    // Update the info label to reflect the new interval setting
334
    QString selected = ui->row_data_source_combo->currentText();
2✔
335
    if (!selected.isEmpty()) {
2✔
336
        updateRowInfoLabel(selected);
2✔
337
    }
338

339
    // Update capture range visibility based on interval setting
340
    updateIntervalSettingsVisibility();
2✔
341
    triggerPreviewDebounced();
2✔
342
}
4✔
343

344

345
void TableDesignerWidget::onBuildTable() {
×
346
    if (_current_table_id.isEmpty()) {
×
UNCOV
347
        updateBuildStatus("No table selected", true);
×
UNCOV
348
        return;
×
349
    }
350

UNCOV
351
    QString row_source = ui->row_data_source_combo->currentText();
×
UNCOV
352
    if (row_source.isEmpty()) {
×
UNCOV
353
        updateBuildStatus("No row data source selected", true);
×
UNCOV
354
        return;
×
355
    }
356

357
    // Get enabled column infos from the tree
UNCOV
358
    auto column_infos = getEnabledColumnInfos();
×
UNCOV
359
    if (column_infos.empty()) {
×
UNCOV
360
        updateBuildStatus("No computers enabled. Check boxes in the tree to enable computers.", true);
×
361
        return;
×
362
    }
363

364
    try {
365
        // Create the row selector
UNCOV
366
        auto row_selector = createRowSelector(row_source);
×
367
        if (!row_selector) {
×
368
            updateBuildStatus("Failed to create row selector", true);
×
369
            return;
×
370
        }
371

372
        // Get the data manager extension
UNCOV
373
        auto * reg = _data_manager->getTableRegistry();
×
374
        auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
×
375
        if (!data_manager_extension) {
×
376
            updateBuildStatus("DataManager extension not available", true);
×
377
            return;
×
378
        }
379

380
        // Create the TableViewBuilder
UNCOV
381
        TableViewBuilder builder(data_manager_extension);
×
382
        builder.setRowSelector(std::move(row_selector));
×
383

384
        // Add all enabled columns from the tree
385
        bool all_columns_valid = true;
×
UNCOV
386
        for (auto const & column_info: column_infos) {
×
UNCOV
387
            if (!reg->addColumnToBuilder(builder, column_info)) {
×
UNCOV
388
                updateBuildStatus(QString("Failed to create column: %1").arg(QString::fromStdString(column_info.name)), true);
×
389
                all_columns_valid = false;
×
390
                break;
×
391
            }
392
        }
393

UNCOV
394
        if (!all_columns_valid) {
×
UNCOV
395
            return;
×
396
        }
397

398
        // Build the table
UNCOV
399
        auto table_view = builder.build();
×
400

401
        // Store the built table in the TableManager and update table info with current columns
402
        if (reg) {
×
403
            // Update table info with current column configuration
404
            auto table_info = reg->getTableInfo(_current_table_id.toStdString());
×
405
            table_info.columns = column_infos;// Store the current enabled columns
×
406
            reg->updateTableInfo(_current_table_id.toStdString(),
×
407
                                 table_info.name, table_info.description);
408

409
            // Store the built table
410
            if (reg->storeBuiltTable(_current_table_id.toStdString(), std::move(table_view))) {
×
411
                updateBuildStatus(QString("Table built successfully with %1 columns!").arg(column_infos.size()));
×
UNCOV
412
                qDebug() << "Successfully built table:" << _current_table_id << "with" << column_infos.size() << "columns";
×
413
                // Populate JSON widget with the current configuration
NEW
414
                setJsonTemplateFromCurrentState();
×
415
            } else {
UNCOV
416
                updateBuildStatus("Failed to store built table", true);
×
417
            }
UNCOV
418
        } else {
×
UNCOV
419
            updateBuildStatus("Registry unavailable", true);
×
420
        }
421

422
    } catch (std::exception const & e) {
×
423
        updateBuildStatus(QString("Error building table: %1").arg(e.what()), true);
×
424
        qDebug() << "Exception during table building:" << e.what();
×
UNCOV
425
    }
×
UNCOV
426
}
×
427

428
bool TableDesignerWidget::buildTableFromTree() {
3✔
429
    // This is essentially the same as onBuildTable but returns success status
430
    if (_current_table_id.isEmpty()) {
3✔
UNCOV
431
        updateBuildStatus("No table selected", true);
×
432
        return false;
×
433
    }
434

435
    QString row_source = ui->row_data_source_combo->currentText();
3✔
436
    if (row_source.isEmpty()) {
3✔
437
        updateBuildStatus("No row data source selected", true);
×
UNCOV
438
        return false;
×
439
    }
440

441
    // Get enabled column infos from the tree
442
    auto column_infos = getEnabledColumnInfos();
3✔
443
    if (column_infos.empty()) {
3✔
444
        updateBuildStatus("No computers enabled. Check boxes in the tree to enable computers.", true);
×
UNCOV
445
        return false;
×
446
    }
447

448
    try {
449
        // Create the row selector
450
        auto row_selector = createRowSelector(row_source);
3✔
451
        if (!row_selector) {
3✔
UNCOV
452
            updateBuildStatus("Failed to create row selector", true);
×
UNCOV
453
            return false;
×
454
        }
455

456
        // Get the data manager extension
457
        auto * reg = _data_manager->getTableRegistry();
3✔
458
        auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
3✔
459
        if (!data_manager_extension) {
3✔
UNCOV
460
            updateBuildStatus("DataManager extension not available", true);
×
UNCOV
461
            return false;
×
462
        }
463

464
        // Create the TableViewBuilder
465
        TableViewBuilder builder(data_manager_extension);
3✔
466
        builder.setRowSelector(std::move(row_selector));
3✔
467

468
        // Add all enabled columns from the tree
469
        bool all_columns_valid = true;
3✔
470
        for (auto const & column_info: column_infos) {
8✔
471
            if (!reg->addColumnToBuilder(builder, column_info)) {
5✔
UNCOV
472
                updateBuildStatus(QString("Failed to create column: %1").arg(QString::fromStdString(column_info.name)), true);
×
UNCOV
473
                all_columns_valid = false;
×
UNCOV
474
                break;
×
475
            }
476
        }
477

478
        if (!all_columns_valid) {
3✔
479
            return false;
×
480
        }
481

482
        // Build the table
483
        auto table_view = builder.build();
3✔
484

485
        // Store the built table in the TableManager and update table info with current columns
486
        if (reg) {
3✔
487
            // Update table info with current column configuration
488
            auto table_info = reg->getTableInfo(_current_table_id.toStdString());
3✔
489
            table_info.columns = column_infos;// Store the current enabled columns
3✔
490
            reg->updateTableInfo(_current_table_id.toStdString(),
3✔
491
                                 table_info.name,
492
                                 table_info.description);
493

494
            // Store the built table
495
            if (reg->storeBuiltTable(_current_table_id.toStdString(), std::move(table_view))) {
3✔
496
                updateBuildStatus(QString("Table built successfully with %1 columns!").arg(column_infos.size()));
3✔
497
                qDebug() << "Successfully built table:" << _current_table_id << "with" << column_infos.size() << "columns";
3✔
498
                // Populate JSON widget with the current configuration as well
499
                setJsonTemplateFromCurrentState();
3✔
500
                return true;
3✔
501
            } else {
UNCOV
502
                updateBuildStatus("Failed to store built table", true);
×
UNCOV
503
                return false;
×
504
            }
505
        } else {
3✔
UNCOV
506
            updateBuildStatus("Registry unavailable", true);
×
UNCOV
507
            return false;
×
508
        }
509

510
    } catch (std::exception const & e) {
3✔
UNCOV
511
        updateBuildStatus(QString("Error building table: %1").arg(e.what()), true);
×
UNCOV
512
        qDebug() << "Exception during table building:" << e.what();
×
UNCOV
513
        return false;
×
UNCOV
514
    }
×
515
}
3✔
516

UNCOV
517
void TableDesignerWidget::onApplyTransform() {
×
UNCOV
518
    if (_current_table_id.isEmpty() || !_data_manager) {
×
UNCOV
519
        updateBuildStatus("No base table selected", true);
×
UNCOV
520
        return;
×
521
    }
522

523
    // Fetch the built base table
UNCOV
524
    auto * reg = _data_manager->getTableRegistry();
×
UNCOV
525
    if (!reg) {
×
526
        updateBuildStatus("Registry unavailable", true);
×
527
        return;
×
528
    }
UNCOV
529
    auto base_view = reg->getBuiltTable(_current_table_id.toStdString());
×
UNCOV
530
    if (!base_view) {
×
531
        updateBuildStatus("Build the base table first", true);
×
532
        return;
×
533
    }
534

535
    // Currently only PCA option is exposed
UNCOV
536
    QString transform = _table_transform_widget ? _table_transform_widget->getTransformType() : QString();
×
537
    if (transform != "PCA") {
×
538
        updateBuildStatus("Unsupported transform", true);
×
539
        return;
×
540
    }
541

542
    // Configure PCA
UNCOV
543
    PCAConfig cfg;
×
544
    cfg.center = _table_transform_widget && _table_transform_widget->isCenterEnabled();
×
545
    cfg.standardize = _table_transform_widget && _table_transform_widget->isStandardizeEnabled();
×
546
    if (_table_transform_widget) {
×
547
        for (auto const & s: _table_transform_widget->getIncludeColumns()) cfg.include.push_back(s);
×
UNCOV
548
        for (auto const & s: _table_transform_widget->getExcludeColumns()) cfg.exclude.push_back(s);
×
549
    }
550

551
    try {
552
        PCATransform pca(cfg);
×
UNCOV
553
        TableView derived = pca.apply(*base_view);
×
554

555
        // Determine output id/name
556
        QString out_name = _table_transform_widget ? _table_transform_widget->getOutputName().trimmed() : QString();
×
557
        if (out_name.isEmpty()) {
×
558
            QString base = _table_info_widget ? _table_info_widget->getName() : QString();
×
559
            out_name = base.isEmpty() ? QString("(PCA)") : QString("%1 (PCA)").arg(base);
×
UNCOV
560
        }
×
561

UNCOV
562
        std::string out_id = reg->generateUniqueTableId((_current_table_id + "_pca").toStdString());
×
563
        if (!reg->createTable(out_id, out_name.toStdString())) {
×
564
            reg->updateTableInfo(out_id, out_name.toStdString(), "");
×
565
        }
566
        if (reg->storeBuiltTable(out_id, std::move(derived))) {
×
567
            updateBuildStatus(QString("Created transformed table: %1").arg(out_name));
×
568
            refreshTableCombo();
×
569
        } else {
UNCOV
570
            updateBuildStatus("Failed to store transformed table", true);
×
571
        }
572
    } catch (std::exception const & e) {
×
573
        updateBuildStatus(QString("Transform failed: %1").arg(e.what()), true);
×
UNCOV
574
    }
×
UNCOV
575
}
×
576

577
std::vector<std::string> TableDesignerWidget::parseCommaSeparatedList(QString const & text) const {
×
578
    std::vector<std::string> out;
×
579
    for (QString s: text.split(",", Qt::SkipEmptyParts)) {
×
580
        s = s.trimmed();
×
UNCOV
581
        if (!s.isEmpty()) out.push_back(s.toStdString());
×
582
    }
×
583
    return out;
×
584
}
×
585

586
void TableDesignerWidget::onExportCsv() {
×
587
    if (_current_table_id.isEmpty() || !_data_manager) {
×
588
        updateBuildStatus("No table selected", true);
×
UNCOV
589
        return;
×
590
    }
591

592
    auto * reg = _data_manager->getTableRegistry();
×
593
    if (!reg) {
×
594
        updateBuildStatus("Registry unavailable", true);
×
595
        return;
×
596
    }
597
    auto view = reg->getBuiltTable(_current_table_id.toStdString());
×
598
    if (!view) {
×
599
        updateBuildStatus("Build the table first", true);
×
600
        return;
×
601
    }
602

603
    QString filename = promptSaveCsvFilename();
×
604
    if (filename.isEmpty()) return;
×
UNCOV
605
    if (!filename.endsWith(".csv", Qt::CaseInsensitive)) filename += ".csv";
×
606

607
    // CSV options from export widget
608
    QString delimiter = _table_export_widget ? _table_export_widget->getDelimiterText() : "Comma";
×
609
    QString lineEnding = _table_export_widget ? _table_export_widget->getLineEndingText() : "LF (\\n)";
×
UNCOV
610
    int precision = _table_export_widget ? _table_export_widget->getPrecision() : 3;
×
UNCOV
611
    bool includeHeader = _table_export_widget && _table_export_widget->isHeaderIncluded();
×
612

613
    std::string delim = ",";
×
614
    if (delimiter == "Space") delim = " ";
×
615
    else if (delimiter == "Tab")
×
UNCOV
616
        delim = "\t";
×
617
    std::string eol = "\n";
×
618
    if (lineEnding.startsWith("CRLF")) eol = "\r\n";
×
619

620
    try {
UNCOV
621
        std::ofstream file(filename.toStdString());
×
UNCOV
622
        if (!file.is_open()) {
×
623
            updateBuildStatus("Could not open file for writing", true);
×
624
            return;
×
625
        }
UNCOV
626
        file << std::fixed << std::setprecision(precision);
×
627

628
        auto names = view->getColumnNames();
×
629
        if (includeHeader) {
×
630
            for (size_t i = 0; i < names.size(); ++i) {
×
631
                if (i > 0) file << delim;
×
UNCOV
632
                file << names[i];
×
633
            }
634
            file << eol;
×
635
        }
636
        size_t rows = view->getRowCount();
×
637
        for (size_t r = 0; r < rows; ++r) {
×
638
            for (size_t c = 0; c < names.size(); ++c) {
×
UNCOV
639
                if (c > 0) file << delim;
×
NEW
640
                bool wrote = false;
×
641
                // Try known scalar types in order
642
                try {
643
                    auto const & vals = view->getColumnValues<double>(names[c].c_str());
×
NEW
644
                    if (r < vals.size()) { file << vals[r]; wrote = true; }
×
NEW
645
                } catch (...) {}
×
NEW
646
                if (!wrote) {
×
647
                    try {
NEW
648
                        auto const & vals = view->getColumnValues<int>(names[c].c_str());
×
NEW
649
                        if (r < vals.size()) { file << vals[r]; wrote = true; }
×
NEW
650
                    } catch (...) {}
×
651
                }
NEW
652
                if (!wrote) {
×
653
                    try {
NEW
654
                        auto const & vals = view->getColumnValues<int64_t>(names[c].c_str());
×
NEW
655
                        if (r < vals.size()) { file << vals[r]; wrote = true; }
×
NEW
656
                    } catch (...) {}
×
657
                }
NEW
658
                if (!wrote) {
×
659
                    try {
NEW
660
                        auto const & vals = view->getColumnValues<bool>(names[c].c_str());
×
NEW
661
                        if (r < vals.size()) { file << (vals[r] ? 1 : 0); wrote = true; }
×
NEW
662
                    } catch (...) {}
×
663
                }
NEW
664
                if (!wrote) file << "NaN";
×
665
            }
666
            file << eol;
×
667
        }
668
        file.close();
×
669
        updateBuildStatus(QString("Exported CSV: %1").arg(filename));
×
UNCOV
670
    } catch (std::exception const & e) {
×
671
        updateBuildStatus(QString("Export failed: %1").arg(e.what()), true);
×
UNCOV
672
    }
×
673
}
×
674

675
QString TableDesignerWidget::promptSaveCsvFilename() const {
×
676
    return QFileDialog::getSaveFileName(const_cast<TableDesignerWidget *>(this), "Export Table to CSV", QString(), "CSV Files (*.csv)");
×
677
}
678

UNCOV
679
void TableDesignerWidget::onSaveTableInfo() {
×
680
    if (_current_table_id.isEmpty()) {
×
681
        return;
×
682
    }
683

UNCOV
684
    QString name = _table_info_widget ? _table_info_widget->getName() : QString();
×
685
    QString description = _table_info_widget ? _table_info_widget->getDescription() : QString();
×
686

687
    if (name.isEmpty()) {
×
UNCOV
688
        QMessageBox::warning(this, "Error", "Table name cannot be empty");
×
689
        return;
×
690
    }
691

692
    if (auto * reg = _data_manager->getTableRegistry(); reg && reg->updateTableInfo(_current_table_id.toStdString(), name.toStdString(), description.toStdString())) {
×
693
        updateBuildStatus("Table information saved");
×
694
        // Refresh the combo to show updated name
695
        refreshTableCombo();
×
696
        // Restore selection
697
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
698
            if (ui->table_combo->itemData(i).toString() == _current_table_id) {
×
699
                ui->table_combo->setCurrentIndex(i);
×
UNCOV
700
                break;
×
701
            }
702
        }
703
    } else {
UNCOV
704
        QMessageBox::warning(this, "Error", "Failed to save table information");
×
705
    }
706
}
×
707

708
void TableDesignerWidget::onTableManagerTableCreated(QString const & table_id) {
12✔
709
    refreshTableCombo();
12✔
710
    qDebug() << "Table created signal received:" << table_id;
12✔
711
}
12✔
712

713
void TableDesignerWidget::onTableManagerTableRemoved(QString const & table_id) {
×
UNCOV
714
    refreshTableCombo();
×
UNCOV
715
    if (_current_table_id == table_id) {
×
716
        _current_table_id.clear();
×
717
        clearUI();
×
718
    }
UNCOV
719
    qDebug() << "Table removed signal received:" << table_id;
×
UNCOV
720
}
×
721

722
void TableDesignerWidget::onTableManagerTableInfoUpdated(QString const & table_id) {
11✔
723
    if (_current_table_id == table_id && !_loading_column_configuration) {
11✔
724
        loadTableInfo(table_id);
11✔
725
    }
726
    qDebug() << "Table info updated signal received:" << table_id;
11✔
727
}
11✔
728

729
void TableDesignerWidget::refreshTableCombo() {
28✔
730
    ui->table_combo->clear();
28✔
731

732
    auto * reg = _data_manager->getTableRegistry();
28✔
733
    auto table_infos = reg ? reg->getAllTableInfo() : std::vector<TableInfo>{};
28✔
734
    for (auto const & info: table_infos) {
43✔
735
        ui->table_combo->addItem(QString::fromStdString(info.name), QString::fromStdString(info.id));
15✔
736
    }
737

738
    if (ui->table_combo->count() == 0) {
28✔
739
        ui->table_combo->addItem("(No tables available)", "");
16✔
740
    }
741
}
56✔
742

743
void TableDesignerWidget::refreshRowDataSourceCombo() {
20✔
744
    ui->row_data_source_combo->clear();
20✔
745

746
    if (!_data_manager) {
20✔
UNCOV
747
        qDebug() << "refreshRowDataSourceCombo: No table manager";
×
UNCOV
748
        return;
×
749
    }
750

751
    auto data_sources = getAvailableDataSources();
20✔
752
    qDebug() << "refreshRowDataSourceCombo: Found" << data_sources.size() << "data sources:" << data_sources;
20✔
753

754
    for (QString const & source: data_sources) {
206✔
755
        // Only include valid row sources in this combo
756
        if (source.startsWith("TimeFrame: ") || source.startsWith("Events: ") || source.startsWith("Intervals: ")) {
186✔
757
            ui->row_data_source_combo->addItem(source);
146✔
758
        }
759
    }
760

761
    if (ui->row_data_source_combo->count() == 0) {
20✔
UNCOV
762
        ui->row_data_source_combo->addItem("(No data sources available)");
×
UNCOV
763
        qDebug() << "refreshRowDataSourceCombo: No data sources available";
×
764
    }
765
}
20✔
766

767

768
void TableDesignerWidget::loadTableInfo(QString const & table_id) {
24✔
769
    if (table_id.isEmpty() || !_data_manager) {
24✔
UNCOV
770
        clearUI();
×
UNCOV
771
        return;
×
772
    }
773

774
    auto * reg = _data_manager->getTableRegistry();
24✔
775
    auto info = reg ? reg->getTableInfo(table_id.toStdString()) : TableInfo{};
24✔
776
    if (info.id.empty()) {
24✔
UNCOV
777
        clearUI();
×
UNCOV
778
        return;
×
779
    }
780

781
    // Load table information
782
    if (_table_info_widget) {
24✔
783
        _table_info_widget->setName(QString::fromStdString(info.name));
24✔
784
        _table_info_widget->setDescription(QString::fromStdString(info.description));
24✔
785
    }
786

787
    // Load row source if available
788
    if (!info.rowSourceName.empty()) {
24✔
789
        int row_index = ui->row_data_source_combo->findText(QString::fromStdString(info.rowSourceName));
12✔
790
        if (row_index >= 0) {
12✔
791
            // Block signals to prevent circular dependency when loading table info
792
            ui->row_data_source_combo->blockSignals(true);
12✔
793
            ui->row_data_source_combo->setCurrentIndex(row_index);
12✔
794
            ui->row_data_source_combo->blockSignals(false);
12✔
795

796
            // Manually update the info label without triggering the signal handler
797
            updateRowInfoLabel(QString::fromStdString(info.rowSourceName));
12✔
798

799
            // Update interval settings visibility
800
            updateIntervalSettingsVisibility();
12✔
801

802
            // Since signals were blocked, this will ensure the tree is refreshed
803
            // when the computers tree is populated later in this function
804
        }
805
    }
806

807
    // Clear old column list (deprecated)
808
    // The computers tree will be populated based on available data sources
809
    refreshComputersTree();
24✔
810

811
    updateBuildStatus(QString("Loaded table: %1").arg(QString::fromStdString(info.name)));
24✔
812
    triggerPreviewDebounced();
24✔
813
}
24✔
814

815
void TableDesignerWidget::clearUI() {
44✔
816
    _current_table_id.clear();
44✔
817

818
    // Clear table info
819
    if (_table_info_widget) {
44✔
820
        _table_info_widget->setName("");
44✔
821
        _table_info_widget->setDescription("");
44✔
822
    }
823

824
    // Clear row source
825
    ui->row_data_source_combo->setCurrentIndex(-1);
44✔
826
    ui->row_info_label->setText("No row source selected");
44✔
827

828
    // Reset capture range and interval settings
829
    setCaptureRange(30000);// Default value
44✔
830
    if (ui->interval_beginning_radio) {
44✔
831
        ui->interval_beginning_radio->setChecked(true);
44✔
832
    }
833
    if (ui->interval_itself_radio) {
44✔
834
        ui->interval_itself_radio->setChecked(false);
44✔
835
    }
836
    if (ui->interval_settings_group) {
44✔
837
        ui->interval_settings_group->setVisible(false);
44✔
838
    }
839

840
    // Clear computers tree
841
    if (ui->computers_tree) {
44✔
842
        ui->computers_tree->clear();
44✔
843
    }
844

845
    // Disable controls
846
    ui->delete_table_btn->setEnabled(false);
44✔
847
    // Table info section is controlled separately
848
    ui->build_table_btn->setEnabled(false);
44✔
849
    if (auto gb = this->findChild<QGroupBox*>("row_source_group")) gb->setEnabled(false);
88✔
850
    if (auto gb = this->findChild<QGroupBox*>("column_design_group")) gb->setEnabled(false);
88✔
851
    if (_table_info_section) _table_info_section->setEnabled(false);
44✔
852

853
    updateBuildStatus("No table selected");
44✔
854
    if (_table_viewer) _table_viewer->clearTable();
44✔
855
}
44✔
856

857
void TableDesignerWidget::updateBuildStatus(QString const & message, bool is_error) {
84✔
858
    ui->build_status_label->setText(message);
84✔
859

860
    if (is_error) {
84✔
UNCOV
861
        ui->build_status_label->setStyleSheet("QLabel { color: red; font-weight: bold; }");
×
862
    } else {
863
        ui->build_status_label->setStyleSheet("QLabel { color: green; }");
84✔
864
    }
865
}
84✔
866

867
QStringList TableDesignerWidget::getAvailableDataSources() const {
96✔
868
    QStringList sources;
96✔
869

870
    if (!_data_manager) {
96✔
UNCOV
871
        qDebug() << "getAvailableDataSources: No table manager";
×
UNCOV
872
        return sources;
×
873
    }
874

875
    auto * reg = _data_manager->getTableRegistry();
96✔
876
    auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
96✔
877
    if (!data_manager_extension) {
96✔
UNCOV
878
        qDebug() << "getAvailableDataSources: No data manager extension";
×
UNCOV
879
        return sources;
×
880
    }
881

882
    if (!_data_manager) {
96✔
UNCOV
883
        qDebug() << "getAvailableDataSources: No data manager";
×
UNCOV
884
        return sources;
×
885
    }
886

887
    // Add TimeFrame keys as potential row sources
888
    // TimeFrames can define intervals for analysis
889
    auto timeframe_keys = _data_manager->getTimeFrameKeys();
96✔
890
    qDebug() << "getAvailableDataSources: TimeFrame keys:" << timeframe_keys.size();
96✔
891
    for (auto const & key: timeframe_keys) {
489✔
892
        QString source = QString("TimeFrame: %1").arg(QString::fromStdString(key.str()));
393✔
893
        sources << source;
393✔
894
        qDebug() << "  Added TimeFrame:" << source;
393✔
895
    }
393✔
896

897
    // Add DigitalEventSeries keys as potential row sources
898
    // Events can be used to define analysis windows or timestamps
899
    auto event_keys = _data_manager->getKeys<DigitalEventSeries>();
96✔
900
    qDebug() << "getAvailableDataSources: Event keys:" << event_keys.size();
96✔
901
    for (auto const & key: event_keys) {
291✔
902
        QString source = QString("Events: %1").arg(QString::fromStdString(key));
195✔
903
        sources << source;
195✔
904
        qDebug() << "  Added Events:" << source;
195✔
905
    }
195✔
906

907
    // Add DigitalIntervalSeries keys as potential row sources
908
    // Intervals directly define analysis windows
909
    auto interval_keys = _data_manager->getKeys<DigitalIntervalSeries>();
96✔
910
    qDebug() << "getAvailableDataSources: Interval keys:" << interval_keys.size();
96✔
911
    for (auto const & key: interval_keys) {
207✔
912
        QString source = QString("Intervals: %1").arg(QString::fromStdString(key));
111✔
913
        sources << source;
111✔
914
        qDebug() << "  Added Intervals:" << source;
111✔
915
    }
111✔
916

917
    // Add AnalogTimeSeries keys as data sources (for computers; not row selectors)
918
    auto analog_keys = _data_manager->getKeys<AnalogTimeSeries>();
96✔
919
    qDebug() << "getAvailableDataSources: Analog keys:" << analog_keys.size();
96✔
920
    for (auto const & key: analog_keys) {
288✔
921
        QString source = QString("analog:%1").arg(QString::fromStdString(key));
192✔
922
        sources << source;
192✔
923
        qDebug() << "  Added Analog:" << source;
192✔
924
    }
192✔
925

926
    qDebug() << "getAvailableDataSources: Total sources found:" << sources.size();
96✔
927

928
    return sources;
96✔
929
}
96✔
930

931
std::pair<std::optional<DataSourceVariant>, RowSelectorType>
932
TableDesignerWidget::createDataSourceVariant(QString const & data_source_string,
705✔
933
                                             std::shared_ptr<DataManagerExtension> data_manager_extension) const {
934
    std::optional<DataSourceVariant> result;
705✔
935
    RowSelectorType row_selector_type = RowSelectorType::IntervalBased;
705✔
936

937
    if (data_source_string.startsWith("TimeFrame: ")) {
705✔
938
        // TimeFrames are used with TimestampSelector for rows; no concrete data source needed
939
        row_selector_type = RowSelectorType::Timestamp;
311✔
940

941
    } else if (data_source_string.startsWith("Events: ")) {
394✔
942
        QString source_name = data_source_string.mid(8);// Remove "Events: " prefix
154✔
943
        // Event-based computers in the registry operate with interval rows
944
        row_selector_type = RowSelectorType::IntervalBased;
154✔
945

946
        if (auto event_source = data_manager_extension->getEventSource(source_name.toStdString())) {
154✔
947
            result = event_source;
154✔
948
        }
154✔
949

950
    } else if (data_source_string.startsWith("Intervals: ")) {
394✔
951
        QString source_name = data_source_string.mid(11);// Remove "Intervals: " prefix
88✔
952
        row_selector_type = RowSelectorType::IntervalBased;
88✔
953

954
        if (auto interval_source = data_manager_extension->getIntervalSource(source_name.toStdString())) {
88✔
955
            result = interval_source;
88✔
956
        }
88✔
957
    } else if (data_source_string.startsWith("analog:")) {
240✔
958
        QString source_name = data_source_string.mid(7);// Remove "analog:" prefix
152✔
959
        row_selector_type = RowSelectorType::IntervalBased;
152✔
960

961
        if (auto analog_source = data_manager_extension->getAnalogSource(source_name.toStdString())) {
152✔
962
            result = analog_source;
152✔
963
        }
152✔
964
    }
152✔
965

966
    return {result, row_selector_type};
1,410✔
967
}
705✔
968

969
void TableDesignerWidget::updateRowInfoLabel(QString const & selected_source) {
42✔
970
    if (selected_source.isEmpty()) {
42✔
UNCOV
971
        ui->row_info_label->setText("No row source selected");
×
UNCOV
972
        return;
×
973
    }
974

975
    // Parse the selected source to get type and name
976
    QString source_type;
42✔
977
    QString source_name;
42✔
978

979
    if (selected_source.startsWith("TimeFrame: ")) {
42✔
980
        source_type = "TimeFrame";
20✔
981
        source_name = selected_source.mid(11);// Remove "TimeFrame: " prefix
20✔
982
    } else if (selected_source.startsWith("Events: ")) {
22✔
UNCOV
983
        source_type = "Events";
×
UNCOV
984
        source_name = selected_source.mid(8);// Remove "Events: " prefix
×
985
    } else if (selected_source.startsWith("Intervals: ")) {
22✔
986
        source_type = "Intervals";
22✔
987
        source_name = selected_source.mid(11);// Remove "Intervals: " prefix
22✔
988
    }
989

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

993
    if (!_data_manager) {
42✔
UNCOV
994
        ui->row_info_label->setText(info_text);
×
UNCOV
995
        return;
×
996
    }
997

998
    auto * reg3 = _data_manager->getTableRegistry();
42✔
999
    auto data_manager_extension = reg3 ? reg3->getDataManagerExtension() : nullptr;
42✔
1000
    if (!data_manager_extension) {
42✔
UNCOV
1001
        ui->row_info_label->setText(info_text);
×
UNCOV
1002
        return;
×
1003
    }
1004

1005
    auto const source_name_str = source_name.toStdString();
42✔
1006

1007
    // Add specific information based on source type
1008
    if (source_type == "TimeFrame") {
42✔
1009
        auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
20✔
1010
        if (timeframe) {
20✔
1011
            info_text += QString(" - %1 time points").arg(timeframe->getTotalFrameCount());
20✔
1012
        }
1013
    } else if (source_type == "Events") {
42✔
UNCOV
1014
        auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
UNCOV
1015
        if (event_series) {
×
UNCOV
1016
            auto events = event_series->getEventSeries();
×
UNCOV
1017
            info_text += QString(" - %1 events").arg(events.size());
×
UNCOV
1018
        }
×
1019
    } else if (source_type == "Intervals") {
22✔
1020
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
22✔
1021
        if (interval_series) {
22✔
1022
            auto intervals = interval_series->getDigitalIntervalSeries();
22✔
1023
            info_text += QString(" - %1 intervals").arg(intervals.size());
22✔
1024

1025
            // Add capture range and interval setting information
1026
            if (isIntervalItselfSelected()) {
22✔
1027
                info_text += QString("\nUsing intervals as-is (no capture range)");
2✔
1028
            } else {
1029
                int capture_range = getCaptureRange();
20✔
1030
                QString interval_point = isIntervalBeginningSelected() ? "beginning" : "end";
20✔
1031
                info_text += QString("\nCapture range: ±%1 samples around %2 of intervals").arg(capture_range).arg(interval_point);
20✔
1032
            }
20✔
1033
        }
22✔
1034
    }
22✔
1035

1036
    ui->row_info_label->setText(info_text);
42✔
1037
}
42✔
1038

1039
std::unique_ptr<IRowSelector> TableDesignerWidget::createRowSelector(QString const & row_source) {
20✔
1040
    // Parse the row source to get type and name
1041
    QString source_type;
20✔
1042
    QString source_name;
20✔
1043

1044
    if (row_source.startsWith("TimeFrame: ")) {
20✔
UNCOV
1045
        source_type = "TimeFrame";
×
UNCOV
1046
        source_name = row_source.mid(11);// Remove "TimeFrame: " prefix
×
1047
    } else if (row_source.startsWith("Events: ")) {
20✔
UNCOV
1048
        source_type = "Events";
×
UNCOV
1049
        source_name = row_source.mid(8);// Remove "Events: " prefix
×
1050
    } else if (row_source.startsWith("Intervals: ")) {
20✔
1051
        source_type = "Intervals";
20✔
1052
        source_name = row_source.mid(11);// Remove "Intervals: " prefix
20✔
1053
    } else {
1054
        qDebug() << "Unknown row source format:" << row_source;
×
1055
        return nullptr;
×
1056
    }
1057

1058
    auto const source_name_str = source_name.toStdString();
20✔
1059

1060
    try {
1061
        if (source_type == "TimeFrame") {
20✔
1062
            // Create IntervalSelector using TimeFrame
UNCOV
1063
            auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
×
UNCOV
1064
            if (!timeframe) {
×
UNCOV
1065
                qDebug() << "TimeFrame not found:" << source_name;
×
UNCOV
1066
                return nullptr;
×
1067
            }
1068

1069
            // Use timestamps to select all rows
UNCOV
1070
            std::vector<TimeFrameIndex> timestamps;
×
UNCOV
1071
            for (int64_t i = 0; i < timeframe->getTotalFrameCount(); ++i) {
×
UNCOV
1072
                timestamps.push_back(TimeFrameIndex(i));
×
1073
            }
UNCOV
1074
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe);
×
1075

1076
        } else if (source_type == "Events") {
20✔
1077
            // Create TimestampSelector using DigitalEventSeries
UNCOV
1078
            auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
UNCOV
1079
            if (!event_series) {
×
UNCOV
1080
                qDebug() << "DigitalEventSeries not found:" << source_name;
×
UNCOV
1081
                return nullptr;
×
1082
            }
1083

UNCOV
1084
            auto events = event_series->getEventSeries();
×
1085
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
×
1086
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
×
UNCOV
1087
            if (!timeframe_obj) {
×
UNCOV
1088
                qDebug() << "TimeFrame not found for events:" << timeframe_key.str();
×
UNCOV
1089
                return nullptr;
×
1090
            }
1091

1092
            // Convert events to TimeFrameIndex
UNCOV
1093
            std::vector<TimeFrameIndex> timestamps;
×
UNCOV
1094
            for (auto const & event: events) {
×
UNCOV
1095
                timestamps.push_back(TimeFrameIndex(static_cast<int64_t>(event)));
×
1096
            }
1097

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

1100
        } else if (source_type == "Intervals") {
20✔
1101
            // Create IntervalSelector using DigitalIntervalSeries with capture range
1102
            auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
20✔
1103
            if (!interval_series) {
20✔
UNCOV
1104
                qDebug() << "DigitalIntervalSeries not found:" << source_name;
×
UNCOV
1105
                return nullptr;
×
1106
            }
1107

1108
            auto intervals = interval_series->getDigitalIntervalSeries();
20✔
1109
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
20✔
1110
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
20✔
1111
            if (!timeframe_obj) {
20✔
UNCOV
1112
                qDebug() << "TimeFrame not found for intervals:" << timeframe_key.str();
×
UNCOV
1113
                return nullptr;
×
1114
            }
1115

1116
            // Get capture range and interval setting
1117
            int capture_range = getCaptureRange();
20✔
1118
            bool use_beginning = isIntervalBeginningSelected();
20✔
1119
            bool use_interval_itself = isIntervalItselfSelected();
20✔
1120

1121
            // Create intervals based on the selected option
1122
            std::vector<TimeFrameInterval> tf_intervals;
20✔
1123
            for (auto const & interval: intervals) {
99✔
1124
                if (use_interval_itself) {
79✔
1125
                    // Use the interval as-is
1126
                    tf_intervals.emplace_back(TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
3✔
1127
                } else {
1128
                    // Determine the reference point (beginning or end of interval)
1129
                    int64_t reference_point;
1130
                    if (use_beginning) {
76✔
1131
                        reference_point = interval.start;
76✔
1132
                    } else {
UNCOV
1133
                        reference_point = interval.end;
×
1134
                    }
1135

1136
                    // Create a new interval around the reference point
1137
                    int64_t start_point = reference_point - capture_range;
76✔
1138
                    int64_t end_point = reference_point + capture_range;
76✔
1139

1140
                    // Ensure bounds are within the timeframe
1141
                    start_point = std::max(start_point, int64_t(0));
76✔
1142
                    end_point = std::min(end_point, static_cast<int64_t>(timeframe_obj->getTotalFrameCount() - 1));
76✔
1143

1144
                    tf_intervals.emplace_back(TimeFrameIndex(start_point), TimeFrameIndex(end_point));
76✔
1145
                }
1146
            }
1147

1148
            return std::make_unique<IntervalSelector>(std::move(tf_intervals), timeframe_obj);
20✔
1149
        }
20✔
1150

UNCOV
1151
    } catch (std::exception const & e) {
×
UNCOV
1152
        qDebug() << "Exception creating row selector:" << e.what();
×
UNCOV
1153
        return nullptr;
×
UNCOV
1154
    }
×
1155

UNCOV
1156
    qDebug() << "Unsupported row source type:" << source_type;
×
UNCOV
1157
    return nullptr;
×
1158
}
20✔
1159

UNCOV
1160
bool TableDesignerWidget::addColumnToBuilder(TableViewBuilder & builder, ColumnInfo const & column_info) {
×
1161
    // Use the simplified TableRegistry method that handles all the type checking internally
UNCOV
1162
    auto * reg = _data_manager->getTableRegistry();
×
UNCOV
1163
    if (!reg) {
×
UNCOV
1164
        qDebug() << "TableRegistry not available";
×
UNCOV
1165
        return false;
×
1166
    }
1167

UNCOV
1168
    bool success = reg->addColumnToBuilder(builder, column_info);
×
UNCOV
1169
    if (!success) {
×
1170
        qDebug() << "Failed to add column to builder:" << QString::fromStdString(column_info.name);
×
1171
    }
1172

UNCOV
1173
    return success;
×
1174
}
1175

1176
void TableDesignerWidget::updateIntervalSettingsVisibility() {
42✔
1177
    if (!ui->interval_settings_group) {
42✔
UNCOV
1178
        return;
×
1179
    }
1180

1181
    QString selected_key = ui->row_data_source_combo->currentText();
42✔
1182
    if (selected_key.isEmpty()) {
42✔
UNCOV
1183
        ui->interval_settings_group->setVisible(false);
×
UNCOV
1184
        if (ui->capture_range_spinbox) {
×
UNCOV
1185
            ui->capture_range_spinbox->setEnabled(false);
×
1186
        }
UNCOV
1187
        return;
×
1188
    }
1189

1190
    if (!_data_manager) {
42✔
1191
        ui->interval_settings_group->setVisible(false);
×
UNCOV
1192
        if (ui->capture_range_spinbox) {
×
1193
            ui->capture_range_spinbox->setEnabled(false);
×
1194
        }
UNCOV
1195
        return;
×
1196
    }
1197

1198
    // Check if the selected source is an interval series
1199
    if (selected_key.startsWith("Intervals: ")) {
42✔
1200
        ui->interval_settings_group->setVisible(true);
22✔
1201

1202
        // Enable/disable capture range based on interval setting
1203
        if (ui->capture_range_spinbox) {
22✔
1204
            bool use_interval_itself = isIntervalItselfSelected();
22✔
1205
            ui->capture_range_spinbox->setEnabled(!use_interval_itself);
22✔
1206
        }
1207
    } else {
1208
        ui->interval_settings_group->setVisible(false);
20✔
1209
        if (ui->capture_range_spinbox) {
20✔
1210
            ui->capture_range_spinbox->setEnabled(false);
20✔
1211
        }
1212
    }
1213
}
42✔
1214

1215
int TableDesignerWidget::getCaptureRange() const {
40✔
1216
    if (ui->capture_range_spinbox) {
40✔
1217
        return ui->capture_range_spinbox->value();
40✔
1218
    }
UNCOV
1219
    return 30000;// Default value
×
1220
}
1221

1222
void TableDesignerWidget::setCaptureRange(int value) {
44✔
1223
    if (ui->capture_range_spinbox) {
44✔
1224
        ui->capture_range_spinbox->blockSignals(true);
44✔
1225
        ui->capture_range_spinbox->setValue(value);
44✔
1226
        ui->capture_range_spinbox->blockSignals(false);
44✔
1227
    }
1228
}
44✔
1229

1230
bool TableDesignerWidget::isIntervalBeginningSelected() const {
40✔
1231
    if (ui->interval_beginning_radio) {
40✔
1232
        return ui->interval_beginning_radio->isChecked();
40✔
1233
    }
UNCOV
1234
    return true;// Default to beginning
×
1235
}
1236

1237
bool TableDesignerWidget::isIntervalItselfSelected() const {
64✔
1238
    if (ui->interval_itself_radio) {
64✔
1239
        return ui->interval_itself_radio->isChecked();
64✔
1240
    }
UNCOV
1241
    return false;// Default to not selected
×
1242
}
1243

1244
void TableDesignerWidget::triggerPreviewDebounced() {
148✔
1245
    if (_preview_debounce_timer) _preview_debounce_timer->start();
148✔
1246
    // Also trigger an immediate rebuild to support non-interactive/test contexts
1247
    rebuildPreviewNow();
148✔
1248
}
148✔
1249

1250
void TableDesignerWidget::rebuildPreviewNow() {
148✔
1251
    if (!_data_manager || !_table_viewer) return;
148✔
1252
    if (_current_table_id.isEmpty()) {
148✔
1253
        _table_viewer->clearTable();
67✔
1254
        return;
67✔
1255
    }
1256

1257
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
81✔
1258
    if (row_source.isEmpty()) {
81✔
1259
        _table_viewer->clearTable();
22✔
1260
        return;
22✔
1261
    }
1262

1263
    // Get enabled column infos from the computers tree
1264
    auto column_infos = getEnabledColumnInfos();
59✔
1265
    if (column_infos.empty()) {
59✔
1266
        _table_viewer->clearTable();
42✔
1267
        return;
42✔
1268
    }
1269

1270
    // Create row selector for the entire dataset
1271
    auto selector = createRowSelector(row_source);
17✔
1272
    if (!selector) {
17✔
UNCOV
1273
        _table_viewer->clearTable();
×
UNCOV
1274
        return;
×
1275
    }
1276

1277
    // Apply any saved column order for this table id
1278
    auto desiredOrder = _table_column_order.value(_current_table_id);
17✔
1279
    if (!desiredOrder.isEmpty()) {
17✔
1280
        std::vector<ColumnInfo> reordered;
10✔
1281
        reordered.reserve(column_infos.size());
10✔
1282
        for (auto const & name : desiredOrder) {
25✔
1283
            auto it = std::find_if(column_infos.begin(), column_infos.end(), [&](ColumnInfo const & ci){ return QString::fromStdString(ci.name) == name; });
37✔
1284
            if (it != column_infos.end()) {
15✔
1285
                reordered.push_back(*it);
15✔
1286
            }
1287
        }
1288
        for (auto const & ci : column_infos) {
29✔
1289
            if (std::find_if(reordered.begin(), reordered.end(), [&](ColumnInfo const & x){ return x.name == ci.name; }) == reordered.end()) {
46✔
1290
                reordered.push_back(ci);
4✔
1291
            }
1292
        }
1293
        column_infos = std::move(reordered);
10✔
1294
    }
10✔
1295

1296
    // Set up the table viewer with pagination
1297
    _table_viewer->setTableConfiguration(
85✔
1298
            std::move(selector),
17✔
1299
            std::move(column_infos),
17✔
1300
            _data_manager,
17✔
1301
            QString("Preview: %1").arg(_current_table_id));
34✔
1302

1303
    // Capture the current visual order from the viewer
1304
    QStringList currentOrder;
17✔
1305
    if (_table_viewer) {
17✔
1306
        auto * tv = _table_viewer->findChild<QTableView*>();
17✔
1307
        if (tv && tv->model()) {
17✔
1308
            auto * header = tv->horizontalHeader();
17✔
1309
            int cols = tv->model()->columnCount();
17✔
1310
            for (int v = 0; header && v < cols; ++v) {
43✔
1311
                int logical = header->logicalIndex(v);
26✔
1312
                auto name = tv->model()->headerData(logical, Qt::Horizontal, Qt::DisplayRole).toString();
26✔
1313
                currentOrder.push_back(name);
26✔
1314
            }
26✔
1315
        }
1316
    }
1317
    if (!currentOrder.isEmpty()) {
17✔
1318
        _table_column_order[_current_table_id] = currentOrder;
17✔
1319
    }
1320
}
123✔
1321

1322
void TableDesignerWidget::refreshComputersTree() {
76✔
1323
    if (!_data_manager) return;
76✔
1324

1325
    _updating_computers_tree = true;
76✔
1326

1327
    // Preserve previous checkbox states and custom column names
1328
    std::map<std::string, std::pair<Qt::CheckState, QString>> previous_states;
76✔
1329
    if (ui->computers_tree && ui->computers_tree->topLevelItemCount() > 0) {
76✔
1330
        for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
495✔
1331
            auto * data_source_item_old = ui->computers_tree->topLevelItem(i);
447✔
1332
            for (int j = 0; j < data_source_item_old->childCount(); ++j) {
1,901✔
1333
                auto * computer_item_old = data_source_item_old->child(j);
1,454✔
1334
                QString ds = computer_item_old->data(0, Qt::UserRole).toString();
1,454✔
1335
                QString cn = computer_item_old->data(1, Qt::UserRole).toString();
1,454✔
1336
                std::string key = (ds + "||" + cn).toStdString();
1,454✔
1337
                previous_states[key] = {computer_item_old->checkState(1), computer_item_old->text(2)};
1,454✔
1338
            }
1,454✔
1339
        }
1340
    }
1341

1342
    ui->computers_tree->clear();
76✔
1343
    ui->computers_tree->setHeaderLabels({"Data Source / Computer", "Enabled", "Column Name"});
380✔
1344

1345
    auto * registry = _data_manager->getTableRegistry();
76✔
1346
    if (!registry) {
76✔
UNCOV
1347
        _updating_computers_tree = false;
×
UNCOV
1348
        return;
×
1349
    }
1350

1351
    auto data_manager_extension = registry->getDataManagerExtension();
76✔
1352
    if (!data_manager_extension) {
76✔
UNCOV
1353
        _updating_computers_tree = false;
×
UNCOV
1354
        return;
×
1355
    }
1356

1357
    auto & computer_registry = registry->getComputerRegistry();
76✔
1358

1359
    // Get available data sources
1360
    auto data_sources = getAvailableDataSources();
76✔
1361

1362
    // Create tree structure: Data Source -> Computers
1363
    for (QString const & data_source: data_sources) {
781✔
1364
        auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
705✔
1365
        data_source_item->setText(0, data_source);
705✔
1366
        data_source_item->setFlags(Qt::ItemIsEnabled);
705✔
1367
        data_source_item->setExpanded(false);// Start collapsed
705✔
1368

1369
        // Convert data source string to DataSourceVariant and determine RowSelectorType
1370
        auto [data_source_variant, row_selector_type] = createDataSourceVariant(data_source, data_manager_extension);
705✔
1371

1372
        if (!data_source_variant.has_value()) {
705✔
1373
            qDebug() << "Failed to create data source variant for:" << data_source;
311✔
1374
            continue;
311✔
1375
        }
1376

1377
        // Get available computers for this specific data source and row selector combination
1378
        auto available_computers = computer_registry.getAvailableComputers(row_selector_type, data_source_variant.value());
394✔
1379

1380
        // Add compatible computers as children
1381
        for (auto const & computer_info: available_computers) {
2,688✔
1382
            auto * computer_item = new QTreeWidgetItem(data_source_item);
2,294✔
1383
            computer_item->setText(0, QString::fromStdString(computer_info.name));
2,294✔
1384
            computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
2,294✔
1385
            computer_item->setCheckState(1, Qt::Unchecked);
2,294✔
1386

1387
            // Generate default column name
1388
            QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
2,294✔
1389
            computer_item->setText(2, default_name);
2,294✔
1390
            computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
2,294✔
1391

1392
            // Store data source and computer name for later use
1393
            computer_item->setData(0, Qt::UserRole, data_source);
2,294✔
1394
            computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
2,294✔
1395

1396
            // Restore previous state if present
1397
            std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
2,294✔
1398
            auto it_prev = previous_states.find(prev_key);
2,294✔
1399
            if (it_prev != previous_states.end()) {
2,294✔
1400
                computer_item->setCheckState(1, it_prev->second.first);
1,451✔
1401
                if (!it_prev->second.second.isEmpty()) {
1,451✔
1402
                    computer_item->setText(2, it_prev->second.second);
1,451✔
1403
                }
1404
            }
1405
        }
2,294✔
1406
    }
705✔
1407

1408
    // Resize columns to content
1409
    ui->computers_tree->resizeColumnToContents(0);
76✔
1410
    ui->computers_tree->resizeColumnToContents(1);
76✔
1411
    ui->computers_tree->resizeColumnToContents(2);
76✔
1412

1413
    _updating_computers_tree = false;
76✔
1414

1415
    // Update preview after refresh
1416
    triggerPreviewDebounced();
76✔
1417
}
152✔
1418

1419
void TableDesignerWidget::setJsonTemplateFromCurrentState() {
3✔
1420
    if (!_table_json_widget) return;
3✔
1421
    // Build a minimal JSON template representing current UI state
1422
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
3✔
1423
    auto columns = getEnabledColumnInfos();
3✔
1424
    if (row_source.isEmpty() && columns.empty()) { _table_json_widget->setJsonText("{}"); return; }
3✔
1425

1426
    QString row_type;
3✔
1427
    QString row_source_name;
3✔
1428
    if (row_source.startsWith("TimeFrame: ")) { row_type = "timestamp"; row_source_name = row_source.mid(11); }
3✔
1429
    else if (row_source.startsWith("Events: ")) { row_type = "timestamp"; row_source_name = row_source.mid(8); }
3✔
1430
    else if (row_source.startsWith("Intervals: ")) { row_type = "interval"; row_source_name = row_source.mid(11); }
3✔
1431

1432
    QStringList column_entries;
3✔
1433
    for (auto const & c : columns) {
8✔
1434
        // Strip any internal prefixes for JSON to keep schema user-friendly
1435
        QString ds = QString::fromStdString(c.dataSourceName);
5✔
1436
        if (ds.startsWith("events:")) ds = ds.mid(7);
5✔
NEW
1437
        else if (ds.startsWith("intervals:")) ds = ds.mid(10);
×
NEW
1438
        else if (ds.startsWith("analog:")) ds = ds.mid(7);
×
1439

1440
        QString entry = QString(
15✔
1441
            "{\n  \"name\": \"%1\",\n  \"description\": \"%2\",\n  \"data_source\": \"%3\",\n  \"computer\": \"%4\"%5\n}"
1442
        ).arg(QString::fromStdString(c.name))
20✔
1443
         .arg(QString::fromStdString(c.description))
20✔
1444
         .arg(ds)
20✔
1445
         .arg(QString::fromStdString(c.computerName))
20✔
1446
         .arg(c.parameters.empty() ? QString() : QString(",\n  \"parameters\": {}"));
10✔
1447
        column_entries << entry;
5✔
1448
    }
5✔
1449

1450
    QString table_name = _table_info_widget ? _table_info_widget->getName() : _current_table_id;
3✔
1451
    QString json = QString(
9✔
1452
        "{\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}"
1453
    ).arg(_current_table_id)
12✔
1454
     .arg(table_name)
12✔
1455
     .arg(row_type)
12✔
1456
     .arg(row_source_name)
12✔
1457
     .arg(column_entries.join(",\n"));
6✔
1458

1459
    _table_json_widget->setJsonText(json);
3✔
1460
}
3✔
1461

1462
void TableDesignerWidget::applyJsonTemplateToUI(QString const & jsonText) {
7✔
1463
    // Very light-weight parser using Qt to extract essential fields.
1464
    // Assumes a schema similar to tests under computers *.test.cpp.
1465
    QJsonParseError err;
7✔
1466
    QByteArray bytes = jsonText.toUtf8();
7✔
1467
    QJsonDocument doc = QJsonDocument::fromJson(bytes, &err);
7✔
1468
    if (err.error != QJsonParseError::NoError || !doc.isObject()) {
7✔
1469
        // Compute line/column from byte offset if possible
1470
        int64_t offset = static_cast<int64_t>(err.offset);
1✔
1471
        int line = 1;
1✔
1472
        int col = 1;
1✔
1473
        // Avoid operator[] ambiguity on some compilers by using qsizetype and at()
1474
        qsizetype len = std::min<qsizetype>(bytes.size(), static_cast<qsizetype>(offset));
1✔
1475
        for (qsizetype i = 0; i < len; ++i) {
149✔
1476
            char ch = bytes.at(i);
148✔
1477
            if (ch == '\n') { ++line; col = 1; }
148✔
1478
            else { ++col; }
142✔
1479
        }
1480
        QString detail = err.error != QJsonParseError::NoError
1✔
1481
                ? QString("%1 (line %2, column %3)").arg(err.errorString()).arg(line).arg(col)
3✔
1482
                : QString("JSON root must be an object");
2✔
1483
        auto * box = new QMessageBox(this);
1✔
1484
        box->setIcon(QMessageBox::Critical);
1✔
1485
        box->setWindowTitle("Invalid JSON");
1✔
1486
        box->setText(QString("JSON format is invalid: %1").arg(detail));
1✔
1487
        box->setAttribute(Qt::WA_DeleteOnClose);
1✔
1488
        box->show();
1✔
1489
        return;
1✔
1490
    }
1✔
1491
    auto obj = doc.object();
6✔
1492
    if (!obj.contains("tables") || !obj["tables"].isArray()) {
6✔
NEW
1493
        auto * box = new QMessageBox(this);
×
NEW
1494
        box->setIcon(QMessageBox::Critical);
×
NEW
1495
        box->setWindowTitle("Invalid JSON");
×
NEW
1496
        box->setText("Missing required key: tables (array)");
×
NEW
1497
        box->setAttribute(Qt::WA_DeleteOnClose);
×
NEW
1498
        box->show();
×
NEW
1499
        return;
×
1500
    }
1501
    auto tables = obj["tables"].toArray();
6✔
1502
    if (tables.isEmpty() || !tables[0].isObject()) return;
6✔
1503
    auto table = tables[0].toObject();
6✔
1504

1505
    // Row selector
1506
    QStringList errors;
6✔
1507
    QString rs_type;
6✔
1508
    QString rs_source;
6✔
1509
    if (table.contains("row_selector") && table["row_selector"].isObject()) {
6✔
1510
        auto rs = table["row_selector"].toObject();
5✔
1511
        rs_type = rs.value("type").toString();
5✔
1512
        rs_source = rs.value("source").toString();
5✔
1513
        if (rs_type.isEmpty() || rs_source.isEmpty()) {
5✔
NEW
1514
            errors << "Missing required keys in row_selector: 'type' and/or 'source'";
×
1515
        } else {
1516
            // Validate existence
1517
            bool source_ok = false;
5✔
1518
            if (rs_type == "interval") {
5✔
1519
                source_ok = (_data_manager && _data_manager->getData<DigitalIntervalSeries>(rs_source.toStdString()) != nullptr);
5✔
NEW
1520
            } else if (rs_type == "timestamp") {
×
NEW
1521
                source_ok = (_data_manager && (
×
NEW
1522
                    _data_manager->getTime(TimeKey(rs_source.toStdString())) != nullptr ||
×
NEW
1523
                    _data_manager->getData<DigitalEventSeries>(rs_source.toStdString()) != nullptr
×
NEW
1524
                ));
×
1525
            } else {
NEW
1526
                errors << QString("Unsupported row_selector type: %1").arg(rs_type);
×
1527
            }
1528
            if (!source_ok) {
5✔
1529
                errors << QString("Row selector data key not found in DataManager: %1").arg(rs_source);
1✔
1530
            } else {
1531
                // Apply selection to UI
1532
                QString entry;
4✔
1533
                if (rs_type == "interval") {
4✔
1534
                    entry = QString("Intervals: %1").arg(rs_source);
4✔
NEW
1535
                } else if (rs_type == "timestamp") {
×
1536
                    // Prefer TimeFrame, fallback to Events
NEW
1537
                    entry = QString("TimeFrame: %1").arg(rs_source);
×
NEW
1538
                    int idx_tf = ui->row_data_source_combo->findText(entry);
×
NEW
1539
                    if (idx_tf < 0) entry = QString("Events: %1").arg(rs_source);
×
1540
                }
1541
                int idx = ui->row_data_source_combo->findText(entry);
4✔
1542
                if (idx >= 0) {
4✔
1543
                    ui->row_data_source_combo->setCurrentIndex(idx);
4✔
1544
                    // Ensure computers tree reflects this row selector before enabling columns
1545
                    refreshComputersTree();
4✔
1546
                } else {
NEW
1547
                    errors << QString("Row selector entry not available in UI: %1").arg(entry);
×
1548
                }
1549
            }
4✔
1550
        }
1551
    } else {
5✔
1552
        errors << "Missing required key: row_selector (object)";
1✔
1553
    }
1554

1555
    // Columns: enable matching computers and set column names
1556
    if (table.contains("columns") && table["columns"].isArray()) {
6✔
1557
        auto cols = table["columns"].toArray();
6✔
1558
        auto * tree = ui->computers_tree;
6✔
1559
        // Avoid recursive preview rebuilds while we toggle many items
1560
        bool prevBlocked = tree->blockSignals(true);
6✔
1561
        for (auto const & cval : cols) {
12✔
1562
            if (!cval.isObject()) continue;
6✔
1563
            auto cobj = cval.toObject();
6✔
1564
            QString data_source = cobj.value("data_source").toString();
6✔
1565
            QString computer = cobj.value("computer").toString();
6✔
1566
            QString name = cobj.value("name").toString();
6✔
1567
            if (data_source.isEmpty() || computer.isEmpty() || name.isEmpty()) {
6✔
NEW
1568
                errors << "Missing required keys in column: 'name', 'data_source', and 'computer'";
×
NEW
1569
                continue;
×
1570
            }
1571
            // Validate data source existence
1572
            bool has_ds = (_data_manager && (
12✔
1573
                _data_manager->getData<DigitalEventSeries>(data_source.toStdString()) != nullptr ||
18✔
1574
                _data_manager->getData<DigitalIntervalSeries>(data_source.toStdString()) != nullptr ||
7✔
1575
                _data_manager->getData<AnalogTimeSeries>(data_source.toStdString()) != nullptr
7✔
1576
            ));
12✔
1577
            if (!has_ds) {
6✔
1578
                errors << QString("Data key not found in DataManager: %1").arg(data_source);
1✔
1579
            }
1580
            // Validate computer exists
1581
            bool computer_exists = false;
6✔
1582
            if (_data_manager) {
6✔
1583
                if (auto * reg = _data_manager->getTableRegistry()) {
6✔
1584
                    auto & cr = reg->getComputerRegistry();
6✔
1585
                    computer_exists = cr.findComputerInfo(computer.toStdString());
6✔
1586
                }
1587
            }
1588
            if (!computer_exists) {
6✔
1589
                errors << QString("Requested computer does not exist: %1").arg(computer);
2✔
1590
            }
1591
            // Validate compatibility (heuristic)
1592
            bool type_event = false, type_interval = false, type_analog = false;
6✔
1593
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
60✔
1594
                auto * ds_item = tree->topLevelItem(i);
54✔
1595
                QString ds_text = ds_item->text(0);
54✔
1596
                if (ds_text.contains(data_source)) {
54✔
1597
                    if (ds_text.startsWith("Events: ")) type_event = true;
5✔
NEW
1598
                    else if (ds_text.startsWith("Intervals: ")) type_interval = true;
×
NEW
1599
                    else if (ds_text.startsWith("analog:")) type_analog = true;
×
1600
                }
1601
            }
54✔
1602
            QString ds_repr;
6✔
1603
            if (type_event) ds_repr = QString("Events: %1").arg(data_source);
6✔
1604
            else if (type_interval) ds_repr = QString("Intervals: %1").arg(data_source);
1✔
1605
            else if (type_analog) ds_repr = QString("analog:%1").arg(data_source);
1✔
1606
            if (!ds_repr.isEmpty() && !isComputerCompatibleWithDataSource(computer.toStdString(), ds_repr)) {
6✔
1607
                errors << QString("Computer '%1' is not valid for data source type requested (%2)").arg(computer, ds_repr);
2✔
1608
            }
1609

1610
            // Find matching tree item with strict preference
1611
            QString exact_events = QString("Events: %1").arg(data_source);
6✔
1612
            QString exact_intervals = QString("Intervals: %1").arg(data_source);
6✔
1613
            QString exact_analog = QString("analog:%1").arg(data_source);
6✔
1614
            QTreeWidgetItem * matched_ds = nullptr;
6✔
1615
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
40✔
1616
                auto * ds_item = tree->topLevelItem(i);
39✔
1617
                QString t = ds_item->text(0);
39✔
1618
                if (t == exact_events || t == exact_intervals || t == exact_analog) { matched_ds = ds_item; break; }
39✔
1619
            }
39✔
1620
            if (!matched_ds) {
6✔
1621
                for (int i = 0; i < tree->topLevelItemCount(); ++i) {
10✔
1622
                    auto * ds_item = tree->topLevelItem(i);
9✔
1623
                    QString t = ds_item->text(0);
9✔
1624
                    if (t.contains(data_source) || t.endsWith(data_source)) { matched_ds = ds_item; break; }
9✔
1625
                }
9✔
1626
            }
1627
            if (matched_ds) {
6✔
1628
                for (int j = 0; j < matched_ds->childCount(); ++j) {
20✔
1629
                    auto * comp_item = matched_ds->child(j);
15✔
1630
                    QString comp_text = comp_item->text(0).trimmed();
15✔
1631
                    if (comp_text == computer || comp_text.contains(computer)) {
15✔
1632
                        comp_item->setCheckState(1, Qt::Checked);
3✔
1633
                        if (!name.isEmpty()) comp_item->setText(2, name);
3✔
1634
                    }
1635
                }
15✔
1636
            } else {
1637
                errors << QString("Data source not found in tree: %1").arg(data_source);
1✔
1638
            }
1639
        }
6✔
1640
        tree->blockSignals(prevBlocked);
6✔
1641
        if (!errors.isEmpty()) {
6✔
1642
            auto * box = new QMessageBox(this);
4✔
1643
            box->setIcon(QMessageBox::Critical);
4✔
1644
            box->setWindowTitle("Invalid Table JSON");
4✔
1645
            box->setText(errors.join("\n"));
4✔
1646
            box->setAttribute(Qt::WA_DeleteOnClose);
4✔
1647
            box->show();
4✔
1648
            return;
4✔
1649
        }
1650
        triggerPreviewDebounced();
2✔
1651
    }
6✔
1652
}
36✔
1653

1654
void TableDesignerWidget::onComputersTreeItemChanged() {
17,492✔
1655
    if (_updating_computers_tree) return;
17,492✔
1656

1657
    // Trigger preview update when checkbox states change
1658
    triggerPreviewDebounced();
15✔
1659
}
1660

1661
void TableDesignerWidget::onComputersTreeItemEdited(QTreeWidgetItem * item, int column) {
17,492✔
1662
    if (_updating_computers_tree) return;
17,492✔
1663

1664
    // Only respond to column name edits (column 2)
1665
    if (column == 2) {
15✔
1666
        // Column name was edited, trigger preview update
1667
        triggerPreviewDebounced();
1✔
1668
    }
1669
}
1670

1671
std::vector<ColumnInfo> TableDesignerWidget::getEnabledColumnInfos() const {
70✔
1672
    std::vector<ColumnInfo> column_infos;
70✔
1673

1674
    if (!ui->computers_tree) return column_infos;
70✔
1675

1676
    // Iterate through all data source items
1677
    for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
721✔
1678
        auto * data_source_item = ui->computers_tree->topLevelItem(i);
651✔
1679

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

1684
            // Check if this computer is enabled
1685
            if (computer_item->checkState(1) == Qt::Checked) {
2,128✔
1686
                QString data_source = computer_item->data(0, Qt::UserRole).toString();
44✔
1687
                QString computer_name = computer_item->data(1, Qt::UserRole).toString();
44✔
1688
                QString column_name = computer_item->text(2);
44✔
1689

1690
                if (column_name.isEmpty()) {
44✔
UNCOV
1691
                    column_name = generateDefaultColumnName(data_source, computer_name);
×
1692
                }
1693

1694
                // Create ColumnInfo (use raw key without UI prefixes)
1695
                QString source_key = data_source;
44✔
1696
                if (source_key.startsWith("Events: ")) {
44✔
1697
                    source_key = QString("events:%1").arg(source_key.mid(8));
43✔
1698
                } else if (source_key.startsWith("Intervals: ")) {
1✔
1699
                    source_key = QString("intervals:%1").arg(source_key.mid(11));
1✔
UNCOV
1700
                } else if (source_key.startsWith("analog:")) {
×
NEW
1701
                    source_key = source_key; // already prefixed
×
UNCOV
1702
                } else if (source_key.startsWith("TimeFrame: ")) {
×
1703
                    // TimeFrame used only for row selector; columns require concrete sources
UNCOV
1704
                    source_key = source_key.mid(11);
×
1705
                }
1706

1707
                ColumnInfo info(column_name.toStdString(),
132✔
1708
                                QString("Column from %1 using %2").arg(data_source, computer_name).toStdString(),
88✔
1709
                                source_key.toStdString(),
88✔
1710
                                computer_name.toStdString());
220✔
1711

1712
                // Set output type based on computer info
1713
                if (auto * registry = _data_manager->getTableRegistry()) {
44✔
1714
                    auto & computer_registry = registry->getComputerRegistry();
44✔
1715
                    auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
44✔
1716
                    if (computer_info) {
44✔
1717
                        info.outputType = computer_info->outputType;
44✔
1718
                        info.outputTypeName = computer_info->outputTypeName;
44✔
1719
                        info.isVectorType = computer_info->isVectorType;
44✔
1720
                        if (info.isVectorType) {
44✔
1721
                            info.elementType = computer_info->elementType;
6✔
1722
                            info.elementTypeName = computer_info->elementTypeName;
6✔
1723
                        }
1724
                    }
1725
                }
1726

1727
                column_infos.push_back(std::move(info));
44✔
1728
            }
44✔
1729
        }
1730
    }
1731

1732
    return column_infos;
70✔
UNCOV
1733
}
×
1734

1735
bool TableDesignerWidget::isComputerCompatibleWithDataSource(std::string const & computer_name, QString const & data_source) const {
5✔
1736
    if (!_data_manager) return false;
5✔
1737

1738
    auto * registry = _data_manager->getTableRegistry();
5✔
1739
    if (!registry) return false;
5✔
1740

1741
    auto & computer_registry = registry->getComputerRegistry();
5✔
1742
    auto computer_info = computer_registry.findComputerInfo(computer_name);
5✔
1743
    if (!computer_info) return false;
5✔
1744

1745
    // Basic compatibility check based on data source type and common computer patterns
1746
    if (data_source.startsWith("Events: ")) {
3✔
1747
        // Event-based computers typically have "Event" in their name
1748
        return computer_name.find("Event") != std::string::npos;
3✔
UNCOV
1749
    } else if (data_source.startsWith("Intervals: ")) {
×
1750
        // Interval-based computers typically work with intervals or events
UNCOV
1751
        return computer_name.find("Event") != std::string::npos ||
×
UNCOV
1752
               computer_name.find("Interval") != std::string::npos;
×
UNCOV
1753
    } else if (data_source.startsWith("analog:")) {
×
1754
        // Analog-based computers typically have "Analog" in their name
UNCOV
1755
        return computer_name.find("Analog") != std::string::npos;
×
UNCOV
1756
    } else if (data_source.startsWith("TimeFrame: ")) {
×
1757
        // TimeFrame-based computers - generally most computers can work with timestamps
UNCOV
1758
        return computer_name.find("Timestamp") != std::string::npos ||
×
UNCOV
1759
               computer_name.find("Time") != std::string::npos;
×
1760
    }
1761

1762
    // Default: assume compatibility for unrecognized patterns
UNCOV
1763
    return true;
×
1764
}
1765

1766
QString TableDesignerWidget::generateDefaultColumnName(QString const & data_source, QString const & computer_name) const {
2,294✔
1767
    QString source_name = data_source;
2,294✔
1768

1769
    // Extract the actual name from prefixed data sources
1770
    if (source_name.startsWith("Events: ")) {
2,294✔
1771
        source_name = source_name.mid(8);
462✔
1772
    } else if (source_name.startsWith("Intervals: ")) {
1,832✔
1773
        source_name = source_name.mid(11);
616✔
1774
    } else if (source_name.startsWith("analog:")) {
1,216✔
1775
        source_name = source_name.mid(7);
1,216✔
UNCOV
1776
    } else if (source_name.startsWith("TimeFrame: ")) {
×
UNCOV
1777
        source_name = source_name.mid(11);
×
1778
    }
1779

1780
    // Create a concise name
1781
    return QString("%1_%2").arg(source_name, computer_name);
6,882✔
1782
}
2,294✔
1783

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