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

paulmthompson / WhiskerToolbox / 17826420298

18 Sep 2025 02:24AM UTC coverage: 71.942% (-0.002%) from 71.944%
17826420298

push

github

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

110 of 158 new or added lines in 12 files covered. (69.62%)

222 existing lines in 9 files now uncovered.

39625 of 55079 relevant lines covered (71.94%)

1301.53 hits per line

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

64.92
/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 <QTableView>
39
#include <QTreeWidget>
40
#include <QTreeWidgetItem>
41
#include <QVBoxLayout>
42

43
#include <QFutureWatcher>
44
#include <QJsonArray>
45
#include <QJsonDocument>
46
#include <QJsonObject>
47
#include <QJsonParseError>
48
#include <QTimer>
49
#include <QtConcurrent>
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)
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✔
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✔
204
                case TableEventType::Removed:
×
205
                    this->onTableManagerTableRemoved(QString::fromStdString(ev.tableId));
×
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

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

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

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

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
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 {
271
        QMessageBox::warning(this, "Error", "Failed to create table with ID: " + QString::fromStdString(table_id));
×
272
    }
273
}
×
274

275
void TableDesignerWidget::onDeleteTable() {
×
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

284
    if (reply == QMessageBox::Yes) {
×
285
        auto * registry = _data_manager->getTableRegistry();
×
286
        if (registry && registry->removeTable(_current_table_id.toStdString())) {
×
287
            // The combo will be refreshed by the signal handler
288
            clearUI();
×
289
        } else {
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

323
void TableDesignerWidget::onCaptureRangeChanged() {
×
324
    // Update the info label to reflect the new capture range
325
    QString selected = ui->row_data_source_combo->currentText();
×
326
    if (!selected.isEmpty()) {
×
327
        updateRowInfoLabel(selected);
×
328
    }
329
    triggerPreviewDebounced();
×
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()) {
×
347
        updateBuildStatus("No table selected", true);
×
348
        return;
×
349
    }
350

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

357
    // Get enabled column infos from the tree
358
    auto column_infos = getEnabledColumnInfos();
×
359
    if (column_infos.empty()) {
×
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
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
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
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;
×
386
        for (auto const & column_info: column_infos) {
×
387
            if (!reg->addColumnToBuilder(builder, column_info)) {
×
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

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

398
        // Build the table
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::make_unique<TableView>(std::move(table_view)))) {
×
411
                updateBuildStatus(QString("Table built successfully with %1 columns!").arg(column_infos.size()));
×
412
                qDebug() << "Successfully built table:" << _current_table_id << "with" << column_infos.size() << "columns";
×
413
                // Populate JSON widget with the current configuration
414
                setJsonTemplateFromCurrentState();
×
415
            } else {
416
                updateBuildStatus("Failed to store built table", true);
×
417
            }
418
        } else {
×
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();
×
425
    }
×
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✔
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);
×
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);
×
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✔
452
            updateBuildStatus("Failed to create row selector", true);
×
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✔
460
            updateBuildStatus("DataManager extension not available", true);
×
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✔
472
                updateBuildStatus(QString("Failed to create column: %1").arg(QString::fromStdString(column_info.name)), true);
×
473
                all_columns_valid = false;
×
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::make_unique<TableView>(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 {
502
                updateBuildStatus("Failed to store built table", true);
×
503
                return false;
×
504
            }
505
        } else {
3✔
506
            updateBuildStatus("Registry unavailable", true);
×
507
            return false;
×
508
        }
509

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

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

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

535
    // Currently only PCA option is exposed
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
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);
×
548
        for (auto const & s: _table_transform_widget->getExcludeColumns()) cfg.exclude.push_back(s);
×
549
    }
550

551
    try {
552
        PCATransform pca(cfg);
×
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);
×
560
        }
×
561

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::make_unique<TableView>(std::move(derived)))) {
×
567
            updateBuildStatus(QString("Created transformed table: %1").arg(out_name));
×
568
            refreshTableCombo();
×
569
        } else {
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);
×
574
    }
×
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();
×
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);
×
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;
×
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)";
×
610
    int precision = _table_export_widget ? _table_export_widget->getPrecision() : 3;
×
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")
×
616
        delim = "\t";
×
617
    std::string eol = "\n";
×
618
    if (lineEnding.startsWith("CRLF")) eol = "\r\n";
×
619

620
    try {
621
        std::ofstream file(filename.toStdString());
×
622
        if (!file.is_open()) {
×
623
            updateBuildStatus("Could not open file for writing", true);
×
624
            return;
×
625
        }
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;
×
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) {
×
639
                if (c > 0) file << delim;
×
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()) {
×
NEW
645
                        file << vals[r];
×
NEW
646
                        wrote = true;
×
647
                    }
648
                } catch (...) {}
×
649
                if (!wrote) {
×
650
                    try {
651
                        auto const & vals = view->getColumnValues<int>(names[c].c_str());
×
NEW
652
                        if (r < vals.size()) {
×
NEW
653
                            file << vals[r];
×
NEW
654
                            wrote = true;
×
655
                        }
656
                    } catch (...) {}
×
657
                }
658
                if (!wrote) {
×
659
                    try {
660
                        auto const & vals = view->getColumnValues<int64_t>(names[c].c_str());
×
NEW
661
                        if (r < vals.size()) {
×
NEW
662
                            file << vals[r];
×
NEW
663
                            wrote = true;
×
664
                        }
UNCOV
665
                    } catch (...) {}
×
666
                }
667
                if (!wrote) {
×
668
                    try {
669
                        auto const & vals = view->getColumnValues<bool>(names[c].c_str());
×
NEW
670
                        if (r < vals.size()) {
×
NEW
671
                            file << (vals[r] ? 1 : 0);
×
NEW
672
                            wrote = true;
×
673
                        }
674
                    } catch (...) {}
×
675
                }
UNCOV
676
                if (!wrote) file << "NaN";
×
677
            }
UNCOV
678
            file << eol;
×
679
        }
UNCOV
680
        file.close();
×
681
        updateBuildStatus(QString("Exported CSV: %1").arg(filename));
×
682
    } catch (std::exception const & e) {
×
683
        updateBuildStatus(QString("Export failed: %1").arg(e.what()), true);
×
684
    }
×
UNCOV
685
}
×
686

UNCOV
687
QString TableDesignerWidget::promptSaveCsvFilename() const {
×
688
    return QFileDialog::getSaveFileName(const_cast<TableDesignerWidget *>(this), "Export Table to CSV", QString(), "CSV Files (*.csv)");
×
689
}
690

UNCOV
691
void TableDesignerWidget::onSaveTableInfo() {
×
692
    if (_current_table_id.isEmpty()) {
×
693
        return;
×
694
    }
695

696
    QString name = _table_info_widget ? _table_info_widget->getName() : QString();
×
697
    QString description = _table_info_widget ? _table_info_widget->getDescription() : QString();
×
698

699
    if (name.isEmpty()) {
×
700
        QMessageBox::warning(this, "Error", "Table name cannot be empty");
×
UNCOV
701
        return;
×
702
    }
703

704
    if (auto * reg = _data_manager->getTableRegistry(); reg && reg->updateTableInfo(_current_table_id.toStdString(), name.toStdString(), description.toStdString())) {
×
705
        updateBuildStatus("Table information saved");
×
706
        // Refresh the combo to show updated name
UNCOV
707
        refreshTableCombo();
×
708
        // Restore selection
709
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
UNCOV
710
            if (ui->table_combo->itemData(i).toString() == _current_table_id) {
×
711
                ui->table_combo->setCurrentIndex(i);
×
712
                break;
×
713
            }
714
        }
715
    } else {
716
        QMessageBox::warning(this, "Error", "Failed to save table information");
×
717
    }
