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

paulmthompson / WhiskerToolbox / 17570184222

09 Sep 2025 02:46AM UTC coverage: 71.734% (+0.3%) from 71.483%
17570184222

push

github

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

36945 of 51503 relevant lines covered (71.73%)

1295.82 hits per line

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

65.07
/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)
15✔
60
    : QWidget(parent),
61
      ui(new Ui::TableDesignerWidget),
30✔
62
      _data_manager(std::move(data_manager)) {
15✔
63

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

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

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

90
    // Connect table viewer signals for better integration
91
    connect(_table_viewer, &TableViewerWidget::rowScrolled, this, [this](size_t row_index) {
15✔
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();
15✔
98
    // Initialize UI to a clean state, then populate controls
99
    clearUI();
15✔
100
    refreshTableCombo();
15✔
101
    refreshRowDataSourceCombo();
15✔
102
    refreshComputersTree();
15✔
103

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

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

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

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

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

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

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

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

161

162
void TableDesignerWidget::connectSignals() {
15✔
163
    // Table selection signals
164
    connect(ui->table_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
45✔
165
            this, &TableDesignerWidget::onTableSelectionChanged);
30✔
166
    connect(ui->new_table_btn, &QPushButton::clicked,
45✔
167
            this, &TableDesignerWidget::onCreateNewTable);
30✔
168
    connect(ui->delete_table_btn, &QPushButton::clicked,
45✔
169
            this, &TableDesignerWidget::onDeleteTable);
30✔
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),
45✔
175
            this, &TableDesignerWidget::onRowDataSourceChanged);
30✔
176
    connect(ui->capture_range_spinbox, QOverload<int>::of(&QSpinBox::valueChanged),
45✔
177
            this, &TableDesignerWidget::onCaptureRangeChanged);
30✔
178
    connect(ui->interval_beginning_radio, &QRadioButton::toggled,
45✔
179
            this, &TableDesignerWidget::onIntervalSettingChanged);
30✔
180
    connect(ui->interval_end_radio, &QRadioButton::toggled,
45✔
181
            this, &TableDesignerWidget::onIntervalSettingChanged);
30✔
182
    connect(ui->interval_itself_radio, &QRadioButton::toggled,
45✔
183
            this, &TableDesignerWidget::onIntervalSettingChanged);
30✔
184

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

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

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

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

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

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

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

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

246
    qDebug() << "Selected table:" << table_id;
12✔
247
}
27✔
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() {
37✔
296
    QString selected = ui->row_data_source_combo->currentText();
37✔
297
    if (selected.isEmpty()) {
37✔
298
        ui->row_info_label->setText("No row source selected");
13✔
299
        return;
13✔
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) {
24✔
305
        if (auto * reg = _data_manager->getTableRegistry()) {
7✔
306
            reg->updateTableRowSource(_current_table_id.toStdString(), selected.toStdString());
7✔
307
        }
308
    }
309

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

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

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

319
    qDebug() << "Row data source changed to:" << selected;
24✔
320
    triggerPreviewDebounced();
24✔
321
}
37✔
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() {
×
333
    // Update the info label to reflect the new interval setting
334
    QString selected = ui->row_data_source_combo->currentText();
×
335
    if (!selected.isEmpty()) {
×
336
        updateRowInfoLabel(selected);
×
337
    }
338

339
    // Update capture range visibility based on interval setting
340
    updateIntervalSettingsVisibility();
×
341
    triggerPreviewDebounced();
×
342
}
×
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::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::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::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
                try {
641
                    auto const & vals = view->getColumnValues<double>(names[c].c_str());
×
642
                    if (r < vals.size()) file << vals[r];
×
643
                    else
644
                        file << "NaN";
×
645
                } catch (...) {
×
646
                    file << "NaN";
×
647
                }
×
648
            }
649
            file << eol;
×
650
        }
651
        file.close();
×
652
        updateBuildStatus(QString("Exported CSV: %1").arg(filename));
×
653
    } catch (std::exception const & e) {
×
654
        updateBuildStatus(QString("Export failed: %1").arg(e.what()), true);
×
655
    }
×
656
}
×
657

658
QString TableDesignerWidget::promptSaveCsvFilename() const {
×
659
    return QFileDialog::getSaveFileName(const_cast<TableDesignerWidget *>(this), "Export Table to CSV", QString(), "CSV Files (*.csv)");
×
660
}
661

662
void TableDesignerWidget::onSaveTableInfo() {
×
663
    if (_current_table_id.isEmpty()) {
×
664
        return;
×
665
    }
666

667
    QString name = _table_info_widget ? _table_info_widget->getName() : QString();
×
668
    QString description = _table_info_widget ? _table_info_widget->getDescription() : QString();
×
669

670
    if (name.isEmpty()) {
×
671
        QMessageBox::warning(this, "Error", "Table name cannot be empty");
×
672
        return;
×
673
    }
674

675
    if (auto * reg = _data_manager->getTableRegistry(); reg && reg->updateTableInfo(_current_table_id.toStdString(), name.toStdString(), description.toStdString())) {
×
676
        updateBuildStatus("Table information saved");
×
677
        // Refresh the combo to show updated name
678
        refreshTableCombo();
×
679
        // Restore selection
680
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
681
            if (ui->table_combo->itemData(i).toString() == _current_table_id) {
×
682
                ui->table_combo->setCurrentIndex(i);
×
683
                break;
×
684
            }
685
        }
686
    } else {
687
        QMessageBox::warning(this, "Error", "Failed to save table information");
×
688
    }
689
}
×
690

691
void TableDesignerWidget::onTableManagerTableCreated(QString const & table_id) {
11✔
692
    refreshTableCombo();
11✔
693
    qDebug() << "Table created signal received:" << table_id;
11✔
694
}
11✔
695

696
void TableDesignerWidget::onTableManagerTableRemoved(QString const & table_id) {
×
697
    refreshTableCombo();
×
698
    if (_current_table_id == table_id) {
×
699
        _current_table_id.clear();
×
700
        clearUI();
×
701
    }
702
    qDebug() << "Table removed signal received:" << table_id;
×
703
}
×
704

705
void TableDesignerWidget::onTableManagerTableInfoUpdated(QString const & table_id) {
10✔
706
    if (_current_table_id == table_id && !_loading_column_configuration) {
10✔
707
        loadTableInfo(table_id);
10✔
708
    }
709
    qDebug() << "Table info updated signal received:" << table_id;
10✔
710
}
10✔
711

712
void TableDesignerWidget::refreshTableCombo() {
26✔
713
    ui->table_combo->clear();
26✔
714

715
    auto * reg = _data_manager->getTableRegistry();
26✔
716
    auto table_infos = reg ? reg->getAllTableInfo() : std::vector<TableInfo>{};
26✔
717
    for (auto const & info: table_infos) {
40✔
718
        ui->table_combo->addItem(QString::fromStdString(info.name), QString::fromStdString(info.id));
14✔
719
    }
720

721
    if (ui->table_combo->count() == 0) {
26✔
722
        ui->table_combo->addItem("(No tables available)", "");
15✔
723
    }
724
}
52✔
725