UNCOV
718
}
×
719

720
void TableDesignerWidget::onTableManagerTableCreated(QString const & table_id) {
12✔
721
    refreshTableCombo();
12✔
722
    qDebug() << "Table created signal received:" << table_id;
12✔
723
}
12✔
724

UNCOV
725
void TableDesignerWidget::onTableManagerTableRemoved(QString const & table_id) {
×
UNCOV
726
    refreshTableCombo();
×
UNCOV
727
    if (_current_table_id == table_id) {
×
728
        _current_table_id.clear();
×
UNCOV
729
        clearUI();
×
730
    }
UNCOV
731
    qDebug() << "Table removed signal received:" << table_id;
×
UNCOV
732
}
×
733

734
void TableDesignerWidget::onTableManagerTableInfoUpdated(QString const & table_id) {
11✔
735
    if (_current_table_id == table_id && !_loading_column_configuration) {
11✔
736
        loadTableInfo(table_id);
11✔
737
    }
738
    qDebug() << "Table info updated signal received:" << table_id;
11✔
739
}
11✔
740

741
void TableDesignerWidget::refreshTableCombo() {
28✔
742
    ui->table_combo->clear();
28✔
743

744
    auto * reg = _data_manager->getTableRegistry();
28✔
745
    auto table_infos = reg ? reg->getAllTableInfo() : std::vector<TableInfo>{};
28✔
746
    for (auto const & info: table_infos) {
43✔
747
        ui->table_combo->addItem(QString::fromStdString(info.name), QString::fromStdString(info.id));
15✔
748
    }
749

750
    if (ui->table_combo->count() == 0) {
28✔
751
        ui->table_combo->addItem("(No tables available)", "");
16✔
752
    }
753
}
56✔
754

755
void TableDesignerWidget::refreshRowDataSourceCombo() {
20✔
756
    ui->row_data_source_combo->clear();
20✔
757

758
    if (!_data_manager) {
20✔
UNCOV
759
        qDebug() << "refreshRowDataSourceCombo: No table manager";
×
UNCOV
760
        return;
×
761
    }
762

763
    auto data_sources = getAvailableDataSources();
20✔
764
    qDebug() << "refreshRowDataSourceCombo: Found" << data_sources.size() << "data sources:" << data_sources;
20✔
765

766
    for (QString const & source: data_sources) {
206✔
767
        // Only include valid row sources in this combo
768
        if (source.startsWith("TimeFrame: ") || source.startsWith("Events: ") || source.startsWith("Intervals: ")) {
186✔
769
            ui->row_data_source_combo->addItem(source);
146✔
770
        }
771
    }
772

773
    if (ui->row_data_source_combo->count() == 0) {
20✔
UNCOV
774
        ui->row_data_source_combo->addItem("(No data sources available)");
×
UNCOV
775
        qDebug() << "refreshRowDataSourceCombo: No data sources available";
×
776
    }
777
}
20✔
778

779

780
void TableDesignerWidget::loadTableInfo(QString const & table_id) {
24✔
781
    if (table_id.isEmpty() || !_data_manager) {
24✔
UNCOV
782
        clearUI();
×
UNCOV
783
        return;
×
784
    }
785

786
    auto * reg = _data_manager->getTableRegistry();
24✔
787
    auto info = reg ? reg->getTableInfo(table_id.toStdString()) : TableInfo{};
24✔
788
    if (info.id.empty()) {
24✔
UNCOV
789
        clearUI();
×
UNCOV
790
        return;
×
791
    }
792

793
    // Load table information
794
    if (_table_info_widget) {
24✔
795
        _table_info_widget->setName(QString::fromStdString(info.name));
24✔
796
        _table_info_widget->setDescription(QString::fromStdString(info.description));
24✔
797
    }
798

799
    // Load row source if available
800
    if (!info.rowSourceName.empty()) {
24✔
801
        int row_index = ui->row_data_source_combo->findText(QString::fromStdString(info.rowSourceName));
12✔
802
        if (row_index >= 0) {
12✔
803
            // Block signals to prevent circular dependency when loading table info
804
            ui->row_data_source_combo->blockSignals(true);
12✔
805
            ui->row_data_source_combo->setCurrentIndex(row_index);
12✔
806
            ui->row_data_source_combo->blockSignals(false);
12✔
807

808
            // Manually update the info label without triggering the signal handler
809
            updateRowInfoLabel(QString::fromStdString(info.rowSourceName));
12✔
810

811
            // Update interval settings visibility
812
            updateIntervalSettingsVisibility();
12✔
813

814
            // Since signals were blocked, this will ensure the tree is refreshed
815
            // when the computers tree is populated later in this function
816
        }
817
    }
818

819
    // Clear old column list (deprecated)
820
    // The computers tree will be populated based on available data sources
821
    refreshComputersTree();
24✔
822

823
    updateBuildStatus(QString("Loaded table: %1").arg(QString::fromStdString(info.name)));
24✔
824
    triggerPreviewDebounced();
24✔
825
}
24✔
826

827
void TableDesignerWidget::clearUI() {
44✔
828
    _current_table_id.clear();
44✔
829

830
    // Clear table info
831
    if (_table_info_widget) {
44✔
832
        _table_info_widget->setName("");
44✔
833
        _table_info_widget->setDescription("");
44✔
834
    }
835

836
    // Clear row source
837
    ui->row_data_source_combo->setCurrentIndex(-1);
44✔
838
    ui->row_info_label->setText("No row source selected");
44✔
839

840
    // Reset capture range and interval settings
841
    setCaptureRange(30000);// Default value
44✔
842
    if (ui->interval_beginning_radio) {
44✔
843
        ui->interval_beginning_radio->setChecked(true);
44✔
844
    }
845
    if (ui->interval_itself_radio) {
44✔
846
        ui->interval_itself_radio->setChecked(false);
44✔
847
    }
848
    if (ui->interval_settings_group) {
44✔
849
        ui->interval_settings_group->setVisible(false);
44✔
850
    }
851

852
    // Clear computers tree
853
    if (ui->computers_tree) {
44✔
854
        ui->computers_tree->clear();
44✔
855
    }
856

857
    // Disable controls
858
    ui->delete_table_btn->setEnabled(false);
44✔
859
    // Table info section is controlled separately
860
    ui->build_table_btn->setEnabled(false);
44✔
861
    if (auto gb = this->findChild<QGroupBox *>("row_source_group")) gb->setEnabled(false);
88✔
862
    if (auto gb = this->findChild<QGroupBox *>("column_design_group")) gb->setEnabled(false);
88✔
863
    if (_table_info_section) _table_info_section->setEnabled(false);
44✔
864

865
    updateBuildStatus("No table selected");
44✔
866
    if (_table_viewer) _table_viewer->clearTable();
44✔
867
}
44✔
868

869
void TableDesignerWidget::updateBuildStatus(QString const & message, bool is_error) {
84✔
870
    ui->build_status_label->setText(message);
84✔
871

872
    if (is_error) {
84✔
UNCOV
873
        ui->build_status_label->setStyleSheet("QLabel { color: red; font-weight: bold; }");
×
874
    } else {
875
        ui->build_status_label->setStyleSheet("QLabel { color: green; }");
84✔
876
    }
877
}
84✔
878