726
void TableDesignerWidget::refreshRowDataSourceCombo() {
17✔
727
    ui->row_data_source_combo->clear();
17✔
728

729
    if (!_data_manager) {
17✔
730
        qDebug() << "refreshRowDataSourceCombo: No table manager";
×
731
        return;
×
732
    }
733

734
    auto data_sources = getAvailableDataSources();
17✔
735
    qDebug() << "refreshRowDataSourceCombo: Found" << data_sources.size() << "data sources:" << data_sources;
17✔
736

737
    for (QString const & source: data_sources) {
171✔
738
        // Only include valid row sources in this combo
739
        if (source.startsWith("TimeFrame: ") || source.startsWith("Events: ") || source.startsWith("Intervals: ")) {
154✔
740
            ui->row_data_source_combo->addItem(source);
120✔
741
        }
742
    }
743

744
    if (ui->row_data_source_combo->count() == 0) {
17✔
745
        ui->row_data_source_combo->addItem("(No data sources available)");
×
746
        qDebug() << "refreshRowDataSourceCombo: No data sources available";
×
747
    }
748
}
17✔
749

750

751
void TableDesignerWidget::loadTableInfo(QString const & table_id) {
22✔
752
    if (table_id.isEmpty() || !_data_manager) {
22✔
753
        clearUI();
×
754
        return;
×
755
    }
756

757
    auto * reg = _data_manager->getTableRegistry();
22✔
758
    auto info = reg ? reg->getTableInfo(table_id.toStdString()) : TableInfo{};
22✔
759
    if (info.id.empty()) {
22✔
760
        clearUI();
×
761
        return;
×
762
    }
763

764
    // Load table information
765
    if (_table_info_widget) {
22✔
766
        _table_info_widget->setName(QString::fromStdString(info.name));
22✔
767
        _table_info_widget->setDescription(QString::fromStdString(info.description));
22✔
768
    }
769

770
    // Load row source if available
771
    if (!info.rowSourceName.empty()) {
22✔
772
        int row_index = ui->row_data_source_combo->findText(QString::fromStdString(info.rowSourceName));
11✔
773
        if (row_index >= 0) {
11✔
774
            // Block signals to prevent circular dependency when loading table info
775
            ui->row_data_source_combo->blockSignals(true);
11✔
776
            ui->row_data_source_combo->setCurrentIndex(row_index);
11✔
777
            ui->row_data_source_combo->blockSignals(false);
11✔
778

779
            // Manually update the info label without triggering the signal handler
780
            updateRowInfoLabel(QString::fromStdString(info.rowSourceName));
11✔
781

782
            // Update interval settings visibility
783
            updateIntervalSettingsVisibility();
11✔
784

785
            // Since signals were blocked, this will ensure the tree is refreshed
786
            // when the computers tree is populated later in this function
787
        }
788
    }
789

790
    // Clear old column list (deprecated)
791
    // The computers tree will be populated based on available data sources
792
    refreshComputersTree();
22✔
793

794
    updateBuildStatus(QString("Loaded table: %1").arg(QString::fromStdString(info.name)));
22✔
795
    triggerPreviewDebounced();
22✔
796
}
22✔
797

798
void TableDesignerWidget::clearUI() {
41✔
799
    _current_table_id.clear();
41✔
800

801
    // Clear table info
802
    if (_table_info_widget) {
41✔
803
        _table_info_widget->setName("");
41✔
804
        _table_info_widget->setDescription("");
41✔
805
    }
806

807
    // Clear row source
808
    ui->row_data_source_combo->setCurrentIndex(-1);
41✔
809
    ui->row_info_label->setText("No row source selected");
41✔
810

811
    // Reset capture range and interval settings
812
    setCaptureRange(30000);// Default value
41✔
813
    if (ui->interval_beginning_radio) {
41✔
814
        ui->interval_beginning_radio->setChecked(true);
41✔
815
    }
816
    if (ui->interval_itself_radio) {
41✔
817
        ui->interval_itself_radio->setChecked(false);
41✔
818
    }
819
    if (ui->interval_settings_group) {
41✔
820
        ui->interval_settings_group->setVisible(false);
41✔
821
    }
822

823
    // Clear computers tree
824
    if (ui->computers_tree) {
41✔
825
        ui->computers_tree->clear();
41✔
826
    }
827

828
    // Disable controls
829
    ui->delete_table_btn->setEnabled(false);
41✔
830
    // Table info section is controlled separately
831
    ui->build_table_btn->setEnabled(false);
41✔
832
    if (auto gb = this->findChild<QGroupBox*>("row_source_group")) gb->setEnabled(false);
82✔
833
    if (auto gb = this->findChild<QGroupBox*>("column_design_group")) gb->setEnabled(false);
82✔
834
    if (_table_info_section) _table_info_section->setEnabled(false);
41✔
835

836
    updateBuildStatus("No table selected");
41✔
837
    if (_table_viewer) _table_viewer->clearTable();
41✔
838
}
41✔
839

840
void TableDesignerWidget::updateBuildStatus(QString const & message, bool is_error) {
78✔
841
    ui->build_status_label->setText(message);
78✔
842

843
    if (is_error) {
78✔
844
        ui->build_status_label->setStyleSheet("QLabel { color: red; font-weight: bold; }");
×
845
    } else {
846
        ui->build_status_label->setStyleSheet("QLabel { color: green; }");
78✔
847
    }
848
}
78✔
849

850
QStringList TableDesignerWidget::getAvailableDataSources() const {
80✔
851
    QStringList sources;
80✔
852

853
    if (!_data_manager) {
80✔
854
        qDebug() << "getAvailableDataSources: No table manager";
×
855
        return sources;
×
856
    }
857

858
    auto * reg = _data_manager->getTableRegistry();
80✔
859
    auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
80✔
860
    if (!data_manager_extension) {
80✔
861
        qDebug() << "getAvailableDataSources: No data manager extension";
×
862
        return sources;
×
863
    }
864

865
    if (!_data_manager) {
80✔
866
        qDebug() << "getAvailableDataSources: No data manager";
×
867
        return sources;
×
868
    }
869

870
    // Add TimeFrame keys as potential row sources
871
    // TimeFrames can define intervals for analysis
872
    auto timeframe_keys = _data_manager->getTimeFrameKeys();
80✔
873
    qDebug() << "getAvailableDataSources: TimeFrame keys:" << timeframe_keys.size();
80✔
874
    for (auto const & key: timeframe_keys) {
400✔
875
        QString source = QString("TimeFrame: %1").arg(QString::fromStdString(key.str()));
320✔
876
        sources << source;
320✔
877
        qDebug() << "  Added TimeFrame:" << source;
320✔
878
    }
320✔
879

880
    // Add DigitalEventSeries keys as potential row sources
881
    // Events can be used to define analysis windows or timestamps
882
    auto event_keys = _data_manager->getKeys<DigitalEventSeries>();
80✔
883
    qDebug() << "getAvailableDataSources: Event keys:" << event_keys.size();
80✔
884
    for (auto const & key: event_keys) {
243✔
885
        QString source = QString("Events: %1").arg(QString::fromStdString(key));
163✔
886
        sources << source;
163✔
887
        qDebug() << "  Added Events:" << source;
163✔
888
    }
163✔
889

890
    // Add DigitalIntervalSeries keys as potential row sources
891
    // Intervals directly define analysis windows
892
    auto interval_keys = _data_manager->getKeys<DigitalIntervalSeries>();
80✔
893
    qDebug() << "getAvailableDataSources: Interval keys:" << interval_keys.size();
80✔
894
    for (auto const & key: interval_keys) {
160✔
895
        QString source = QString("Intervals: %1").arg(QString::fromStdString(key));
80✔
896
        sources << source;
80✔
897
        qDebug() << "  Added Intervals:" << source;
80✔
898
    }
80✔
899

900
    // Add AnalogTimeSeries keys as data sources (for computers; not row selectors)
901
    auto analog_keys = _data_manager->getKeys<AnalogTimeSeries>();
80✔
902
    qDebug() << "getAvailableDataSources: Analog keys:" << analog_keys.size();
80✔
903
    for (auto const & key: analog_keys) {
240✔
904
        QString source = QString("analog:%1").arg(QString::fromStdString(key));
160✔
905
        sources << source;
160✔
906
        qDebug() << "  Added Analog:" << source;
160✔
907
    }
160✔
908

909
    qDebug() << "getAvailableDataSources: Total sources found:" << sources.size();
80✔
910

911
    return sources;
80✔
912
}
80✔
913

914
std::pair<std::optional<DataSourceVariant>, RowSelectorType>
915
TableDesignerWidget::createDataSourceVariant(QString const & data_source_string,
569✔
916
                                             std::shared_ptr<DataManagerExtension> data_manager_extension) const {
917
    std::optional<DataSourceVariant> result;
569✔
918
    RowSelectorType row_selector_type = RowSelectorType::IntervalBased;
569✔
919

920
    if (data_source_string.startsWith("TimeFrame: ")) {
569✔
921
        // TimeFrames are used with TimestampSelector for rows; no concrete data source needed
922
        row_selector_type = RowSelectorType::Timestamp;
252✔
923

924
    } else if (data_source_string.startsWith("Events: ")) {
317✔
925
        QString source_name = data_source_string.mid(8);// Remove "Events: " prefix
128✔
926
        // Event-based computers in the registry operate with interval rows
927
        row_selector_type = RowSelectorType::IntervalBased;
128✔
928

929
        if (auto event_source = data_manager_extension->getEventSource(source_name.toStdString())) {
128✔
930
            result = event_source;
128✔
931
        }
128✔
932

933
    } else if (data_source_string.startsWith("Intervals: ")) {
317✔
934
        QString source_name = data_source_string.mid(11);// Remove "Intervals: " prefix
63✔
935
        row_selector_type = RowSelectorType::IntervalBased;
63✔
936

937
        if (auto interval_source = data_manager_extension->getIntervalSource(source_name.toStdString())) {
63✔
938
            result = interval_source;
63✔
939
        }
63✔
940
    } else if (data_source_string.startsWith("analog:")) {
189✔
941
        QString source_name = data_source_string.mid(7);// Remove "analog:" prefix
126✔
942
        row_selector_type = RowSelectorType::IntervalBased;
126✔
943

944
        if (auto analog_source = data_manager_extension->getAnalogSource(source_name.toStdString())) {
126✔
945
            result = analog_source;
126✔
946
        }
126✔
947
    }
126✔
948

949
    return {result, row_selector_type};
1,138✔
950
}
569✔
951

952
void TableDesignerWidget::updateRowInfoLabel(QString const & selected_source) {
35✔
953
    if (selected_source.isEmpty()) {
35✔
954
        ui->row_info_label->setText("No row source selected");
×
955
        return;
×
956
    }
957

958
    // Parse the selected source to get type and name
959
    QString source_type;
35✔
960
    QString source_name;
35✔
961

962
    if (selected_source.startsWith("TimeFrame: ")) {
35✔
963
        source_type = "TimeFrame";
17✔
964
        source_name = selected_source.mid(11);// Remove "TimeFrame: " prefix
17✔
965
    } else if (selected_source.startsWith("Events: ")) {
18✔
966
        source_type = "Events";
×
967
        source_name = selected_source.mid(8);// Remove "Events: " prefix
×
968
    } else if (selected_source.startsWith("Intervals: ")) {
18✔
969
        source_type = "Intervals";
18✔
970
        source_name = selected_source.mid(11);// Remove "Intervals: " prefix
18✔
971
    }
972

973
    // Get additional information about the selected source
974
    QString info_text = QString("Selected: %1 (%2)").arg(source_name, source_type);
35✔
975

976
    if (!_data_manager) {
35✔
977
        ui->row_info_label->setText(info_text);
×
978
        return;
×
979
    }
980

981
    auto * reg3 = _data_manager->getTableRegistry();
35✔
982
    auto data_manager_extension = reg3 ? reg3->getDataManagerExtension() : nullptr;
35✔
983
    if (!data_manager_extension) {
35✔
984
        ui->row_info_label->setText(info_text);
×
985
        return;
×
986
    }
987

988
    auto const source_name_str = source_name.toStdString();
35✔
989

990
    // Add specific information based on source type
991
    if (source_type == "TimeFrame") {
35✔
992
        auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
17✔
993
        if (timeframe) {
17✔
994
            info_text += QString(" - %1 time points").arg(timeframe->getTotalFrameCount());
17✔
995
        }
996
    } else if (source_type == "Events") {
35✔
997
        auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
998
        if (event_series) {
×
999
            auto events = event_series->getEventSeries();
×
1000
            info_text += QString(" - %1 events").arg(events.size());
×
1001
        }
×
1002
    } else if (source_type == "Intervals") {
18✔
1003
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
18✔
1004
        if (interval_series) {
18✔
1005
            auto intervals = interval_series->getDigitalIntervalSeries();
18✔
1006
            info_text += QString(" - %1 intervals").arg(intervals.size());
18✔
1007

1008
            // Add capture range and interval setting information
1009
            if (isIntervalItselfSelected()) {
18✔
1010
                info_text += QString("\nUsing intervals as-is (no capture range)");
×
1011
            } else {
1012
                int capture_range = getCaptureRange();
18✔
1013
                QString interval_point = isIntervalBeginningSelected() ? "beginning" : "end";
18✔
1014
                info_text += QString("\nCapture range: ±%1 samples around %2 of intervals").arg(capture_range).arg(interval_point);
18✔
1015
            }
18✔
1016
        }
18✔
1017
    }
18✔
1018

1019
    ui->row_info_label->setText(info_text);
35✔
1020
}
35✔
1021