879
QStringList TableDesignerWidget::getAvailableDataSources() const {
96✔
880
    QStringList sources;
96✔
881

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

887
    auto * reg = _data_manager->getTableRegistry();
96✔
888
    auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
96✔
889
    if (!data_manager_extension) {
96✔
UNCOV
890
        qDebug() << "getAvailableDataSources: No data manager extension";
×
UNCOV
891
        return sources;
×
892
    }
893

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

899
    // Add TimeFrame keys as potential row sources
900
    // TimeFrames can define intervals for analysis
901
    auto timeframe_keys = _data_manager->getTimeFrameKeys();
96✔
902
    qDebug() << "getAvailableDataSources: TimeFrame keys:" << timeframe_keys.size();
96✔
903
    for (auto const & key: timeframe_keys) {
489✔
904
        QString source = QString("TimeFrame: %1").arg(QString::fromStdString(key.str()));
393✔
905
        sources << source;
393✔
906
        qDebug() << "  Added TimeFrame:" << source;
393✔
907
    }
393✔
908

909
    // Add DigitalEventSeries keys as potential row sources
910
    // Events can be used to define analysis windows or timestamps
911
    auto event_keys = _data_manager->getKeys<DigitalEventSeries>();
96✔
912
    qDebug() << "getAvailableDataSources: Event keys:" << event_keys.size();
96✔
913
    for (auto const & key: event_keys) {
291✔
914
        QString source = QString("Events: %1").arg(QString::fromStdString(key));
195✔
915
        sources << source;
195✔
916
        qDebug() << "  Added Events:" << source;
195✔
917
    }
195✔
918

919
    // Add DigitalIntervalSeries keys as potential row sources
920
    // Intervals directly define analysis windows
921
    auto interval_keys = _data_manager->getKeys<DigitalIntervalSeries>();
96✔
922
    qDebug() << "getAvailableDataSources: Interval keys:" << interval_keys.size();
96✔
923
    for (auto const & key: interval_keys) {
207✔
924
        QString source = QString("Intervals: %1").arg(QString::fromStdString(key));
111✔
925
        sources << source;
111✔
926
        qDebug() << "  Added Intervals:" << source;
111✔
927
    }
111✔
928

929
    // Add AnalogTimeSeries keys as data sources (for computers; not row selectors)
930
    auto analog_keys = _data_manager->getKeys<AnalogTimeSeries>();
96✔
931
    qDebug() << "getAvailableDataSources: Analog keys:" << analog_keys.size();
96✔
932
    for (auto const & key: analog_keys) {
288✔
933
        QString source = QString("analog:%1").arg(QString::fromStdString(key));
192✔
934
        sources << source;
192✔
935
        qDebug() << "  Added Analog:" << source;
192✔
936
    }
192✔
937

938
    qDebug() << "getAvailableDataSources: Total sources found:" << sources.size();
96✔
939

940
    return sources;
96✔
941
}
96✔
942

943
std::pair<std::optional<DataSourceVariant>, RowSelectorType>
944
TableDesignerWidget::createDataSourceVariant(QString const & data_source_string,
705✔
945
                                             std::shared_ptr<DataManagerExtension> data_manager_extension) const {
946
    std::optional<DataSourceVariant> result;
705✔
947
    RowSelectorType row_selector_type = RowSelectorType::IntervalBased;
705✔
948

949
    if (data_source_string.startsWith("TimeFrame: ")) {
705✔
950
        // TimeFrames are used with TimestampSelector for rows; no concrete data source needed
951
        row_selector_type = RowSelectorType::Timestamp;
311✔
952

953
    } else if (data_source_string.startsWith("Events: ")) {
394✔
954
        QString source_name = data_source_string.mid(8);// Remove "Events: " prefix
154✔
955
        // Event-based computers in the registry operate with interval rows
956
        row_selector_type = RowSelectorType::IntervalBased;
154✔
957

958
        if (auto event_source = data_manager_extension->getEventSource(source_name.toStdString())) {
154✔
959
            result = event_source;
154✔
960
        }
154✔
961

962
    } else if (data_source_string.startsWith("Intervals: ")) {
394✔
963
        QString source_name = data_source_string.mid(11);// Remove "Intervals: " prefix
88✔
964
        row_selector_type = RowSelectorType::IntervalBased;
88✔
965

966
        if (auto interval_source = data_manager_extension->getIntervalSource(source_name.toStdString())) {
88✔
967
            result = interval_source;
88✔
968
        }
88✔
969
    } else if (data_source_string.startsWith("analog:")) {
240✔
970
        QString source_name = data_source_string.mid(7);// Remove "analog:" prefix
152✔
971
        row_selector_type = RowSelectorType::IntervalBased;
152✔
972

973
        if (auto analog_source = data_manager_extension->getAnalogSource(source_name.toStdString())) {
152✔
974
            result = analog_source;
152✔
975
        }
152✔
976
    }
152✔
977

978
    return {result, row_selector_type};
1,410✔
979
}
705✔
980

981
void TableDesignerWidget::updateRowInfoLabel(QString const & selected_source) {
42✔
982
    if (selected_source.isEmpty()) {
42✔
UNCOV
983
        ui->row_info_label->setText("No row source selected");
×
UNCOV
984
        return;
×
985
    }
986

987
    // Parse the selected source to get type and name
988
    QString source_type;
42✔
989
    QString source_name;
42✔
990

991
    if (selected_source.startsWith("TimeFrame: ")) {
42✔
992
        source_type = "TimeFrame";
20✔
993
        source_name = selected_source.mid(11);// Remove "TimeFrame: " prefix
20✔
994
    } else if (selected_source.startsWith("Events: ")) {
22✔
995
        source_type = "Events";
×
996
        source_name = selected_source.mid(8);// Remove "Events: " prefix
×
997
    } else if (selected_source.startsWith("Intervals: ")) {
22✔
998
        source_type = "Intervals";
22✔
999
        source_name = selected_source.mid(11);// Remove "Intervals: " prefix
22✔
1000
    }
1001

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

1005
    if (!_data_manager) {
42✔
UNCOV
1006
        ui->row_info_label->setText(info_text);
×
1007
        return;
×
1008
    }
1009

1010
    auto * reg3 = _data_manager->getTableRegistry();
42✔
1011
    auto data_manager_extension = reg3 ? reg3->getDataManagerExtension() : nullptr;
42✔
1012
    if (!data_manager_extension) {
42✔
UNCOV
1013
        ui->row_info_label->setText(info_text);
×
UNCOV
1014
        return;
×
1015
    }
1016

1017
    auto const source_name_str = source_name.toStdString();
42✔
1018

1019
    // Add specific information based on source type
1020
    if (source_type == "TimeFrame") {
42✔
1021
        auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
20✔
1022
        if (timeframe) {
20✔
1023
            info_text += QString(" - %1 time points").arg(timeframe->getTotalFrameCount());
20✔
1024
        }
1025
    } else if (source_type == "Events") {
42✔
1026
        auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
UNCOV
1027
        if (event_series) {
×
UNCOV
1028
            auto events = event_series->getEventSeries();
×
UNCOV
1029
            info_text += QString(" - %1 events").arg(events.size());
×
UNCOV
1030
        }
×
1031
    } else if (source_type == "Intervals") {
22✔
1032
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
22✔
1033
        if (interval_series) {
22✔
1034
            auto intervals = interval_series->getDigitalIntervalSeries();
22✔
1035
            info_text += QString(" - %1 intervals").arg(intervals.size());
22✔
1036

1037
            // Add capture range and interval setting information
1038
            if (isIntervalItselfSelected()) {
22✔
1039
                info_text += QString("\nUsing intervals as-is (no capture range)");
2✔
1040
            } else {
1041
                int capture_range = getCaptureRange();
20✔
1042
                QString interval_point = isIntervalBeginningSelected() ? "beginning" : "end";
20✔
1043
                info_text += QString("\nCapture range: ±%1 samples around %2 of intervals").arg(capture_range).arg(interval_point);
20✔
1044
            }
20✔
1045
        }
22✔
1046
    }
22✔
1047

1048
    ui->row_info_label->setText(info_text);
42✔
1049
}
42✔
1050