1022
std::unique_ptr<IRowSelector> TableDesignerWidget::createRowSelector(QString const & row_source) {
21✔
1023
    // Parse the row source to get type and name
1024
    QString source_type;
21✔
1025
    QString source_name;
21✔
1026

1027
    if (row_source.startsWith("TimeFrame: ")) {
21✔
1028
        source_type = "TimeFrame";
×
1029
        source_name = row_source.mid(11);// Remove "TimeFrame: " prefix
×
1030
    } else if (row_source.startsWith("Events: ")) {
21✔
1031
        source_type = "Events";
×
1032
        source_name = row_source.mid(8);// Remove "Events: " prefix
×
1033
    } else if (row_source.startsWith("Intervals: ")) {
21✔
1034
        source_type = "Intervals";
21✔
1035
        source_name = row_source.mid(11);// Remove "Intervals: " prefix
21✔
1036
    } else {
1037
        qDebug() << "Unknown row source format:" << row_source;
×
1038
        return nullptr;
×
1039
    }
1040

1041
    auto const source_name_str = source_name.toStdString();
21✔
1042

1043
    try {
1044
        if (source_type == "TimeFrame") {
21✔
1045
            // Create IntervalSelector using TimeFrame
1046
            auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
×
1047
            if (!timeframe) {
×
1048
                qDebug() << "TimeFrame not found:" << source_name;
×
1049
                return nullptr;
×
1050
            }
1051

1052
            // Use timestamps to select all rows
1053
            std::vector<TimeFrameIndex> timestamps;
×
1054
            for (int64_t i = 0; i < timeframe->getTotalFrameCount(); ++i) {
×
1055
                timestamps.push_back(TimeFrameIndex(i));
×
1056
            }
1057
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe);
×
1058

1059
        } else if (source_type == "Events") {
21✔
1060
            // Create TimestampSelector using DigitalEventSeries
1061
            auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
1062
            if (!event_series) {
×
1063
                qDebug() << "DigitalEventSeries not found:" << source_name;
×
1064
                return nullptr;
×
1065
            }
1066

1067
            auto events = event_series->getEventSeries();
×
1068
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
×
1069
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
×
1070
            if (!timeframe_obj) {
×
1071
                qDebug() << "TimeFrame not found for events:" << timeframe_key.str();
×
1072
                return nullptr;
×
1073
            }
1074

1075
            // Convert events to TimeFrameIndex
1076
            std::vector<TimeFrameIndex> timestamps;
×
1077
            for (auto const & event: events) {
×
1078
                timestamps.push_back(TimeFrameIndex(static_cast<int64_t>(event)));
×
1079
            }
1080

1081
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe_obj);
×
1082

1083
        } else if (source_type == "Intervals") {
21✔
1084
            // Create IntervalSelector using DigitalIntervalSeries with capture range
1085
            auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
21✔
1086
            if (!interval_series) {
21✔
1087
                qDebug() << "DigitalIntervalSeries not found:" << source_name;
×
1088
                return nullptr;
×
1089
            }
1090

1091
            auto intervals = interval_series->getDigitalIntervalSeries();
21✔
1092
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
21✔
1093
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
21✔
1094
            if (!timeframe_obj) {
21✔
1095
                qDebug() << "TimeFrame not found for intervals:" << timeframe_key.str();
×
1096
                return nullptr;
×
1097
            }
1098

1099
            // Get capture range and interval setting
1100
            int capture_range = getCaptureRange();
21✔
1101
            bool use_beginning = isIntervalBeginningSelected();
21✔
1102
            bool use_interval_itself = isIntervalItselfSelected();
21✔
1103

1104
            // Create intervals based on the selected option
1105
            std::vector<TimeFrameInterval> tf_intervals;
21✔
1106
            for (auto const & interval: intervals) {
105✔
1107
                if (use_interval_itself) {
84✔
1108
                    // Use the interval as-is
1109
                    tf_intervals.emplace_back(TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
×
1110
                } else {
1111
                    // Determine the reference point (beginning or end of interval)
1112
                    int64_t reference_point;
1113
                    if (use_beginning) {
84✔
1114
                        reference_point = interval.start;
84✔
1115
                    } else {
1116
                        reference_point = interval.end;
×
1117
                    }
1118

1119
                    // Create a new interval around the reference point
1120
                    int64_t start_point = reference_point - capture_range;
84✔
1121
                    int64_t end_point = reference_point + capture_range;
84✔
1122

1123
                    // Ensure bounds are within the timeframe
1124
                    start_point = std::max(start_point, int64_t(0));
84✔
1125
                    end_point = std::min(end_point, static_cast<int64_t>(timeframe_obj->getTotalFrameCount() - 1));
84✔
1126

1127
                    tf_intervals.emplace_back(TimeFrameIndex(start_point), TimeFrameIndex(end_point));
84✔
1128
                }
1129
            }
1130

1131
            return std::make_unique<IntervalSelector>(std::move(tf_intervals), timeframe_obj);
21✔
1132
        }
21✔
1133

1134
    } catch (std::exception const & e) {
×
1135
        qDebug() << "Exception creating row selector:" << e.what();
×
1136
        return nullptr;
×
1137
    }
×
1138

1139
    qDebug() << "Unsupported row source type:" << source_type;
×
1140
    return nullptr;
×
1141
}
21✔
1142

1143
bool TableDesignerWidget::addColumnToBuilder(TableViewBuilder & builder, ColumnInfo const & column_info) {
×
1144
    // Use the simplified TableRegistry method that handles all the type checking internally
1145
    auto * reg = _data_manager->getTableRegistry();
×
1146
    if (!reg) {
×
1147
        qDebug() << "TableRegistry not available";
×
1148
        return false;
×
1149
    }
1150

1151
    bool success = reg->addColumnToBuilder(builder, column_info);
×
1152
    if (!success) {
×
1153
        qDebug() << "Failed to add column to builder:" << QString::fromStdString(column_info.name);
×
1154
    }
1155

1156
    return success;
×
1157
}
1158

1159
void TableDesignerWidget::updateIntervalSettingsVisibility() {
35✔
1160
    if (!ui->interval_settings_group) {
35✔
1161
        return;
×
1162
    }
1163

1164
    QString selected_key = ui->row_data_source_combo->currentText();
35✔
1165
    if (selected_key.isEmpty()) {
35✔
1166
        ui->interval_settings_group->setVisible(false);
×
1167
        if (ui->capture_range_spinbox) {
×
1168
            ui->capture_range_spinbox->setEnabled(false);
×
1169
        }
1170
        return;
×
1171
    }
1172

1173
    if (!_data_manager) {
35✔
1174
        ui->interval_settings_group->setVisible(false);
×
1175
        if (ui->capture_range_spinbox) {
×
1176
            ui->capture_range_spinbox->setEnabled(false);
×
1177
        }
1178
        return;
×
1179
    }
1180

1181
    // Check if the selected source is an interval series
1182
    if (selected_key.startsWith("Intervals: ")) {
35✔
1183
        ui->interval_settings_group->setVisible(true);
18✔
1184

1185
        // Enable/disable capture range based on interval setting
1186
        if (ui->capture_range_spinbox) {
18✔
1187
            bool use_interval_itself = isIntervalItselfSelected();
18✔
1188
            ui->capture_range_spinbox->setEnabled(!use_interval_itself);
18✔
1189
        }
1190
    } else {
1191
        ui->interval_settings_group->setVisible(false);
17✔
1192
        if (ui->capture_range_spinbox) {
17✔
1193
            ui->capture_range_spinbox->setEnabled(false);
17✔
1194
        }
1195
    }
1196
}
35✔
1197