1051
std::unique_ptr<IRowSelector> TableDesignerWidget::createRowSelector(QString const & row_source) {
20✔
1052
    // Parse the row source to get type and name
1053
    QString source_type;
20✔
1054
    QString source_name;
20✔
1055

1056
    if (row_source.startsWith("TimeFrame: ")) {
20✔
UNCOV
1057
        source_type = "TimeFrame";
×
UNCOV
1058
        source_name = row_source.mid(11);// Remove "TimeFrame: " prefix
×
1059
    } else if (row_source.startsWith("Events: ")) {
20✔
UNCOV
1060
        source_type = "Events";
×
UNCOV
1061
        source_name = row_source.mid(8);// Remove "Events: " prefix
×
1062
    } else if (row_source.startsWith("Intervals: ")) {
20✔
1063
        source_type = "Intervals";
20✔
1064
        source_name = row_source.mid(11);// Remove "Intervals: " prefix
20✔
1065
    } else {
UNCOV
1066
        qDebug() << "Unknown row source format:" << row_source;
×
UNCOV
1067
        return nullptr;
×
1068
    }
1069

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

1072
    try {
1073
        if (source_type == "TimeFrame") {
20✔
1074
            // Create IntervalSelector using TimeFrame
UNCOV
1075
            auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
×
UNCOV
1076
            if (!timeframe) {
×
UNCOV
1077
                qDebug() << "TimeFrame not found:" << source_name;
×
1078
                return nullptr;
×
1079
            }
1080

1081
            // Use timestamps to select all rows
UNCOV
1082
            std::vector<TimeFrameIndex> timestamps;
×
UNCOV
1083
            for (int64_t i = 0; i < timeframe->getTotalFrameCount(); ++i) {
×
UNCOV
1084
                timestamps.push_back(TimeFrameIndex(i));
×
1085
            }
UNCOV
1086
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe);
×
1087

1088
        } else if (source_type == "Events") {
20✔
1089
            // Create TimestampSelector using DigitalEventSeries
1090
            auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
UNCOV
1091
            if (!event_series) {
×
UNCOV
1092
                qDebug() << "DigitalEventSeries not found:" << source_name;
×
UNCOV
1093
                return nullptr;
×
1094
            }
1095

1096
            auto events = event_series->getEventSeries();
×
UNCOV
1097
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
×
1098
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
×
UNCOV
1099
            if (!timeframe_obj) {
×
UNCOV
1100
                qDebug() << "TimeFrame not found for events:" << timeframe_key.str();
×
UNCOV
1101
                return nullptr;
×
1102
            }
1103

1104
            // Convert events to TimeFrameIndex
1105
            std::vector<TimeFrameIndex> timestamps;
×
UNCOV
1106
            for (auto const & event: events) {
×
UNCOV
1107
                timestamps.push_back(TimeFrameIndex(static_cast<int64_t>(event)));
×
1108
            }
1109

1110
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe_obj);
×
1111

1112
        } else if (source_type == "Intervals") {
20✔
1113
            // Create IntervalSelector using DigitalIntervalSeries with capture range
1114
            auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
20✔
1115
            if (!interval_series) {
20✔
UNCOV
1116
                qDebug() << "DigitalIntervalSeries not found:" << source_name;
×
1117
                return nullptr;
×
1118
            }
1119

1120
            auto intervals = interval_series->getDigitalIntervalSeries();
20✔
1121
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
20✔
1122
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
20✔
1123
            if (!timeframe_obj) {
20✔
UNCOV
1124
                qDebug() << "TimeFrame not found for intervals:" << timeframe_key.str();
×
UNCOV
1125
                return nullptr;
×
1126
            }
1127

1128
            // Get capture range and interval setting
1129
            int capture_range = getCaptureRange();
20✔
1130
            bool use_beginning = isIntervalBeginningSelected();
20✔
1131
            bool use_interval_itself = isIntervalItselfSelected();
20✔
1132

1133
            // Create intervals based on the selected option
1134
            std::vector<TimeFrameInterval> tf_intervals;
20✔
1135
            for (auto const & interval: intervals) {
99✔
1136
                if (use_interval_itself) {
79✔
1137
                    // Use the interval as-is
1138
                    tf_intervals.emplace_back(TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
3✔
1139
                } else {
1140
                    // Determine the reference point (beginning or end of interval)
1141
                    int64_t reference_point;
1142
                    if (use_beginning) {
76✔
1143
                        reference_point = interval.start;
76✔
1144
                    } else {
UNCOV
1145
                        reference_point = interval.end;
×
1146
                    }
1147

1148
                    // Create a new interval around the reference point
1149
                    int64_t start_point = reference_point - capture_range;
76✔
1150
                    int64_t end_point = reference_point + capture_range;
76✔
1151

1152
                    // Ensure bounds are within the timeframe
1153
                    start_point = std::max(start_point, int64_t(0));
76✔
1154
                    end_point = std::min(end_point, static_cast<int64_t>(timeframe_obj->getTotalFrameCount() - 1));
76✔
1155

1156
                    tf_intervals.emplace_back(TimeFrameIndex(start_point), TimeFrameIndex(end_point));
76✔
1157
                }
1158
            }
1159

1160
            return std::make_unique<IntervalSelector>(std::move(tf_intervals), timeframe_obj);
20✔
1161
        }
20✔
1162

UNCOV
1163
    } catch (std::exception const & e) {
×
UNCOV
1164
        qDebug() << "Exception creating row selector:" << e.what();
×
UNCOV
1165
        return nullptr;
×
UNCOV
1166
    }
×
1167

UNCOV
1168
    qDebug() << "Unsupported row source type:" << source_type;
×
UNCOV
1169
    return nullptr;
×
1170
}
20✔
1171

UNCOV
1172
bool TableDesignerWidget::addColumnToBuilder(TableViewBuilder & builder, ColumnInfo const & column_info) {
×
1173
    // Use the simplified TableRegistry method that handles all the type checking internally
UNCOV
1174
    auto * reg = _data_manager->getTableRegistry();
×
1175
    if (!reg) {
×
1176
        qDebug() << "TableRegistry not available";
×
1177
        return false;
×
1178
    }
1179

1180
    bool success = reg->addColumnToBuilder(builder, column_info);
×
1181
    if (!success) {
×
UNCOV
1182
        qDebug() << "Failed to add column to builder:" << QString::fromStdString(column_info.name);
×
1183
    }
1184

UNCOV
1185
    return success;
×
1186
}
1187

1188
void TableDesignerWidget::updateIntervalSettingsVisibility() {
42✔
1189
    if (!ui->interval_settings_group) {
42✔
UNCOV
1190
        return;
×
1191
    }
1192

1193
    QString selected_key = ui->row_data_source_combo->currentText();
42✔
1194
    if (selected_key.isEmpty()) {
42✔
UNCOV
1195
        ui->interval_settings_group->setVisible(false);
×
UNCOV
1196
        if (ui->capture_range_spinbox) {
×
1197
            ui->capture_range_spinbox->setEnabled(false);
×
1198
        }
UNCOV
1199
        return;
×
1200
    }
1201

1202
    if (!_data_manager) {
42✔
UNCOV
1203
        ui->interval_settings_group->setVisible(false);
×
UNCOV
1204
        if (ui->capture_range_spinbox) {
×
UNCOV
1205
            ui->capture_range_spinbox->setEnabled(false);
×
1206
        }
1207
        return;
×
1208
    }
1209

1210
    // Check if the selected source is an interval series
1211
    if (selected_key.startsWith("Intervals: ")) {
42✔
1212
        ui->interval_settings_group->setVisible(true);
22✔
1213

1214
        // Enable/disable capture range based on interval setting
1215
        if (ui->capture_range_spinbox) {
22✔
1216
            bool use_interval_itself = isIntervalItselfSelected();
22✔
1217
            ui->capture_range_spinbox->setEnabled(!use_interval_itself);
22✔
1218
        }
1219
    } else {
1220
        ui->interval_settings_group->setVisible(false);
20✔
1221
        if (ui->capture_range_spinbox) {
20✔
1222
            ui->capture_range_spinbox->setEnabled(false);
20✔
1223
        }
1224
    }
1225
}
42✔
1226