1198
int TableDesignerWidget::getCaptureRange() const {
39✔
1199
    if (ui->capture_range_spinbox) {
39✔
1200
        return ui->capture_range_spinbox->value();
39✔
1201
    }
1202
    return 30000;// Default value
×
1203
}
1204

1205
void TableDesignerWidget::setCaptureRange(int value) {
41✔
1206
    if (ui->capture_range_spinbox) {
41✔
1207
        ui->capture_range_spinbox->blockSignals(true);
41✔
1208
        ui->capture_range_spinbox->setValue(value);
41✔
1209
        ui->capture_range_spinbox->blockSignals(false);
41✔
1210
    }
1211
}
41✔
1212

1213
bool TableDesignerWidget::isIntervalBeginningSelected() const {
39✔
1214
    if (ui->interval_beginning_radio) {
39✔
1215
        return ui->interval_beginning_radio->isChecked();
39✔
1216
    }
1217
    return true;// Default to beginning
×
1218
}
1219

1220
bool TableDesignerWidget::isIntervalItselfSelected() const {
57✔
1221
    if (ui->interval_itself_radio) {
57✔
1222
        return ui->interval_itself_radio->isChecked();
57✔
1223
    }
1224
    return false;// Default to not selected
×
1225
}
1226

1227
void TableDesignerWidget::triggerPreviewDebounced() {
131✔
1228
    if (_preview_debounce_timer) _preview_debounce_timer->start();
131✔
1229
    // Also trigger an immediate rebuild to support non-interactive/test contexts
1230
    rebuildPreviewNow();
131✔
1231
}
131✔
1232

1233
void TableDesignerWidget::rebuildPreviewNow() {
131✔
1234
    if (!_data_manager || !_table_viewer) return;
131✔
1235
    if (_current_table_id.isEmpty()) {
131✔
1236
        _table_viewer->clearTable();
58✔
1237
        return;
58✔
1238
    }
1239

1240
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
73✔
1241
    if (row_source.isEmpty()) {
73✔
1242
        _table_viewer->clearTable();
23✔
1243
        return;
23✔
1244
    }
1245

1246
    // Get enabled column infos from the computers tree
1247
    auto column_infos = getEnabledColumnInfos();
50✔
1248
    if (column_infos.empty()) {
50✔
1249
        _table_viewer->clearTable();
32✔
1250
        return;
32✔
1251
    }
1252

1253
    // Create row selector for the entire dataset
1254
    auto selector = createRowSelector(row_source);
18✔
1255
    if (!selector) {
18✔
1256
        _table_viewer->clearTable();
×
1257
        return;
×
1258
    }
1259

1260
    // Apply any saved column order for this table id
1261
    auto desiredOrder = _table_column_order.value(_current_table_id);
18✔
1262
    if (!desiredOrder.isEmpty()) {
18✔
1263
        std::vector<ColumnInfo> reordered;
12✔
1264
        reordered.reserve(column_infos.size());
12✔
1265
        for (auto const & name : desiredOrder) {
29✔
1266
            auto it = std::find_if(column_infos.begin(), column_infos.end(), [&](ColumnInfo const & ci){ return QString::fromStdString(ci.name) == name; });
41✔
1267
            if (it != column_infos.end()) {
17✔
1268
                reordered.push_back(*it);
17✔
1269
            }
1270
        }
1271
        for (auto const & ci : column_infos) {
33✔
1272
            if (std::find_if(reordered.begin(), reordered.end(), [&](ColumnInfo const & x){ return x.name == ci.name; }) == reordered.end()) {
50✔
1273
                reordered.push_back(ci);
4✔
1274
            }
1275
        }
1276
        column_infos = std::move(reordered);
12✔
1277
    }
12✔
1278

1279
    // Set up the table viewer with pagination
1280
    _table_viewer->setTableConfiguration(
90✔
1281
            std::move(selector),
18✔
1282
            std::move(column_infos),
18✔
1283
            _data_manager,
18✔
1284
            QString("Preview: %1").arg(_current_table_id));
36✔
1285

1286
    // Capture the current visual order from the viewer
1287
    QStringList currentOrder;
18✔
1288
    if (_table_viewer) {
18✔
1289
        auto * tv = _table_viewer->findChild<QTableView*>();
18✔
1290
        if (tv && tv->model()) {
18✔
1291
            auto * header = tv->horizontalHeader();
18✔
1292
            int cols = tv->model()->columnCount();
18✔
1293
            for (int v = 0; header && v < cols; ++v) {
45✔
1294
                int logical = header->logicalIndex(v);
27✔
1295
                auto name = tv->model()->headerData(logical, Qt::Horizontal, Qt::DisplayRole).toString();
27✔
1296
                currentOrder.push_back(name);
27✔
1297
            }
27✔
1298
        }
1299
    }
1300
    if (!currentOrder.isEmpty()) {
18✔
1301
        _table_column_order[_current_table_id] = currentOrder;
18✔
1302
    }
1303
}
105✔
1304

1305
void TableDesignerWidget::refreshComputersTree() {
63✔
1306
    if (!_data_manager) return;
63✔
1307

1308
    _updating_computers_tree = true;
63✔
1309

1310
    // Preserve previous checkbox states and custom column names
1311
    std::map<std::string, std::pair<Qt::CheckState, QString>> previous_states;
63✔
1312
    if (ui->computers_tree && ui->computers_tree->topLevelItemCount() > 0) {
63✔
1313
        for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
372✔
1314
            auto * data_source_item_old = ui->computers_tree->topLevelItem(i);
335✔
1315
            for (int j = 0; j < data_source_item_old->childCount(); ++j) {
1,414✔
1316
                auto * computer_item_old = data_source_item_old->child(j);
1,079✔
1317
                QString ds = computer_item_old->data(0, Qt::UserRole).toString();
1,079✔
1318
                QString cn = computer_item_old->data(1, Qt::UserRole).toString();
1,079✔
1319
                std::string key = (ds + "||" + cn).toStdString();
1,079✔
1320
                previous_states[key] = {computer_item_old->checkState(1), computer_item_old->text(2)};
1,079✔
1321
            }
1,079✔
1322
        }
1323
    }
1324

1325
    ui->computers_tree->clear();
63✔
1326
    ui->computers_tree->setHeaderLabels({"Data Source / Computer", "Enabled", "Column Name"});
315✔
1327

1328
    auto * registry = _data_manager->getTableRegistry();
63✔
1329
    if (!registry) {
63✔
1330
        _updating_computers_tree = false;
×
1331
        return;
×
1332
    }
1333

1334
    auto data_manager_extension = registry->getDataManagerExtension();
63✔
1335
    if (!data_manager_extension) {
63✔
1336
        _updating_computers_tree = false;
×
1337
        return;
×
1338
    }