1227
int TableDesignerWidget::getCaptureRange() const {
40✔
1228
    if (ui->capture_range_spinbox) {
40✔
1229
        return ui->capture_range_spinbox->value();
40✔
1230
    }
UNCOV
1231
    return 30000;// Default value
×
1232
}
1233

1234
void TableDesignerWidget::setCaptureRange(int value) {
44✔
1235
    if (ui->capture_range_spinbox) {
44✔
1236
        ui->capture_range_spinbox->blockSignals(true);
44✔
1237
        ui->capture_range_spinbox->setValue(value);
44✔
1238
        ui->capture_range_spinbox->blockSignals(false);
44✔
1239
    }
1240
}
44✔
1241

1242
bool TableDesignerWidget::isIntervalBeginningSelected() const {
40✔
1243
    if (ui->interval_beginning_radio) {
40✔
1244
        return ui->interval_beginning_radio->isChecked();
40✔
1245
    }
UNCOV
1246
    return true;// Default to beginning
×
1247
}
1248

1249
bool TableDesignerWidget::isIntervalItselfSelected() const {
64✔
1250
    if (ui->interval_itself_radio) {
64✔
1251
        return ui->interval_itself_radio->isChecked();
64✔
1252
    }
UNCOV
1253
    return false;// Default to not selected
×
1254
}
1255

1256
void TableDesignerWidget::triggerPreviewDebounced() {
148✔
1257
    if (_preview_debounce_timer) _preview_debounce_timer->start();
148✔
1258
    // Also trigger an immediate rebuild to support non-interactive/test contexts
1259
    rebuildPreviewNow();
148✔
1260
}
148✔
1261

1262
void TableDesignerWidget::rebuildPreviewNow() {
148✔
1263
    if (!_data_manager || !_table_viewer) return;
148✔
1264
    if (_current_table_id.isEmpty()) {
148✔
1265
        _table_viewer->clearTable();
67✔
1266
        return;
67✔
1267
    }
1268

1269
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
81✔
1270
    if (row_source.isEmpty()) {
81✔
1271
        _table_viewer->clearTable();
22✔
1272
        return;
22✔
1273
    }
1274

1275
    // Get enabled column infos from the computers tree
1276
    auto column_infos = getEnabledColumnInfos();
59✔
1277
    if (column_infos.empty()) {
59✔
1278
        _table_viewer->clearTable();
42✔
1279
        return;
42✔
1280
    }
1281

1282
    // Create row selector for the entire dataset
1283
    auto selector = createRowSelector(row_source);
17✔
1284
    if (!selector) {
17✔
UNCOV
1285
        _table_viewer->clearTable();
×
UNCOV
1286
        return;
×
1287
    }
1288

1289
    // Apply any saved column order for this table id
1290
    auto desiredOrder = _table_column_order.value(_current_table_id);
17✔
1291
    if (!desiredOrder.isEmpty()) {
17✔
1292
        std::vector<ColumnInfo> reordered;
10✔
1293
        reordered.reserve(column_infos.size());
10✔
1294
        for (auto const & name: desiredOrder) {
25✔
1295
            auto it = std::find_if(column_infos.begin(), column_infos.end(), [&](ColumnInfo const & ci) { return QString::fromStdString(ci.name) == name; });
37✔
1296
            if (it != column_infos.end()) {
15✔
1297
                reordered.push_back(*it);
15✔
1298
            }
1299
        }
1300
        for (auto const & ci: column_infos) {
29✔
1301
            if (std::find_if(reordered.begin(), reordered.end(), [&](ColumnInfo const & x) { return x.name == ci.name; }) == reordered.end()) {
46✔
1302
                reordered.push_back(ci);
4✔
1303
            }
1304
        }
1305
        column_infos = std::move(reordered);
10✔
1306
    }
10✔
1307

1308
    // Set up the table viewer with pagination
1309
    _table_viewer->setTableConfiguration(
85✔
1310
            std::move(selector),
17✔
1311
            std::move(column_infos),
17✔
1312
            _data_manager,
17✔
1313
            QString("Preview: %1").arg(_current_table_id));
34✔
1314

1315
    // Capture the current visual order from the viewer
1316
    QStringList currentOrder;
17✔
1317
    if (_table_viewer) {
17✔
1318
        auto * tv = _table_viewer->findChild<QTableView *>();
17✔
1319
        if (tv && tv->model()) {
17✔
1320
            auto * header = tv->horizontalHeader();
17✔
1321
            int cols = tv->model()->columnCount();
17✔
1322
            for (int v = 0; header && v < cols; ++v) {
43✔
1323
                int logical = header->logicalIndex(v);
26✔
1324
                auto name = tv->model()->headerData(logical, Qt::Horizontal, Qt::DisplayRole).toString();
26✔
1325
                currentOrder.push_back(name);
26✔
1326
            }
26✔
1327
        }
1328
    }
1329
    if (!currentOrder.isEmpty()) {
17✔
1330
        _table_column_order[_current_table_id] = currentOrder;
17✔
1331
    }
1332
}
123✔
1333

1334
void TableDesignerWidget::refreshComputersTree() {
76✔
1335
    if (!_data_manager) return;
76✔
1336

1337
    _updating_computers_tree = true;
76✔
1338

1339
    // Preserve previous checkbox states and custom column names
1340
    std::map<std::string, std::pair<Qt::CheckState, QString>> previous_states;
76✔
1341
    if (ui->computers_tree && ui->computers_tree->topLevelItemCount() > 0) {
76✔
1342
        for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
495✔
1343
            auto * data_source_item_old = ui->computers_tree->topLevelItem(i);
447✔
1344
            for (int j = 0; j < data_source_item_old->childCount(); ++j) {
1,901✔
1345
                auto * computer_item_old = data_source_item_old->child(j);
1,454✔
1346
                QString ds = computer_item_old->data(0, Qt::UserRole).toString();
1,454✔
1347
                QString cn = computer_item_old->data(1, Qt::UserRole).toString();
1,454✔
1348
                std::string key = (ds + "||" + cn).toStdString();
1,454✔
1349
                previous_states[key] = {computer_item_old->checkState(1), computer_item_old->text(2)};
1,454✔
1350
            }
1,454✔
1351
        }
1352
    }
1353

1354
    ui->computers_tree->clear();
76✔
1355
    ui->computers_tree->setHeaderLabels({"Data Source / Computer", "Enabled", "Column Name"});
380✔
1356

1357
    auto * registry = _data_manager->getTableRegistry();
76✔
1358
    if (!registry) {
76✔
UNCOV
1359
        _updating_computers_tree = false;
×
UNCOV
1360
        return;
×
1361
    }
1362

1363
    auto data_manager_extension = registry->getDataManagerExtension();
76✔
1364
    if (!data_manager_extension) {
76✔
UNCOV
1365
        _updating_computers_tree = false;
×
UNCOV
1366
        return;
×
1367
    }
1368

1369
    auto & computer_registry = registry->getComputerRegistry();
76✔
1370

1371
    // Get available data sources
1372
    auto data_sources = getAvailableDataSources();
76✔
1373

1374
    // Create tree structure: Data Source -> Computers