1339

1340
    auto & computer_registry = registry->getComputerRegistry();
63✔
1341

1342
    // Get available data sources
1343
    auto data_sources = getAvailableDataSources();
63✔
1344

1345
    // Create tree structure: Data Source -> Computers
1346
    for (QString const & data_source: data_sources) {
632✔
1347
        auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
569✔
1348
        data_source_item->setText(0, data_source);
569✔
1349
        data_source_item->setFlags(Qt::ItemIsEnabled);
569✔
1350
        data_source_item->setExpanded(false);// Start collapsed
569✔
1351

1352
        // Convert data source string to DataSourceVariant and determine RowSelectorType
1353
        auto [data_source_variant, row_selector_type] = createDataSourceVariant(data_source, data_manager_extension);
569✔
1354

1355
        if (!data_source_variant.has_value()) {
569✔
1356
            qDebug() << "Failed to create data source variant for:" << data_source;
252✔
1357
            continue;
252✔
1358
        }
1359

1360
        // Get available computers for this specific data source and row selector combination
1361
        auto available_computers = computer_registry.getAvailableComputers(row_selector_type, data_source_variant.value());
317✔
1362

1363
        // Add compatible computers as children
1364
        for (auto const & computer_info: available_computers) {
2,150✔
1365
            auto * computer_item = new QTreeWidgetItem(data_source_item);
1,833✔
1366
            computer_item->setText(0, QString::fromStdString(computer_info.name));
1,833✔
1367
            computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
1,833✔
1368
            computer_item->setCheckState(1, Qt::Unchecked);
1,833✔
1369

1370
            // Generate default column name
1371
            QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
1,833✔
1372
            computer_item->setText(2, default_name);
1,833✔
1373
            computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
1,833✔
1374

1375
            // Store data source and computer name for later use
1376
            computer_item->setData(0, Qt::UserRole, data_source);
1,833✔
1377
            computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
1,833✔
1378

1379
            // Restore previous state if present
1380
            std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
1,833✔
1381
            auto it_prev = previous_states.find(prev_key);
1,833✔
1382
            if (it_prev != previous_states.end()) {
1,833✔
1383
                computer_item->setCheckState(1, it_prev->second.first);
1,076✔
1384
                if (!it_prev->second.second.isEmpty()) {
1,076✔
1385
                    computer_item->setText(2, it_prev->second.second);
1,076✔
1386
                }
1387
            }
1388
        }
1,833✔
1389
    }
569✔
1390

1391
    // Resize columns to content
1392
    ui->computers_tree->resizeColumnToContents(0);
63✔
1393
    ui->computers_tree->resizeColumnToContents(1);
63✔
1394
    ui->computers_tree->resizeColumnToContents(2);
63✔
1395

1396
    _updating_computers_tree = false;
63✔
1397

1398
    // Update preview after refresh
1399
    triggerPreviewDebounced();
63✔
1400
}
126✔
1401

1402
void TableDesignerWidget::setJsonTemplateFromCurrentState() {
3✔
1403
    if (!_table_json_widget) return;
3✔
1404
    // Build a minimal JSON template representing current UI state
1405
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
3✔
1406
    auto columns = getEnabledColumnInfos();
3✔
1407
    if (row_source.isEmpty() && columns.empty()) { _table_json_widget->setJsonText("{}"); return; }
3✔
1408

1409
    QString row_type;
3✔
1410
    QString row_source_name;
3✔
1411
    if (row_source.startsWith("TimeFrame: ")) { row_type = "timestamp"; row_source_name = row_source.mid(11); }
3✔
1412
    else if (row_source.startsWith("Events: ")) { row_type = "timestamp"; row_source_name = row_source.mid(8); }
3✔
1413
    else if (row_source.startsWith("Intervals: ")) { row_type = "interval"; row_source_name = row_source.mid(11); }
3✔
1414

1415
    QStringList column_entries;
3✔
1416
    for (auto const & c : columns) {
8✔
1417
        QString entry = QString(
15✔
1418
            "{\n  \"name\": \"%1\",\n  \"description\": \"%2\",\n  \"data_source\": \"%3\",\n  \"computer\": \"%4\"%5\n}"
1419
        ).arg(QString::fromStdString(c.name))
20✔
1420
         .arg(QString::fromStdString(c.description))
20✔
1421
         .arg(QString::fromStdString(c.dataSourceName))
20✔
1422
         .arg(QString::fromStdString(c.computerName))
20✔
1423
         .arg(c.parameters.empty() ? QString() : QString(",\n  \"parameters\": {}"));
10✔
1424
        column_entries << entry;
5✔
1425
    }
5✔
1426

1427
    QString table_name = _table_info_widget ? _table_info_widget->getName() : _current_table_id;
3✔
1428
    QString json = QString(
9✔
1429
        "{\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}"
1430
    ).arg(_current_table_id)
12✔
1431
     .arg(table_name)
12✔
1432
     .arg(row_type)
12✔
1433
     .arg(row_source_name)
12✔
1434
     .arg(column_entries.join(",\n"));
6✔
1435

1436
    _table_json_widget->setJsonText(json);
3✔
1437
}
3✔
1438