1375
    for (QString const & data_source: data_sources) {
781✔
1376
        auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
705✔
1377
        data_source_item->setText(0, data_source);
705✔
1378
        data_source_item->setFlags(Qt::ItemIsEnabled);
705✔
1379
        data_source_item->setExpanded(false);// Start collapsed
705✔
1380

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

1384
        if (!data_source_variant.has_value()) {
705✔
1385
            qDebug() << "Failed to create data source variant for:" << data_source;
311✔
1386
            continue;
311✔
1387
        }
1388

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

1392
        // Add compatible computers as children
1393
        for (auto const & computer_info: available_computers) {
2,688✔
1394
            auto * computer_item = new QTreeWidgetItem(data_source_item);
2,294✔
1395
            computer_item->setText(0, QString::fromStdString(computer_info.name));
2,294✔
1396
            computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
2,294✔
1397
            computer_item->setCheckState(1, Qt::Unchecked);
2,294✔
1398

1399
            // Generate default column name
1400
            QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
2,294✔
1401
            computer_item->setText(2, default_name);
2,294✔
1402
            computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
2,294✔
1403

1404
            // Store data source and computer name for later use
1405
            computer_item->setData(0, Qt::UserRole, data_source);
2,294✔
1406
            computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
2,294✔
1407

1408
            // Restore previous state if present
1409
            std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
2,294✔
1410
            auto it_prev = previous_states.find(prev_key);
2,294✔
1411
            if (it_prev != previous_states.end()) {
2,294✔
1412
                computer_item->setCheckState(1, it_prev->second.first);
1,451✔
1413
                if (!it_prev->second.second.isEmpty()) {
1,451✔
1414
                    computer_item->setText(2, it_prev->second.second);
1,451✔
1415
                }
1416
            }
1417
        }
2,294✔
1418
    }
705✔
1419

1420
    // Resize columns to content
1421
    ui->computers_tree->resizeColumnToContents(0);
76✔
1422
    ui->computers_tree->resizeColumnToContents(1);
76✔
1423
    ui->computers_tree->resizeColumnToContents(2);
76✔
1424

1425
    _updating_computers_tree = false;
76✔
1426

1427
    // Update preview after refresh
1428
    triggerPreviewDebounced();
76✔
1429
}
152✔
1430

1431
void TableDesignerWidget::setJsonTemplateFromCurrentState() {
3✔
1432
    if (!_table_json_widget) return;
3✔
1433
    // Build a minimal JSON template representing current UI state
1434
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
3✔
1435
    auto columns = getEnabledColumnInfos();
3✔
1436
    if (row_source.isEmpty() && columns.empty()) {
3✔
NEW
1437
        _table_json_widget->setJsonText("{}");
×
NEW
1438
        return;
×
1439
    }
1440

1441
    QString row_type;
3✔
1442
    QString row_source_name;
3✔
1443
    if (row_source.startsWith("TimeFrame: ")) {
3✔
NEW
1444
        row_type = "timestamp";
×
NEW
1445
        row_source_name = row_source.mid(11);
×
1446
    } else if (row_source.startsWith("Events: ")) {
3✔
NEW
1447
        row_type = "timestamp";
×
NEW
1448
        row_source_name = row_source.mid(8);
×
1449
    } else if (row_source.startsWith("Intervals: ")) {
3✔
1450
        row_type = "interval";
3✔
1451
        row_source_name = row_source.mid(11);
3✔
1452
    }
1453

1454
    QStringList column_entries;
3✔
1455
    for (auto const & c: columns) {
8✔
1456
        // Strip any internal prefixes for JSON to keep schema user-friendly
1457
        QString ds = QString::fromStdString(c.dataSourceName);
5✔
1458
        if (ds.startsWith("events:")) ds = ds.mid(7);
5✔
NEW
1459
        else if (ds.startsWith("intervals:"))
×
NEW
1460
            ds = ds.mid(10);
×
NEW
1461
        else if (ds.startsWith("analog:"))
×
NEW
1462
            ds = ds.mid(7);
×
1463

1464
        QString entry = QString(
15✔
1465
                                "{\n  \"name\": \"%1\",\n  \"description\": \"%2\",\n  \"data_source\": \"%3\",\n  \"computer\": \"%4\"%5\n}")
1466
                                .arg(QString::fromStdString(c.name))
20✔
1467
                                .arg(QString::fromStdString(c.description))
20✔
1468
                                .arg(ds)
20✔
1469
                                .arg(QString::fromStdString(c.computerName))
20✔
1470
                                .arg(c.parameters.empty() ? QString() : QString(",\n  \"parameters\": {}"));
10✔
1471
        column_entries << entry;
5✔
1472
    }
5✔
1473

1474
    QString table_name = _table_info_widget ? _table_info_widget->getName() : _current_table_id;
3✔
1475
    QString json = QString(
9✔
1476
                           "{\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}")
1477
                           .arg(_current_table_id)
12✔
1478
                           .arg(table_name)
12✔
1479
                           .arg(row_type)
12✔
1480
                           .arg(row_source_name)
12✔
1481
                           .arg(column_entries.join(",\n"));
6✔
1482

1483
    _table_json_widget->setJsonText(json);
3✔
1484
}
3✔
1485