1439
void TableDesignerWidget::applyJsonTemplateToUI(QString const & jsonText) {
7✔
1440
    // Very light-weight parser using Qt to extract essential fields.
1441
    // Assumes a schema similar to tests under computers *.test.cpp.
1442
    QJsonParseError err;
7✔
1443
    QByteArray bytes = jsonText.toUtf8();
7✔
1444
    QJsonDocument doc = QJsonDocument::fromJson(bytes, &err);
7✔
1445
    if (err.error != QJsonParseError::NoError || !doc.isObject()) {
7✔
1446
        // Compute line/column from byte offset if possible
1447
        int64_t offset = static_cast<int64_t>(err.offset);
1✔
1448
        int line = 1;
1✔
1449
        int col = 1;
1✔
1450
        for (int64_t i = 0; i < offset && i < bytes.size(); ++i) {
149✔
1451
            if (bytes[i] == '\n') { ++line; col = 1; }
148✔
1452
            else { ++col; }
142✔
1453
        }
1454
        QString detail = err.error != QJsonParseError::NoError
1✔
1455
                ? QString("%1 (line %2, column %3)").arg(err.errorString()).arg(line).arg(col)
3✔
1456
                : QString("JSON root must be an object");
2✔
1457
        auto * box = new QMessageBox(this);
1✔
1458
        box->setIcon(QMessageBox::Critical);
1✔
1459
        box->setWindowTitle("Invalid JSON");
1✔
1460
        box->setText(QString("JSON format is invalid: %1").arg(detail));
1✔
1461
        box->setAttribute(Qt::WA_DeleteOnClose);
1✔
1462
        box->show();
1✔
1463
        return;
1✔
1464
    }
1✔
1465
    auto obj = doc.object();
6✔
1466
    if (!obj.contains("tables") || !obj["tables"].isArray()) {
6✔
1467
        auto * box = new QMessageBox(this);
×
1468
        box->setIcon(QMessageBox::Critical);
×
1469
        box->setWindowTitle("Invalid JSON");
×
1470
        box->setText("Missing required key: tables (array)");
×
1471
        box->setAttribute(Qt::WA_DeleteOnClose);
×
1472
        box->show();
×
1473
        return;
×
1474
    }
1475
    auto tables = obj["tables"].toArray();
6✔
1476
    if (tables.isEmpty() || !tables[0].isObject()) return;
6✔
1477
    auto table = tables[0].toObject();
6✔
1478

1479
    // Row selector
1480
    QStringList errors;
6✔
1481
    QString rs_type;
6✔
1482
    QString rs_source;
6✔
1483
    if (table.contains("row_selector") && table["row_selector"].isObject()) {
6✔
1484
        auto rs = table["row_selector"].toObject();
5✔
1485
        rs_type = rs.value("type").toString();
5✔
1486
        rs_source = rs.value("source").toString();
5✔
1487
        if (rs_type.isEmpty() || rs_source.isEmpty()) {
5✔
1488
            errors << "Missing required keys in row_selector: 'type' and/or 'source'";
×
1489
        } else {
1490
            // Validate existence
1491
            bool source_ok = false;
5✔
1492
            if (rs_type == "interval") {
5✔
1493
                source_ok = (_data_manager && _data_manager->getData<DigitalIntervalSeries>(rs_source.toStdString()) != nullptr);
5✔
1494
            } else if (rs_type == "timestamp") {
×
1495
                source_ok = (_data_manager && (
×
1496
                    _data_manager->getTime(TimeKey(rs_source.toStdString())) != nullptr ||
×
1497
                    _data_manager->getData<DigitalEventSeries>(rs_source.toStdString()) != nullptr
×
1498
                ));
×
1499
            } else {
1500
                errors << QString("Unsupported row_selector type: %1").arg(rs_type);
×
1501
            }
1502
            if (!source_ok) {
5✔
1503
                errors << QString("Row selector data key not found in DataManager: %1").arg(rs_source);
1✔
1504
            } else {
1505
                // Apply selection to UI
1506
                QString entry;
4✔
1507
                if (rs_type == "interval") {
4✔
1508
                    entry = QString("Intervals: %1").arg(rs_source);
4✔
1509
                } else if (rs_type == "timestamp") {
×
1510
                    // Prefer TimeFrame, fallback to Events
1511
                    entry = QString("TimeFrame: %1").arg(rs_source);
×
1512
                    int idx_tf = ui->row_data_source_combo->findText(entry);
×
1513
                    if (idx_tf < 0) entry = QString("Events: %1").arg(rs_source);
×
1514
                }
1515
                int idx = ui->row_data_source_combo->findText(entry);
4✔
1516
                if (idx >= 0) {
4✔
1517
                    ui->row_data_source_combo->setCurrentIndex(idx);
4✔
1518
                } else {
1519
                    errors << QString("Row selector entry not available in UI: %1").arg(entry);
×
1520
                }
1521
            }
4✔
1522
        }
1523
    } else {
5✔
1524
        errors << "Missing required key: row_selector (object)";
1✔
1525
    }
1526

1527
    // Columns: enable matching computers and set column names
1528
    if (table.contains("columns") && table["columns"].isArray()) {
6✔
1529
        auto cols = table["columns"].toArray();
6✔
1530
        auto * tree = ui->computers_tree;
6✔
1531
        for (auto const & cval : cols) {
12✔
1532
            if (!cval.isObject()) continue;
6✔
1533
            auto cobj = cval.toObject();
6✔
1534
            QString data_source = cobj.value("data_source").toString();
6✔
1535
            QString computer = cobj.value("computer").toString();
6✔
1536
            QString name = cobj.value("name").toString();
6✔
1537
            if (data_source.isEmpty() || computer.isEmpty() || name.isEmpty()) {
6✔
1538
                errors << "Missing required keys in column: 'name', 'data_source', and 'computer'";
×
1539
                continue;
×
1540
            }
1541
            // Validate data source existence
1542
            bool has_ds = (_data_manager && (
12✔
1543
                _data_manager->getData<DigitalEventSeries>(data_source.toStdString()) != nullptr ||
18✔
1544
                _data_manager->getData<DigitalIntervalSeries>(data_source.toStdString()) != nullptr ||
7✔
1545
                _data_manager->getData<AnalogTimeSeries>(data_source.toStdString()) != nullptr
7✔
1546
            ));
12✔
1547
            if (!has_ds) {
6✔
1548
                errors << QString("Data key not found in DataManager: %1").arg(data_source);
1✔
1549
            }
1550
            // Validate computer exists
1551
            bool computer_exists = false;
6✔
1552
            if (_data_manager) {
6✔
1553
                if (auto * reg = _data_manager->getTableRegistry()) {
6✔
1554
                    auto & cr = reg->getComputerRegistry();
6✔
1555
                    computer_exists = cr.findComputerInfo(computer.toStdString());
6✔
1556
                }
1557
            }
1558
            if (!computer_exists) {
6✔
1559
                errors << QString("Requested computer does not exist: %1").arg(computer);
2✔
1560
            }
1561
            // Validate compatibility (heuristic)
1562
            bool type_event = false, type_interval = false, type_analog = false;
6✔
1563
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
60✔
1564
                auto * ds_item = tree->topLevelItem(i);
54✔
1565
                QString ds_text = ds_item->text(0);
54✔
1566
                if (ds_text.contains(data_source)) {
54✔
1567
                    if (ds_text.startsWith("Events: ")) type_event = true;
5✔
1568
                    else if (ds_text.startsWith("Intervals: ")) type_interval = true;
×
1569
                    else if (ds_text.startsWith("analog:")) type_analog = true;
×
1570
                }
1571
            }
54✔
1572
            QString ds_repr;
6✔
1573
            if (type_event) ds_repr = QString("Events: %1").arg(data_source);
6✔
1574
            else if (type_interval) ds_repr = QString("Intervals: %1").arg(data_source);
1✔
1575
            else if (type_analog) ds_repr = QString("analog:%1").arg(data_source);
1✔
1576
            if (!ds_repr.isEmpty() && !isComputerCompatibleWithDataSource(computer.toStdString(), ds_repr)) {
6✔
1577
                errors << QString("Computer '%1' is not valid for data source type requested (%2)").arg(computer, ds_repr);
2✔
1578
            }
1579

1580
            // Find matching tree item
1581
            for (int i = 0; i < tree->topLevelItemCount(); ++i) {
60✔
1582
                auto * ds_item = tree->topLevelItem(i);
54✔
1583
                QString ds_text = ds_item->text(0);
54✔
1584
                if (!(ds_text.contains(data_source) || ds_text.endsWith(data_source))) continue;
54✔
1585
                for (int j = 0; j < ds_item->childCount(); ++j) {
20✔
1586
                    auto * comp_item = ds_item->child(j);
15✔
1587
                    if (comp_item->text(0) == computer) {
15✔
1588
                        comp_item->setCheckState(1, Qt::Checked);
3✔
1589
                        if (!name.isEmpty()) comp_item->setText(2, name);
3✔
1590
                    }
1591
                }
1592
            }
54✔
1593
        }
6✔
1594
        if (!errors.isEmpty()) {
6✔
1595
            auto * box = new QMessageBox(this);
4✔
1596
            box->setIcon(QMessageBox::Critical);
4✔
1597
            box->setWindowTitle("Invalid Table JSON");
4✔
1598
            box->setText(errors.join("\n"));
4✔
1599
            box->setAttribute(Qt::WA_DeleteOnClose);
4✔
1600
            box->show();
4✔
1601
            return;
4✔
1602
        }
1603
        triggerPreviewDebounced();
2✔
1604
    }