1486
void TableDesignerWidget::applyJsonTemplateToUI(QString const & jsonText) {
7✔
1487
    // Very light-weight parser using Qt to extract essential fields.
1488
    // Assumes a schema similar to tests under computers *.test.cpp.
1489
    QJsonParseError err;
7✔
1490
    QByteArray bytes = jsonText.toUtf8();
7✔
1491
    QJsonDocument doc = QJsonDocument::fromJson(bytes, &err);
7✔
1492
    if (err.error != QJsonParseError::NoError || !doc.isObject()) {
7✔
1493
        // Compute line/column from byte offset if possible
1494
        int64_t offset = static_cast<int64_t>(err.offset);
1✔
1495
        int line = 1;
1✔
1496
        int col = 1;
1✔
1497
        // Avoid operator[] ambiguity on some compilers by using qsizetype and at()
1498
        qsizetype len = std::min<qsizetype>(bytes.size(), static_cast<qsizetype>(offset));
1✔
1499
        for (qsizetype i = 0; i < len; ++i) {
149✔
1500
            char ch = bytes.at(i);
148✔
1501
            if (ch == '\n') {
148✔
1502
                ++line;
6✔
1503
                col = 1;
6✔
1504
            } else {
1505
                ++col;
142✔
1506
            }
1507
        }
1508
        QString detail = err.error != QJsonParseError::NoError
1✔
1509
                                 ? QString("%1 (line %2, column %3)").arg(err.errorString()).arg(line).arg(col)
3✔
1510
                                 : QString("JSON root must be an object");
2✔
1511
        auto * box = new QMessageBox(this);
1✔
1512
        box->setIcon(QMessageBox::Critical);
1✔
1513
        box->setWindowTitle("Invalid JSON");
1✔
1514
        box->setText(QString("JSON format is invalid: %1").arg(detail));
1✔
1515
        box->setAttribute(Qt::WA_DeleteOnClose);
1✔
1516
        box->show();
1✔
1517
        return;
1✔
1518
    }
1✔
1519
    auto obj = doc.object();
6✔
1520
    if (!obj.contains("tables") || !obj["tables"].isArray()) {
6✔
UNCOV
1521
        auto * box = new QMessageBox(this);
×
UNCOV
1522
        box->setIcon(QMessageBox::Critical);
×
UNCOV
1523
        box->setWindowTitle("Invalid JSON");
×
UNCOV
1524
        box->setText("Missing required key: tables (array)");
×
UNCOV
1525
        box->setAttribute(Qt::WA_DeleteOnClose);
×
UNCOV
1526
        box->show();
×
UNCOV
1527
        return;
×
1528
    }
1529
    auto tables = obj["tables"].toArray();
6✔
1530
    if (tables.isEmpty() || !tables[0].isObject()) return;
6✔
1531
    auto table = tables[0].toObject();
6✔
1532

1533
    // Row selector
1534
    QStringList errors;
6✔
1535
    QString rs_type;
6✔
1536
    QString rs_source;
6✔
1537
    if (table.contains("row_selector") && table["row_selector"].isObject()) {
6✔
1538
        auto rs = table["row_selector"].toObject();
5✔
1539
        rs_type = rs.value("type").toString();
5✔
1540
        rs_source = rs.value("source").toString();
5✔
1541
        if (rs_type.isEmpty() || rs_source.isEmpty()) {
5✔
UNCOV
1542
            errors << "Missing required keys in row_selector: 'type' and/or 'source'";
×
1543
        } else {
1544
            // Validate existence
1545
            bool source_ok = false;
5✔
1546
            if (rs_type == "interval") {
5✔
1547
                source_ok = (_data_manager && _data_manager->getData<DigitalIntervalSeries>(rs_source.toStdString()) != nullptr);
5✔
UNCOV
1548
            } else if (rs_type == "timestamp") {
×
NEW
1549
                source_ok = (_data_manager && (_data_manager->getTime(TimeKey(rs_source.toStdString())) != nullptr ||
×
NEW
1550
                                               _data_manager->getData<DigitalEventSeries>(rs_source.toStdString()) != nullptr));
×
1551
            } else {
1552
                errors << QString("Unsupported row_selector type: %1").arg(rs_type);
×
1553
            }
1554
            if (!source_ok) {
5✔
1555
                errors << QString("Row selector data key not found in DataManager: %1").arg(rs_source);
1✔
1556
            } else {
1557
                // Apply selection to UI
1558
                QString entry;
4✔
1559
                if (rs_type == "interval") {
4✔
1560
                    entry = QString("Intervals: %1").arg(rs_source);
4✔
UNCOV
1561
                } else if (rs_type == "timestamp") {
×
1562
                    // Prefer TimeFrame, fallback to Events
UNCOV
1563
                    entry = QString("TimeFrame: %1").arg(rs_source);
×
UNCOV
1564
                    int idx_tf = ui->row_data_source_combo->findText(entry);
×
UNCOV
1565
                    if (idx_tf < 0) entry = QString("Events: %1").arg(rs_source);
×
1566
                }
1567
                int idx = ui->row_data_source_combo->findText(entry);
4✔
1568
                if (idx >= 0) {
4✔
1569
                    ui->row_data_source_combo->setCurrentIndex(idx);
4✔
1570
                    // Ensure computers tree reflects this row selector before enabling columns
1571
                    refreshComputersTree();
4✔
1572
                } else {
UNCOV
1573
                    errors << QString("Row selector entry not available in UI: %1").arg(entry);
×
1574
                }
1575
            }
4✔
1576
        }
1577
    } else {
5✔
1578
        errors << "Missing required key: row_selector (object)";
1✔
1579
    }
1580

1581
    // Columns: enable matching computers and set column names
1582
    if (table.contains("columns") && table["columns"].isArray()) {
6✔
1583
        auto cols = table["columns"].toArray();
6✔
1584
        auto * tree = ui->computers_tree;
6✔
1585
        // Avoid recursive preview rebuilds while we toggle many items
1586
        bool prevBlocked = tree->blockSignals(true);
6✔
1587
        for (auto const & cval: cols) {
12✔
1588
            if (!cval.isObject()) continue;
6✔
1589
            auto cobj = cval.toObject();
6✔
1590
            QString data_source = cobj.value("data_source").toString();
6✔
1591
            QString computer = cobj.value("computer").toString();
6✔
1592
            QString name = cobj.value("name").toString();
6✔
1593
            if (data_source.isEmpty() || computer.isEmpty() || name.isEmpty()) {
6✔
UNCOV
1594
                errors << "Missing required keys in column: 'name', 'data_source', and 'computer'";
×
UNCOV
1595
                continue;
×
1596
            }
1597
            // Validate data source existence
1598
            bool has_ds = (_data_manager && (_data_manager->getData<DigitalEventSeries>(data_source.toStdString()) != nullptr ||
19✔
1599
                                             _data_manager->getData<DigitalIntervalSeries>(data_source.toStdString()) != nullptr ||
7✔
1600
                                             _data_manager->getData<AnalogTimeSeries>(data_source.toStdString()) != nullptr));
13✔
1601
            if (!has_ds) {
6✔
1602
                errors << QString("Data key not found in DataManager: %1").arg(data_source);
1✔
1603
            }
1604
            // Validate computer exists
1605
            bool computer_exists = false;
6✔
1606
            if (_data_manager) {
6✔
1607
                if (auto * reg = _data_manager->getTableRegistry()) {
6✔
1608
                    auto & cr = reg->getComputerRegistry();
6✔
1609
                    computer_exists = cr.findComputerInfo(computer.toStdString());
6✔
1610
                }
1611
            }
1612
            if (!computer_exists) {
6✔
1613
                errors << QString("Requested computer does not exist: %1").arg(computer);
2✔
1614
            }
1615
            // Validate compatibility (heuristic)
1616
            bool type_event = false, type_interval = false, type_analog = false;
6✔
1617
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
60✔
1618
                auto * ds_item = tree->topLevelItem(i);
54✔
1619
                QString ds_text = ds_item->text(0);
54✔
1620
                if (ds_text.contains(data_source)) {
54✔
1621
                    if (ds_text.startsWith("Events: ")) type_event = true;
5✔
NEW
1622
                    else if (ds_text.startsWith("Intervals: "))
×
NEW
1623
                        type_interval = true;
×
NEW
1624
                    else if (ds_text.startsWith("analog:"))
×
NEW
1625
                        type_analog = true;
×
1626
                }
1627
            }
54✔
1628
            QString ds_repr;
6✔
1629
            if (type_event) ds_repr = QString("Events: %1").arg(data_source);
6✔
1630
            else if (type_interval)
1✔
NEW
1631
                ds_repr = QString("Intervals: %1").arg(data_source);
×
1632
            else if (type_analog)
1✔
NEW
1633
                ds_repr = QString("analog:%1").arg(data_source);
×
1634
            if (!ds_repr.isEmpty() && !isComputerCompatibleWithDataSource(computer.toStdString(), ds_repr)) {
6✔
1635
                errors << QString("Computer '%1' is not valid for data source type requested (%2)").arg(computer, ds_repr);
2✔
1636
            }
1637

1638
            // Find matching tree item with strict preference
1639
            QString exact_events = QString("Events: %1").arg(data_source);
6✔
1640
            QString exact_intervals = QString("Intervals: %1").arg(data_source);
6✔
1641
            QString exact_analog = QString("analog:%1").arg(data_source);
6✔
1642
            QTreeWidgetItem * matched_ds = nullptr;
6✔
1643
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
40✔
1644
                auto * ds_item = tree->topLevelItem(i);
39✔
1645
                QString t = ds_item->text(0);
39✔
1646
                if (t == exact_events || t == exact_intervals || t == exact_analog) {
39✔
1647
                    matched_ds = ds_item;
5✔
1648
                    break;
5✔
1649
                }
1650
            }
39✔
1651
            if (!matched_ds) {
6✔
1652
                for (int i = 0; i < tree->topLevelItemCount(); ++i) {
10✔
1653
                    auto * ds_item = tree->topLevelItem(i);
9✔
1654
                    QString t = ds_item->text(0);
9✔
1655
                    if (t.contains(data_source) || t.endsWith(data_source)) {
9✔
NEW
1656
                        matched_ds = ds_item;
×
NEW
1657
                        break;
×
1658
                    }
1659
                }
9✔
1660
            }
1661
            if (matched_ds) {
6✔
1662
                for (int j = 0; j < matched_ds->childCount(); ++j) {
20✔
1663
                    auto * comp_item = matched_ds->child(j);
15✔
1664
                    QString comp_text = comp_item->text(0).trimmed();
15✔
1665
                    if (comp_text == computer || comp_text.contains(computer)) {
15✔
1666
                        comp_item->setCheckState(1, Qt::Checked);
3✔
1667
                        if (!name.isEmpty()) comp_item->setText(2, name);
3✔
1668
                    }
1669
                }
15✔
1670
            } else {
1671
                errors << QString("Data source not found in tree: %1").arg(data_source);
1✔
1672
            }
1673
        }