6✔
1605
}
36✔
1606

1607
void TableDesignerWidget::onComputersTreeItemChanged() {
13,996✔
1608
    if (_updating_computers_tree) return;
13,996✔
1609

1610
    // Trigger preview update when checkbox states change
1611
    triggerPreviewDebounced();
18✔
1612
}
1613

1614
void TableDesignerWidget::onComputersTreeItemEdited(QTreeWidgetItem * item, int column) {
13,996✔
1615
    if (_updating_computers_tree) return;
13,996✔
1616

1617
    // Only respond to column name edits (column 2)
1618
    if (column == 2) {
18✔
1619
        // Column name was edited, trigger preview update
1620
        triggerPreviewDebounced();
2✔
1621
    }
1622
}
1623

1624
std::vector<ColumnInfo> TableDesignerWidget::getEnabledColumnInfos() const {
61✔
1625
    std::vector<ColumnInfo> column_infos;
61✔
1626

1627
    if (!ui->computers_tree) return column_infos;
61✔
1628

1629
    // Iterate through all data source items
1630
    for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
610✔
1631
        auto * data_source_item = ui->computers_tree->topLevelItem(i);
549✔
1632

1633
        // Iterate through computer items under each data source
1634
        for (int j = 0; j < data_source_item->childCount(); ++j) {
2,318✔
1635
            auto * computer_item = data_source_item->child(j);
1,769✔
1636

1637
            // Check if this computer is enabled
1638
            if (computer_item->checkState(1) == Qt::Checked) {
1,769✔
1639
                QString data_source = computer_item->data(0, Qt::UserRole).toString();
45✔
1640
                QString computer_name = computer_item->data(1, Qt::UserRole).toString();
45✔
1641
                QString column_name = computer_item->text(2);
45✔
1642

1643
                if (column_name.isEmpty()) {
45✔
1644
                    column_name = generateDefaultColumnName(data_source, computer_name);
×
1645
                }
1646

1647
                // Create ColumnInfo (use raw key without UI prefixes)
1648
                QString source_key = data_source;
45✔
1649
                if (source_key.startsWith("Events: ")) {
45✔
1650
                    source_key = source_key.mid(8);
45✔
1651
                } else if (source_key.startsWith("Intervals: ")) {
×
1652
                    source_key = source_key.mid(11);
×
1653
                } else if (source_key.startsWith("analog:")) {
×
1654
                    source_key = source_key.mid(7);
×
1655
                } else if (source_key.startsWith("TimeFrame: ")) {
×
1656
                    source_key = source_key.mid(11);
×
1657
                }
1658

1659
                ColumnInfo info(column_name.toStdString(),
135✔
1660
                                QString("Column from %1 using %2").arg(data_source, computer_name).toStdString(),
90✔
1661
                                source_key.toStdString(),
90✔
1662
                                computer_name.toStdString());
225✔
1663

1664
                // Set output type based on computer info
1665
                if (auto * registry = _data_manager->getTableRegistry()) {
45✔
1666
                    auto & computer_registry = registry->getComputerRegistry();
45✔
1667
                    auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
45✔
1668
                    if (computer_info) {
45✔
1669
                        info.outputType = computer_info->outputType;
45✔
1670
                        info.outputTypeName = computer_info->outputTypeName;
45✔
1671
                        info.isVectorType = computer_info->isVectorType;
45✔
1672
                        if (info.isVectorType) {
45✔
1673
                            info.elementType = computer_info->elementType;
6✔
1674
                            info.elementTypeName = computer_info->elementTypeName;
6✔
1675
                        }
1676
                    }
1677
                }
1678

1679
                column_infos.push_back(std::move(info));
45✔
1680
            }
45✔
1681
        }
1682
    }
1683

1684
    return column_infos;
61✔
1685
}
×
1686

1687
bool TableDesignerWidget::isComputerCompatibleWithDataSource(std::string const & computer_name, QString const & data_source) const {
5✔
1688
    if (!_data_manager) return false;
5✔
1689

1690
    auto * registry = _data_manager->getTableRegistry();
5✔
1691
    if (!registry) return false;
5✔
1692

1693
    auto & computer_registry = registry->getComputerRegistry();
5✔
1694
    auto computer_info = computer_registry.findComputerInfo(computer_name);
5✔
1695
    if (!computer_info) return false;
5✔
1696

1697
    // Basic compatibility check based on data source type and common computer patterns
1698
    if (data_source.startsWith("Events: ")) {
3✔
1699
        // Event-based computers typically have "Event" in their name
1700
        return computer_name.find("Event") != std::string::npos;
3✔
1701
    } else if (data_source.startsWith("Intervals: ")) {
×
1702
        // Interval-based computers typically work with intervals or events
1703
        return computer_name.find("Event") != std::string::npos ||
×
1704
               computer_name.find("Interval") != std::string::npos;
×
1705
    } else if (data_source.startsWith("analog:")) {
×
1706
        // Analog-based computers typically have "Analog" in their name
1707
        return computer_name.find("Analog") != std::string::npos;
×
1708
    } else if (data_source.startsWith("TimeFrame: ")) {
×
1709
        // TimeFrame-based computers - generally most computers can work with timestamps
1710
        return computer_name.find("Timestamp") != std::string::npos ||
×
1711
               computer_name.find("Time") != std::string::npos;
×
1712
    }
1713

1714
    // Default: assume compatibility for unrecognized patterns
1715
    return true;
×
1716
}
1717

1718
QString TableDesignerWidget::generateDefaultColumnName(QString const & data_source, QString const & computer_name) const {
1,833✔
1719
    QString source_name = data_source;
1,833✔
1720

1721
    // Extract the actual name from prefixed data sources
1722
    if (source_name.startsWith("Events: ")) {
1,833✔
1723
        source_name = source_name.mid(8);
384✔
1724
    } else if (source_name.startsWith("Intervals: ")) {
1,449✔
1725
        source_name = source_name.mid(11);
441✔
1726
    } else if (source_name.startsWith("analog:")) {
1,008✔
1727
        source_name = source_name.mid(7);
1,008✔
1728
    } else if (source_name.startsWith("TimeFrame: ")) {
×
1729
        source_name = source_name.mid(11);
×
1730
    }
1731

1732
    // Create a concise name
1733
    return QString("%1_%2").arg(source_name, computer_name);
5,499✔
1734
}
1,833✔
1735

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