6✔
1674
        tree->blockSignals(prevBlocked);
6✔
1675
        if (!errors.isEmpty()) {
6✔
1676
            auto * box = new QMessageBox(this);
4✔
1677
            box->setIcon(QMessageBox::Critical);
4✔
1678
            box->setWindowTitle("Invalid Table JSON");
4✔
1679
            box->setText(errors.join("\n"));
4✔
1680
            box->setAttribute(Qt::WA_DeleteOnClose);
4✔
1681
            box->show();
4✔
1682
            return;
4✔
1683
        }
1684
        triggerPreviewDebounced();
2✔
1685
    }
6✔
1686
}
36✔
1687

1688
void TableDesignerWidget::onComputersTreeItemChanged() {
17,492✔
1689
    if (_updating_computers_tree) return;
17,492✔
1690

1691
    // Trigger preview update when checkbox states change
1692
    triggerPreviewDebounced();
15✔
1693
}
1694

1695
void TableDesignerWidget::onComputersTreeItemEdited(QTreeWidgetItem * item, int column) {
17,492✔
1696
    if (_updating_computers_tree) return;
17,492✔
1697

1698
    // Only respond to column name edits (column 2)
1699
    if (column == 2) {
15✔
1700
        // Column name was edited, trigger preview update
1701
        triggerPreviewDebounced();
1✔
1702
    }
1703
}
1704

1705
std::vector<ColumnInfo> TableDesignerWidget::getEnabledColumnInfos() const {
70✔
1706
    std::vector<ColumnInfo> column_infos;
70✔
1707

1708
    if (!ui->computers_tree) return column_infos;
70✔
1709

1710
    // Iterate through all data source items
1711
    for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
721✔
1712
        auto * data_source_item = ui->computers_tree->topLevelItem(i);
651✔
1713

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

1718
            // Check if this computer is enabled
1719
            if (computer_item->checkState(1) == Qt::Checked) {
2,128✔
1720
                QString data_source = computer_item->data(0, Qt::UserRole).toString();
44✔
1721
                QString computer_name = computer_item->data(1, Qt::UserRole).toString();
44✔
1722
                QString column_name = computer_item->text(2);
44✔
1723

1724
                if (column_name.isEmpty()) {
44✔
UNCOV
1725
                    column_name = generateDefaultColumnName(data_source, computer_name);
×
1726
                }
1727

1728
                // Create ColumnInfo (use raw key without UI prefixes)
1729
                QString source_key = data_source;
44✔
1730
                if (source_key.startsWith("Events: ")) {
44✔
1731
                    source_key = QString("events:%1").arg(source_key.mid(8));
43✔
1732
                } else if (source_key.startsWith("Intervals: ")) {
1✔
1733
                    source_key = QString("intervals:%1").arg(source_key.mid(11));
1✔
UNCOV
1734
                } else if (source_key.startsWith("analog:")) {
×
NEW
1735
                    source_key = source_key;// already prefixed
×
UNCOV
1736
                } else if (source_key.startsWith("TimeFrame: ")) {
×
1737
                    // TimeFrame used only for row selector; columns require concrete sources
UNCOV
1738
                    source_key = source_key.mid(11);
×
1739
                }
1740

1741
                ColumnInfo info(column_name.toStdString(),
132✔
1742
                                QString("Column from %1 using %2").arg(data_source, computer_name).toStdString(),
88✔
1743
                                source_key.toStdString(),
88✔
1744
                                computer_name.toStdString());
220✔
1745

1746
                // Set output type based on computer info
1747
                if (auto * registry = _data_manager->getTableRegistry()) {
44✔
1748
                    auto & computer_registry = registry->getComputerRegistry();
44✔
1749
                    auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
44✔
1750
                    if (computer_info) {
44✔
1751
                        info.outputType = computer_info->outputType;
44✔
1752
                        info.outputTypeName = computer_info->outputTypeName;
44✔
1753
                        info.isVectorType = computer_info->isVectorType;
44✔
1754
                        if (info.isVectorType) {
44✔
1755
                            info.elementType = computer_info->elementType;
6✔
1756
                            info.elementTypeName = computer_info->elementTypeName;
6✔
1757
                        }
1758
                    }
1759
                }
1760

1761
                column_infos.push_back(std::move(info));
44✔
1762
            }
44✔
1763
        }
1764
    }
1765

1766
    return column_infos;
70✔
UNCOV
1767
}
×
1768

1769
bool TableDesignerWidget::isComputerCompatibleWithDataSource(std::string const & computer_name, QString const & data_source) const {
5✔
1770
    if (!_data_manager) return false;
5✔
1771

1772
    auto * registry = _data_manager->getTableRegistry();
5✔
1773
    if (!registry) return false;
5✔
1774

1775
    auto & computer_registry = registry->getComputerRegistry();
5✔
1776
    auto computer_info = computer_registry.findComputerInfo(computer_name);
5✔
1777
    if (!computer_info) return false;
5✔
1778

1779
    // Basic compatibility check based on data source type and common computer patterns
1780
    if (data_source.startsWith("Events: ")) {
3✔
1781
        // Event-based computers typically have "Event" in their name
1782
        return computer_name.find("Event") != std::string::npos;
3✔
UNCOV
1783
    } else if (data_source.startsWith("Intervals: ")) {
×
1784
        // Interval-based computers typically work with intervals or events
UNCOV
1785
        return computer_name.find("Event") != std::string::npos ||
×
UNCOV
1786
               computer_name.find("Interval") != std::string::npos;
×
UNCOV
1787
    } else if (data_source.startsWith("analog:")) {
×
1788
        // Analog-based computers typically have "Analog" in their name
UNCOV
1789
        return computer_name.find("Analog") != std::string::npos;
×
UNCOV
1790
    } else if (data_source.startsWith("TimeFrame: ")) {
×
1791
        // TimeFrame-based computers - generally most computers can work with timestamps
UNCOV
1792
        return computer_name.find("Timestamp") != std::string::npos ||
×
UNCOV
1793
               computer_name.find("Time") != std::string::npos;
×
1794
    }
1795

1796
    // Default: assume compatibility for unrecognized patterns
UNCOV
1797
    return true;
×
1798
}
1799

1800
QString TableDesignerWidget::generateDefaultColumnName(QString const & data_source, QString const & computer_name) const {
2,294✔
1801
    QString source_name = data_source;
2,294✔
1802

1803
    // Extract the actual name from prefixed data sources
1804
    if (source_name.startsWith("Events: ")) {
2,294✔
1805
        source_name = source_name.mid(8);
462✔
1806
    } else if (source_name.startsWith("Intervals: ")) {
1,832✔
1807
        source_name = source_name.mid(11);
616✔
1808
    } else if (source_name.startsWith("analog:")) {
1,216✔
1809
        source_name = source_name.mid(7);
1,216✔
UNCOV
1810
    } else if (source_name.startsWith("TimeFrame: ")) {
×
UNCOV
1811
        source_name = source_name.mid(11);
×
1812
    }
1813

1814
    // Create a concise name
1815
    return QString("%1_%2").arg(source_name, computer_name);
6,882✔
1816
}
2,294✔
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