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

paulmthompson / WhiskerToolbox / 17564956715

08 Sep 2025 09:35PM UTC coverage: 71.483% (+0.02%) from 71.465%
17564956715

push

github

paulmthompson
table export in collapsable section. Also change order of sections

15 of 27 new or added lines in 2 files covered. (55.56%)

192 existing lines in 1 file now uncovered.

36468 of 51016 relevant lines covered (71.48%)

1279.92 hits per line

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

60.18
/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

26

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

42
#include <QFutureWatcher>
43
#include <QTimer>
44
#include <QtConcurrent>
45

46
#include <algorithm>
47
#include <fstream>
48
#include <iomanip>
49
#include <limits>
50
#include <tuple>
51
#include <typeindex>
52
#include <vector>
53

54
TableDesignerWidget::TableDesignerWidget(std::shared_ptr<DataManager> data_manager, QWidget * parent)
9✔
55
    : QWidget(parent),
56
      ui(new Ui::TableDesignerWidget),
18✔
57
      _data_manager(std::move(data_manager)) {
9✔
58

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

75
    _table_info_widget = new TableInfoWidget(this);
9✔
76
    _table_info_section = new Section(this, "Table Information");
9✔
77
    _table_info_section->setContentLayout(*new QVBoxLayout());
9✔
78
    _table_info_section->layout()->addWidget(_table_info_widget);
9✔
79
    _table_info_section->autoSetContentLayout();
9✔
80
    ui->main_layout->insertWidget(1, _table_info_section);
9✔
81

82
    // Hook save from table info widget
83
    connect(_table_info_widget, &TableInfoWidget::saveClicked, this, &TableDesignerWidget::onSaveTableInfo);
9✔
84

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

92
    connectSignals();
9✔
93
    // Initialize UI to a clean state, then populate controls
94
    clearUI();
9✔
95
    refreshTableCombo();
9✔
96
    refreshRowDataSourceCombo();
9✔
97
    refreshComputersTree();
9✔
98

99
    // Insert Transform section
100
    _table_transform_widget = new TableTransformWidget(this);
9✔
101
    _table_transform_section = new Section(this, "Transforms");
9✔
102
    _table_transform_section->setContentLayout(*new QVBoxLayout());
9✔
103
    _table_transform_section->layout()->addWidget(_table_transform_widget);
9✔
104
    _table_transform_section->autoSetContentLayout();
9✔
105
    // Place after build_group (after preview)
106
    ui->main_layout->insertWidget( ui->main_layout->indexOf(ui->build_group) + 1, _table_transform_section );
9✔
107
    connect(_table_transform_widget, &TableTransformWidget::applyTransformClicked,
27✔
108
            this, &TableDesignerWidget::onApplyTransform);
18✔
109

110
    // Insert Export section
111
    _table_export_widget = new TableExportWidget(this);
9✔
112
    _table_export_section = new Section(this, "Export");
9✔
113
    _table_export_section->setContentLayout(*new QVBoxLayout());
9✔
114
    _table_export_section->layout()->addWidget(_table_export_widget);
9✔
115
    _table_export_section->autoSetContentLayout();
9✔
116
    ui->main_layout->insertWidget( ui->main_layout->indexOf(ui->build_group) + 2, _table_export_section );
9✔
117
    connect(_table_export_widget, &TableExportWidget::exportClicked,
27✔
118
            this, &TableDesignerWidget::onExportCsv);
18✔
119

120
    // Add observer to automatically refresh dropdowns when DataManager changes
121
    if (_data_manager) {
9✔
122
        _data_manager->addObserver([this]() {
9✔
123
            refreshAllDataSources();
2✔
124
        });
2✔
125
    }
126

127
    qDebug() << "TableDesignerWidget initialized with TableViewerWidget for efficient pagination";
9✔
128
}
9✔
129

130
TableDesignerWidget::~TableDesignerWidget() {
9✔
131
    delete ui;
9✔
132
}
9✔
133

134
void TableDesignerWidget::refreshAllDataSources() {
2✔
135
    qDebug() << "Manually refreshing all data sources...";
2✔
136
    refreshRowDataSourceCombo();
2✔
137
    refreshComputersTree();
2✔
138

139
    // If we have a selected table, refresh its info
140
    if (!_current_table_id.isEmpty()) {
2✔
UNCOV
141
        loadTableInfo(_current_table_id);
×
142
    }
143
}
2✔
144

145

146
void TableDesignerWidget::connectSignals() {
9✔
147
    // Table selection signals
148
    connect(ui->table_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
27✔
149
            this, &TableDesignerWidget::onTableSelectionChanged);
18✔
150
    connect(ui->new_table_btn, &QPushButton::clicked,
27✔
151
            this, &TableDesignerWidget::onCreateNewTable);
18✔
152
    connect(ui->delete_table_btn, &QPushButton::clicked,
27✔
153
            this, &TableDesignerWidget::onDeleteTable);
18✔
154

155
    // Table info signals are connected via TableInfoWidget
156

157
    // Row source signals
158
    connect(ui->row_data_source_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
27✔
159
            this, &TableDesignerWidget::onRowDataSourceChanged);
18✔
160
    connect(ui->capture_range_spinbox, QOverload<int>::of(&QSpinBox::valueChanged),
27✔
161
            this, &TableDesignerWidget::onCaptureRangeChanged);
18✔
162
    connect(ui->interval_beginning_radio, &QRadioButton::toggled,
27✔
163
            this, &TableDesignerWidget::onIntervalSettingChanged);
18✔
164
    connect(ui->interval_end_radio, &QRadioButton::toggled,
27✔
165
            this, &TableDesignerWidget::onIntervalSettingChanged);
18✔
166
    connect(ui->interval_itself_radio, &QRadioButton::toggled,
27✔
167
            this, &TableDesignerWidget::onIntervalSettingChanged);
18✔
168

169
    // Column design signals (tree-based)
170
    connect(ui->computers_tree, &QTreeWidget::itemChanged,
27✔
171
            this, &TableDesignerWidget::onComputersTreeItemChanged);
18✔
172
    connect(ui->computers_tree, &QTreeWidget::itemChanged,
27✔
173
            this, &TableDesignerWidget::onComputersTreeItemEdited);
18✔
174

175
    // Build signals
176
    connect(ui->build_table_btn, &QPushButton::clicked,
27✔
177
            this, &TableDesignerWidget::onBuildTable);
18✔
178
    // Transform apply handled via TableTransformWidget
179
    // Export handled via TableExportWidget
180

181
    // Subscribe to DataManager table observer
182
    if (_data_manager) {
9✔
183
        auto token = _data_manager->addTableObserver([this](TableEvent const & ev) {
18✔
184
            switch (ev.type) {
8✔
185
                case TableEventType::Created:
3✔
186
                    this->onTableManagerTableCreated(QString::fromStdString(ev.tableId));
3✔
187
                    break;
3✔
UNCOV
188
                case TableEventType::Removed:
×
UNCOV
189
                    this->onTableManagerTableRemoved(QString::fromStdString(ev.tableId));
×
UNCOV
190
                    break;
×
191
                case TableEventType::InfoUpdated:
4✔
192
                    this->onTableManagerTableInfoUpdated(QString::fromStdString(ev.tableId));
4✔
193
                    break;
4✔
194
                case TableEventType::DataChanged:
1✔
195
                    // No direct UI change needed here for designer list
196
                    break;
1✔
197
            }
198
        });
17✔
199
        (void) token;// Optionally store and remove on dtor
200
    }
201
}
9✔
202

203
void TableDesignerWidget::onTableSelectionChanged() {
15✔
204
    int current_index = ui->table_combo->currentIndex();
15✔
205
    if (current_index < 0) {
15✔
206
        clearUI();
3✔
207
        return;
3✔
208
    }
209

210
    QString table_id = ui->table_combo->itemData(current_index).toString();
12✔
211
    if (table_id.isEmpty()) {
12✔
212
        clearUI();
9✔
213
        return;
9✔
214
    }
215

216
    _current_table_id = table_id;
3✔
217
    loadTableInfo(table_id);
3✔
218

219
    // Enable/disable controls
220
    ui->delete_table_btn->setEnabled(true);
3✔
221
    // Table info section is controlled separately
222
    ui->build_table_btn->setEnabled(true);
3✔
223
    if (auto gb = this->findChild<QGroupBox*>("row_source_group")) gb->setEnabled(true);
6✔
224
    if (auto gb = this->findChild<QGroupBox*>("column_design_group")) gb->setEnabled(true);
6✔
225
    // Enable save info within TableInfoWidget
226
    if (_table_info_section) _table_info_section->setEnabled(true);
3✔
227

228
    updateBuildStatus("Table selected: " + table_id);
3✔
229

230
    qDebug() << "Selected table:" << table_id;
3✔
231
}
12✔
232

233
void TableDesignerWidget::onCreateNewTable() {
×
234
    bool ok;
×
UNCOV
235
    QString name = QInputDialog::getText(this, "New Table", "Enter table name:", QLineEdit::Normal, "New Table", &ok);
×
236

UNCOV
237
    if (!ok || name.isEmpty()) {
×
UNCOV
238
        return;
×
239
    }
240

241
    auto * registry = _data_manager->getTableRegistry();
×
242
    if (!registry) { return; }
×
UNCOV
243
    auto table_id = registry->generateUniqueTableId("Table");
×
244

UNCOV
245
    if (registry->createTable(table_id, name.toStdString())) {
×
246
        // The combo will be refreshed by the signal handler
247
        // Set the new table as selected
248
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
UNCOV
249
            if (ui->table_combo->itemData(i).toString().toStdString() == table_id) {
×
250
                ui->table_combo->setCurrentIndex(i);
×
251
                break;
×
252
            }
253
        }
254
    } else {
255
        QMessageBox::warning(this, "Error", "Failed to create table with ID: " + QString::fromStdString(table_id));
×
256
    }
257
}
×
258

259
void TableDesignerWidget::onDeleteTable() {
×
260
    if (_current_table_id.isEmpty()) {
×
261
        return;
×
262
    }
263

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

UNCOV
268
    if (reply == QMessageBox::Yes) {
×
UNCOV
269
        auto * registry = _data_manager->getTableRegistry();
×
UNCOV
270
        if (registry && registry->removeTable(_current_table_id.toStdString())) {
×
271
            // The combo will be refreshed by the signal handler
UNCOV
272
            clearUI();
×
273
        } else {
UNCOV
274
            QMessageBox::warning(this, "Error", "Failed to delete table: " + _current_table_id);
×
275
        }
276
    }
277
}
278

279
void TableDesignerWidget::onRowDataSourceChanged() {
19✔
280
    QString selected = ui->row_data_source_combo->currentText();
19✔
281
    if (selected.isEmpty()) {
19✔
282
        ui->row_info_label->setText("No row source selected");
5✔
283
        return;
5✔
284
    }
285

286
    // Save the row source selection to the current table
287
    // Only save if we have a current table and we're not loading table info
288
    if (!_current_table_id.isEmpty() && _data_manager) {
14✔
289
        if (auto * reg = _data_manager->getTableRegistry()) {
3✔
290
            reg->updateTableRowSource(_current_table_id.toStdString(), selected.toStdString());
3✔
291
        }
292
    }
293

294
    // Update the info label
295
    updateRowInfoLabel(selected);
14✔
296

297
    // Update interval settings visibility
298
    updateIntervalSettingsVisibility();
14✔
299

300
    // Refresh computers tree since available computers depend on row selector type
301
    refreshComputersTree();
14✔
302

303
    qDebug() << "Row data source changed to:" << selected;
14✔
304
    triggerPreviewDebounced();
14✔
305
}
19✔
306

307
void TableDesignerWidget::onCaptureRangeChanged() {
×
308
    // Update the info label to reflect the new capture range
309
    QString selected = ui->row_data_source_combo->currentText();
×
310
    if (!selected.isEmpty()) {
×
311
        updateRowInfoLabel(selected);
×
312
    }
UNCOV
313
    triggerPreviewDebounced();
×
UNCOV
314
}
×
315

316
void TableDesignerWidget::onIntervalSettingChanged() {
×
317
    // Update the info label to reflect the new interval setting
UNCOV
318
    QString selected = ui->row_data_source_combo->currentText();
×
UNCOV
319
    if (!selected.isEmpty()) {
×
320
        updateRowInfoLabel(selected);
×
321
    }
322

323
    // Update capture range visibility based on interval setting
UNCOV
324
    updateIntervalSettingsVisibility();
×
UNCOV
325
    triggerPreviewDebounced();
×
326
}
×
327

328

329
void TableDesignerWidget::onBuildTable() {
×
UNCOV
330
    if (_current_table_id.isEmpty()) {
×
UNCOV
331
        updateBuildStatus("No table selected", true);
×
UNCOV
332
        return;
×
333
    }
334

335
    QString row_source = ui->row_data_source_combo->currentText();
×
336
    if (row_source.isEmpty()) {
×
UNCOV
337
        updateBuildStatus("No row data source selected", true);
×
UNCOV
338
        return;
×
339
    }
340

341
    // Get enabled column infos from the tree
342
    auto column_infos = getEnabledColumnInfos();
×
343
    if (column_infos.empty()) {
×
344
        updateBuildStatus("No computers enabled. Check boxes in the tree to enable computers.", true);
×
UNCOV
345
        return;
×
346
    }
347

348
    try {
349
        // Create the row selector
350
        auto row_selector = createRowSelector(row_source);
×
351
        if (!row_selector) {
×
352
            updateBuildStatus("Failed to create row selector", true);
×
UNCOV
353
            return;
×
354
        }
355

356
        // Get the data manager extension
357
        auto * reg = _data_manager->getTableRegistry();
×
UNCOV
358
        auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
×
UNCOV
359
        if (!data_manager_extension) {
×
360
            updateBuildStatus("DataManager extension not available", true);
×
361
            return;
×
362
        }
363

364
        // Create the TableViewBuilder
365
        TableViewBuilder builder(data_manager_extension);
×
UNCOV
366
        builder.setRowSelector(std::move(row_selector));
×
367

368
        // Add all enabled columns from the tree
369
        bool all_columns_valid = true;
×
370
        for (auto const & column_info: column_infos) {
×
UNCOV
371
            if (!reg->addColumnToBuilder(builder, column_info)) {
×
UNCOV
372
                updateBuildStatus(QString("Failed to create column: %1").arg(QString::fromStdString(column_info.name)), true);
×
UNCOV
373
                all_columns_valid = false;
×
374
                break;
×
375
            }
376
        }
377

UNCOV
378
        if (!all_columns_valid) {
×
379
            return;
×
380
        }
381

382
        // Build the table
UNCOV
383
        auto table_view = builder.build();
×
384

385
        // Store the built table in the TableManager and update table info with current columns
386
        if (reg) {
×
387
            // Update table info with current column configuration
UNCOV
388
            auto table_info = reg->getTableInfo(_current_table_id.toStdString());
×
389
            table_info.columns = column_infos;// Store the current enabled columns
×
UNCOV
390
            reg->updateTableInfo(_current_table_id.toStdString(),
×
391
                                 table_info.name, table_info.description);
392

393
            // Store the built table
UNCOV
394
            if (reg->storeBuiltTable(_current_table_id.toStdString(), std::move(table_view))) {
×
395
                updateBuildStatus(QString("Table built successfully with %1 columns!").arg(column_infos.size()));
×
396
                qDebug() << "Successfully built table:" << _current_table_id << "with" << column_infos.size() << "columns";
×
397
            } else {
398
                updateBuildStatus("Failed to store built table", true);
×
399
            }
UNCOV
400
        } else {
×
UNCOV
401
            updateBuildStatus("Registry unavailable", true);
×
402
        }
403

404
    } catch (std::exception const & e) {
×
405
        updateBuildStatus(QString("Error building table: %1").arg(e.what()), true);
×
UNCOV
406
        qDebug() << "Exception during table building:" << e.what();
×
UNCOV
407
    }
×
UNCOV
408
}
×
409

410
bool TableDesignerWidget::buildTableFromTree() {
1✔
411
    // This is essentially the same as onBuildTable but returns success status
412
    if (_current_table_id.isEmpty()) {
1✔
UNCOV
413
        updateBuildStatus("No table selected", true);
×
UNCOV
414
        return false;
×
415
    }
416

417
    QString row_source = ui->row_data_source_combo->currentText();
1✔
418
    if (row_source.isEmpty()) {
1✔
UNCOV
419
        updateBuildStatus("No row data source selected", true);
×
UNCOV
420
        return false;
×
421
    }
422

423
    // Get enabled column infos from the tree
424
    auto column_infos = getEnabledColumnInfos();
1✔
425
    if (column_infos.empty()) {
1✔
426
        updateBuildStatus("No computers enabled. Check boxes in the tree to enable computers.", true);
×
UNCOV
427
        return false;
×
428
    }
429

430
    try {
431
        // Create the row selector
432
        auto row_selector = createRowSelector(row_source);
1✔
433
        if (!row_selector) {
1✔
434
            updateBuildStatus("Failed to create row selector", true);
×
UNCOV
435
            return false;
×
436
        }
437

438
        // Get the data manager extension
439
        auto * reg = _data_manager->getTableRegistry();
1✔
440
        auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
1✔
441
        if (!data_manager_extension) {
1✔
UNCOV
442
            updateBuildStatus("DataManager extension not available", true);
×
UNCOV
443
            return false;
×
444
        }
445

446
        // Create the TableViewBuilder
447
        TableViewBuilder builder(data_manager_extension);
1✔
448
        builder.setRowSelector(std::move(row_selector));
1✔
449

450
        // Add all enabled columns from the tree
451
        bool all_columns_valid = true;
1✔
452
        for (auto const & column_info: column_infos) {
4✔
453
            if (!reg->addColumnToBuilder(builder, column_info)) {
3✔
UNCOV
454
                updateBuildStatus(QString("Failed to create column: %1").arg(QString::fromStdString(column_info.name)), true);
×
UNCOV
455
                all_columns_valid = false;
×
UNCOV
456
                break;
×
457
            }
458
        }
459

460
        if (!all_columns_valid) {
1✔
UNCOV
461
            return false;
×
462
        }
463

464
        // Build the table
465
        auto table_view = builder.build();
1✔
466

467
        // Store the built table in the TableManager and update table info with current columns
468
        if (reg) {
1✔
469
            // Update table info with current column configuration
470
            auto table_info = reg->getTableInfo(_current_table_id.toStdString());
1✔
471
            table_info.columns = column_infos;// Store the current enabled columns
1✔
472
            reg->updateTableInfo(_current_table_id.toStdString(),
1✔
473
                                 table_info.name,
474
                                 table_info.description);
475

476
            // Store the built table
477
            if (reg->storeBuiltTable(_current_table_id.toStdString(), std::move(table_view))) {
1✔
478
                updateBuildStatus(QString("Table built successfully with %1 columns!").arg(column_infos.size()));
1✔
479
                qDebug() << "Successfully built table:" << _current_table_id << "with" << column_infos.size() << "columns";
1✔
480
                return true;
1✔
481
            } else {
482
                updateBuildStatus("Failed to store built table", true);
×
483
                return false;
×
484
            }
485
        } else {
1✔
UNCOV
486
            updateBuildStatus("Registry unavailable", true);
×
UNCOV
487
            return false;
×
488
        }
489

490
    } catch (std::exception const & e) {
1✔
491
        updateBuildStatus(QString("Error building table: %1").arg(e.what()), true);
×
UNCOV
492
        qDebug() << "Exception during table building:" << e.what();
×
UNCOV
493
        return false;
×
UNCOV
494
    }
×
495
}
1✔
496

497
void TableDesignerWidget::onApplyTransform() {
×
498
    if (_current_table_id.isEmpty() || !_data_manager) {
×
UNCOV
499
        updateBuildStatus("No base table selected", true);
×
500
        return;
×
501
    }
502

503
    // Fetch the built base table
UNCOV
504
    auto * reg = _data_manager->getTableRegistry();
×
UNCOV
505
    if (!reg) {
×
UNCOV
506
        updateBuildStatus("Registry unavailable", true);
×
507
        return;
×
508
    }
509
    auto base_view = reg->getBuiltTable(_current_table_id.toStdString());
×
510
    if (!base_view) {
×
UNCOV
511
        updateBuildStatus("Build the base table first", true);
×
UNCOV
512
        return;
×
513
    }
514

515
    // Currently only PCA option is exposed
516
    QString transform = _table_transform_widget ? _table_transform_widget->getTransformType() : QString();
×
517
    if (transform != "PCA") {
×
518
        updateBuildStatus("Unsupported transform", true);
×
UNCOV
519
        return;
×
520
    }
521

522
    // Configure PCA
UNCOV
523
    PCAConfig cfg;
×
UNCOV
524
    cfg.center = _table_transform_widget && _table_transform_widget->isCenterEnabled();
×
525
    cfg.standardize = _table_transform_widget && _table_transform_widget->isStandardizeEnabled();
×
526
    if (_table_transform_widget) {
×
UNCOV
527
        for (auto const & s: _table_transform_widget->getIncludeColumns()) cfg.include.push_back(s);
×
UNCOV
528
        for (auto const & s: _table_transform_widget->getExcludeColumns()) cfg.exclude.push_back(s);
×
529
    }
530

531
    try {
532
        PCATransform pca(cfg);
×
533
        TableView derived = pca.apply(*base_view);
×
534

535
        // Determine output id/name
536
        QString out_name = _table_transform_widget ? _table_transform_widget->getOutputName().trimmed() : QString();
×
537
        if (out_name.isEmpty()) {
×
538
            QString base = _table_info_widget ? _table_info_widget->getName() : QString();
×
UNCOV
539
            out_name = base.isEmpty() ? QString("(PCA)") : QString("%1 (PCA)").arg(base);
×
540
        }
×
541

542
        std::string out_id = reg->generateUniqueTableId((_current_table_id + "_pca").toStdString());
×
UNCOV
543
        if (!reg->createTable(out_id, out_name.toStdString())) {
×
544
            reg->updateTableInfo(out_id, out_name.toStdString(), "");
×
545
        }
546
        if (reg->storeBuiltTable(out_id, std::move(derived))) {
×
547
            updateBuildStatus(QString("Created transformed table: %1").arg(out_name));
×
548
            refreshTableCombo();
×
549
        } else {
UNCOV
550
            updateBuildStatus("Failed to store transformed table", true);
×
551
        }
552
    } catch (std::exception const & e) {
×
553
        updateBuildStatus(QString("Transform failed: %1").arg(e.what()), true);
×
554
    }
×
555
}
×
556

557
std::vector<std::string> TableDesignerWidget::parseCommaSeparatedList(QString const & text) const {
×
558
    std::vector<std::string> out;
×
UNCOV
559
    for (QString s: text.split(",", Qt::SkipEmptyParts)) {
×
560
        s = s.trimmed();
×
561
        if (!s.isEmpty()) out.push_back(s.toStdString());
×
562
    }
×
563
    return out;
×
UNCOV
564
}
×
565

566
void TableDesignerWidget::onExportCsv() {
×
567
    if (_current_table_id.isEmpty() || !_data_manager) {
×
568
        updateBuildStatus("No table selected", true);
×
569
        return;
×
570
    }
571

572
    auto * reg = _data_manager->getTableRegistry();
×
573
    if (!reg) {
×
574
        updateBuildStatus("Registry unavailable", true);
×
UNCOV
575
        return;
×
576
    }
577
    auto view = reg->getBuiltTable(_current_table_id.toStdString());
×
578
    if (!view) {
×
579
        updateBuildStatus("Build the table first", true);
×
UNCOV
580
        return;
×
581
    }
582

583
    QString filename = promptSaveCsvFilename();
×
584
    if (filename.isEmpty()) return;
×
585
    if (!filename.endsWith(".csv", Qt::CaseInsensitive)) filename += ".csv";
×
586

587
    // CSV options from export widget
NEW
588
    QString delimiter = _table_export_widget ? _table_export_widget->getDelimiterText() : "Comma";
×
NEW
589
    QString lineEnding = _table_export_widget ? _table_export_widget->getLineEndingText() : "LF (\\n)";
×
NEW
590
    int precision = _table_export_widget ? _table_export_widget->getPrecision() : 3;
×
NEW
591
    bool includeHeader = _table_export_widget && _table_export_widget->isHeaderIncluded();
×
592

UNCOV
593
    std::string delim = ",";
×
UNCOV
594
    if (delimiter == "Space") delim = " ";
×
595
    else if (delimiter == "Tab")
×
596
        delim = "\t";
×
597
    std::string eol = "\n";
×
598
    if (lineEnding.startsWith("CRLF")) eol = "\r\n";
×
599

600
    try {
UNCOV
601
        std::ofstream file(filename.toStdString());
×
602
        if (!file.is_open()) {
×
603
            updateBuildStatus("Could not open file for writing", true);
×
604
            return;
×
605
        }
606
        file << std::fixed << std::setprecision(precision);
×
607

608
        auto names = view->getColumnNames();
×
UNCOV
609
        if (includeHeader) {
×
610
            for (size_t i = 0; i < names.size(); ++i) {
×
611
                if (i > 0) file << delim;
×
612
                file << names[i];
×
613
            }
UNCOV
614
            file << eol;
×
615
        }
616
        size_t rows = view->getRowCount();
×
UNCOV
617
        for (size_t r = 0; r < rows; ++r) {
×
618
            for (size_t c = 0; c < names.size(); ++c) {
×
619
                if (c > 0) file << delim;
×
620
                try {
621
                    auto const & vals = view->getColumnValues<double>(names[c].c_str());
×
UNCOV
622
                    if (r < vals.size()) file << vals[r];
×
623
                    else
UNCOV
624
                        file << "NaN";
×
625
                } catch (...) {
×
626
                    file << "NaN";
×
627
                }
×
628
            }
629
            file << eol;
×
630
        }
UNCOV
631
        file.close();
×
632
        updateBuildStatus(QString("Exported CSV: %1").arg(filename));
×
633
    } catch (std::exception const & e) {
×
UNCOV
634
        updateBuildStatus(QString("Export failed: %1").arg(e.what()), true);
×
UNCOV
635
    }
×
636
}
×
637

638
QString TableDesignerWidget::promptSaveCsvFilename() const {
×
UNCOV
639
    return QFileDialog::getSaveFileName(const_cast<TableDesignerWidget *>(this), "Export Table to CSV", QString(), "CSV Files (*.csv)");
×
640
}
641

642
void TableDesignerWidget::onSaveTableInfo() {
×
UNCOV
643
    if (_current_table_id.isEmpty()) {
×
644
        return;
×
645
    }
646

UNCOV
647
    QString name = _table_info_widget ? _table_info_widget->getName() : QString();
×
UNCOV
648
    QString description = _table_info_widget ? _table_info_widget->getDescription() : QString();
×
649

650
    if (name.isEmpty()) {
×
UNCOV
651
        QMessageBox::warning(this, "Error", "Table name cannot be empty");
×
652
        return;
×
653
    }
654

655
    if (auto * reg = _data_manager->getTableRegistry(); reg && reg->updateTableInfo(_current_table_id.toStdString(), name.toStdString(), description.toStdString())) {
×
656
        updateBuildStatus("Table information saved");
×
657
        // Refresh the combo to show updated name
UNCOV
658
        refreshTableCombo();
×
659
        // Restore selection
UNCOV
660
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
661
            if (ui->table_combo->itemData(i).toString() == _current_table_id) {
×
UNCOV
662
                ui->table_combo->setCurrentIndex(i);
×
663
                break;
×
664
            }
665
        }
666
    } else {
UNCOV
667
        QMessageBox::warning(this, "Error", "Failed to save table information");
×
668
    }
UNCOV
669
}
×
670

671
void TableDesignerWidget::onTableManagerTableCreated(QString const & table_id) {
3✔
672
    refreshTableCombo();
3✔
673
    qDebug() << "Table created signal received:" << table_id;
3✔
674
}
3✔
675

676
void TableDesignerWidget::onTableManagerTableRemoved(QString const & table_id) {
×
677
    refreshTableCombo();
×
UNCOV
678
    if (_current_table_id == table_id) {
×
UNCOV
679
        _current_table_id.clear();
×
UNCOV
680
        clearUI();
×
681
    }
UNCOV
682
    qDebug() << "Table removed signal received:" << table_id;
×
UNCOV
683
}
×
684

685
void TableDesignerWidget::onTableManagerTableInfoUpdated(QString const & table_id) {
4✔
686
    if (_current_table_id == table_id && !_loading_column_configuration) {
4✔
687
        loadTableInfo(table_id);
4✔
688
    }
689
    qDebug() << "Table info updated signal received:" << table_id;
4✔
690
}
4✔
691

692
void TableDesignerWidget::refreshTableCombo() {
12✔
693
    ui->table_combo->clear();
12✔
694

695
    auto * reg = _data_manager->getTableRegistry();
12✔
696
    auto table_infos = reg ? reg->getAllTableInfo() : std::vector<TableInfo>{};
12✔
697
    for (auto const & info: table_infos) {
15✔
698
        ui->table_combo->addItem(QString::fromStdString(info.name), QString::fromStdString(info.id));
3✔
699
    }
700

701
    if (ui->table_combo->count() == 0) {
12✔
702
        ui->table_combo->addItem("(No tables available)", "");
9✔
703
    }
704
}
24✔
705

706
void TableDesignerWidget::refreshRowDataSourceCombo() {
11✔
707
    ui->row_data_source_combo->clear();
11✔
708

709
    if (!_data_manager) {
11✔
UNCOV
710
        qDebug() << "refreshRowDataSourceCombo: No table manager";
×
UNCOV
711
        return;
×
712
    }
713

714
    auto data_sources = getAvailableDataSources();
11✔
715
    qDebug() << "refreshRowDataSourceCombo: Found" << data_sources.size() << "data sources:" << data_sources;
11✔
716

717
    for (QString const & source: data_sources) {
111✔
718
        // Only include valid row sources in this combo
719
        if (source.startsWith("TimeFrame: ") || source.startsWith("Events: ") || source.startsWith("Intervals: ")) {
100✔
720
            ui->row_data_source_combo->addItem(source);
78✔
721
        }
722
    }
723

724
    if (ui->row_data_source_combo->count() == 0) {
11✔
UNCOV
725
        ui->row_data_source_combo->addItem("(No data sources available)");
×
UNCOV
726
        qDebug() << "refreshRowDataSourceCombo: No data sources available";
×
727
    }
728
}
11✔
729

730

731
void TableDesignerWidget::loadTableInfo(QString const & table_id) {
7✔
732
    if (table_id.isEmpty() || !_data_manager) {
7✔
UNCOV
733
        clearUI();
×
734
        return;
×
735
    }
736

737
    auto * reg = _data_manager->getTableRegistry();
7✔
738
    auto info = reg ? reg->getTableInfo(table_id.toStdString()) : TableInfo{};
7✔
739
    if (info.id.empty()) {
7✔
UNCOV
740
        clearUI();
×
UNCOV
741
        return;
×
742
    }
743

744
    // Load table information
745
    if (_table_info_widget) {
7✔
746
        _table_info_widget->setName(QString::fromStdString(info.name));
7✔
747
        _table_info_widget->setDescription(QString::fromStdString(info.description));
7✔
748
    }
749

750
    // Load row source if available
751
    if (!info.rowSourceName.empty()) {
7✔
752
        int row_index = ui->row_data_source_combo->findText(QString::fromStdString(info.rowSourceName));
4✔
753
        if (row_index >= 0) {
4✔
754
            // Block signals to prevent circular dependency when loading table info
755
            ui->row_data_source_combo->blockSignals(true);
4✔
756
            ui->row_data_source_combo->setCurrentIndex(row_index);
4✔
757
            ui->row_data_source_combo->blockSignals(false);
4✔
758

759
            // Manually update the info label without triggering the signal handler
760
            updateRowInfoLabel(QString::fromStdString(info.rowSourceName));
4✔
761

762
            // Update interval settings visibility
763
            updateIntervalSettingsVisibility();
4✔
764

765
            // Since signals were blocked, this will ensure the tree is refreshed
766
            // when the computers tree is populated later in this function
767
        }
768
    }
769

770
    // Clear old column list (deprecated)
771
    // The computers tree will be populated based on available data sources
772
    refreshComputersTree();
7✔
773

774
    updateBuildStatus(QString("Loaded table: %1").arg(QString::fromStdString(info.name)));
7✔
775
    triggerPreviewDebounced();
7✔
776
}
7✔
777

778
void TableDesignerWidget::clearUI() {
21✔
779
    _current_table_id.clear();
21✔
780

781
    // Clear table info
782
    if (_table_info_widget) {
21✔
783
        _table_info_widget->setName("");
21✔
784
        _table_info_widget->setDescription("");
21✔
785
    }
786

787
    // Clear row source
788
    ui->row_data_source_combo->setCurrentIndex(-1);
21✔
789
    ui->row_info_label->setText("No row source selected");
21✔
790

791
    // Reset capture range and interval settings
792
    setCaptureRange(30000);// Default value
21✔
793
    if (ui->interval_beginning_radio) {
21✔
794
        ui->interval_beginning_radio->setChecked(true);
21✔
795
    }
796
    if (ui->interval_itself_radio) {
21✔
797
        ui->interval_itself_radio->setChecked(false);
21✔
798
    }
799
    if (ui->interval_settings_group) {
21✔
800
        ui->interval_settings_group->setVisible(false);
21✔
801
    }
802

803
    // Clear computers tree
804
    if (ui->computers_tree) {
21✔
805
        ui->computers_tree->clear();
21✔
806
    }
807

808
    // Disable controls
809
    ui->delete_table_btn->setEnabled(false);
21✔
810
    // Table info section is controlled separately
811
    ui->build_table_btn->setEnabled(false);
21✔
812
    if (auto gb = this->findChild<QGroupBox*>("row_source_group")) gb->setEnabled(false);
42✔
813
    if (auto gb = this->findChild<QGroupBox*>("column_design_group")) gb->setEnabled(false);
42✔
814
    if (_table_info_section) _table_info_section->setEnabled(false);
21✔
815

816
    updateBuildStatus("No table selected");
21✔
817
    if (_table_viewer) _table_viewer->clearTable();
21✔
818
}
21✔
819

820
void TableDesignerWidget::updateBuildStatus(QString const & message, bool is_error) {
32✔
821
    ui->build_status_label->setText(message);
32✔
822

823
    if (is_error) {
32✔
UNCOV
824
        ui->build_status_label->setStyleSheet("QLabel { color: red; font-weight: bold; }");
×
825
    } else {
826
        ui->build_status_label->setStyleSheet("QLabel { color: green; }");
32✔
827
    }
828
}
32✔
829

830
QStringList TableDesignerWidget::getAvailableDataSources() const {
43✔
831
    QStringList sources;
43✔
832

833
    if (!_data_manager) {
43✔
UNCOV
834
        qDebug() << "getAvailableDataSources: No table manager";
×
835
        return sources;
×
836
    }
837

838
    auto * reg = _data_manager->getTableRegistry();
43✔
839
    auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
43✔
840
    if (!data_manager_extension) {
43✔
841
        qDebug() << "getAvailableDataSources: No data manager extension";
×
UNCOV
842
        return sources;
×
843
    }
844

845
    if (!_data_manager) {
43✔
UNCOV
846
        qDebug() << "getAvailableDataSources: No data manager";
×
UNCOV
847
        return sources;
×
848
    }
849

850
    // Add TimeFrame keys as potential row sources
851
    // TimeFrames can define intervals for analysis
852
    auto timeframe_keys = _data_manager->getTimeFrameKeys();
43✔
853
    qDebug() << "getAvailableDataSources: TimeFrame keys:" << timeframe_keys.size();
43✔
854
    for (auto const & key: timeframe_keys) {
215✔
855
        QString source = QString("TimeFrame: %1").arg(QString::fromStdString(key.str()));
172✔
856
        sources << source;
172✔
857
        qDebug() << "  Added TimeFrame:" << source;
172✔
858
    }
172✔
859

860
    // Add DigitalEventSeries keys as potential row sources
861
    // Events can be used to define analysis windows or timestamps
862
    auto event_keys = _data_manager->getKeys<DigitalEventSeries>();
43✔
863
    qDebug() << "getAvailableDataSources: Event keys:" << event_keys.size();
43✔
864
    for (auto const & key: event_keys) {
132✔
865
        QString source = QString("Events: %1").arg(QString::fromStdString(key));
89✔
866
        sources << source;
89✔
867
        qDebug() << "  Added Events:" << source;
89✔
868
    }
89✔
869

870
    // Add DigitalIntervalSeries keys as potential row sources
871
    // Intervals directly define analysis windows
872
    auto interval_keys = _data_manager->getKeys<DigitalIntervalSeries>();
43✔
873
    qDebug() << "getAvailableDataSources: Interval keys:" << interval_keys.size();
43✔
874
    for (auto const & key: interval_keys) {
86✔
875
        QString source = QString("Intervals: %1").arg(QString::fromStdString(key));
43✔
876
        sources << source;
43✔
877
        qDebug() << "  Added Intervals:" << source;
43✔
878
    }
43✔
879

880
    // Add AnalogTimeSeries keys as data sources (for computers; not row selectors)
881
    auto analog_keys = _data_manager->getKeys<AnalogTimeSeries>();
43✔
882
    qDebug() << "getAvailableDataSources: Analog keys:" << analog_keys.size();
43✔
883
    for (auto const & key: analog_keys) {
129✔
884
        QString source = QString("analog:%1").arg(QString::fromStdString(key));
86✔
885
        sources << source;
86✔
886
        qDebug() << "  Added Analog:" << source;
86✔
887
    }
86✔
888

889
    qDebug() << "getAvailableDataSources: Total sources found:" << sources.size();
43✔
890

891
    return sources;
43✔
892
}
43✔
893

894
std::pair<std::optional<DataSourceVariant>, RowSelectorType>
895
TableDesignerWidget::createDataSourceVariant(QString const & data_source_string,
290✔
896
                                             std::shared_ptr<DataManagerExtension> data_manager_extension) const {
897
    std::optional<DataSourceVariant> result;
290✔
898
    RowSelectorType row_selector_type = RowSelectorType::IntervalBased;
290✔
899

900
    if (data_source_string.startsWith("TimeFrame: ")) {
290✔
901
        // TimeFrames are used with TimestampSelector for rows; no concrete data source needed
902
        row_selector_type = RowSelectorType::Timestamp;
128✔
903

904
    } else if (data_source_string.startsWith("Events: ")) {
162✔
905
        QString source_name = data_source_string.mid(8);// Remove "Events: " prefix
66✔
906
        // Event-based computers in the registry operate with interval rows
907
        row_selector_type = RowSelectorType::IntervalBased;
66✔
908

909
        if (auto event_source = data_manager_extension->getEventSource(source_name.toStdString())) {
66✔
910
            result = event_source;
66✔
911
        }
66✔
912

913
    } else if (data_source_string.startsWith("Intervals: ")) {
162✔
914
        QString source_name = data_source_string.mid(11);// Remove "Intervals: " prefix
32✔
915
        row_selector_type = RowSelectorType::IntervalBased;
32✔
916

917
        if (auto interval_source = data_manager_extension->getIntervalSource(source_name.toStdString())) {
32✔
918
            result = interval_source;
32✔
919
        }
32✔
920
    } else if (data_source_string.startsWith("analog:")) {
96✔
921
        QString source_name = data_source_string.mid(7);// Remove "analog:" prefix
64✔
922
        row_selector_type = RowSelectorType::IntervalBased;
64✔
923

924
        if (auto analog_source = data_manager_extension->getAnalogSource(source_name.toStdString())) {
64✔
925
            result = analog_source;
64✔
926
        }
64✔
927
    }
64✔
928

929
    return {result, row_selector_type};
580✔
930
}
290✔
931

932
void TableDesignerWidget::updateRowInfoLabel(QString const & selected_source) {
18✔
933
    if (selected_source.isEmpty()) {
18✔
UNCOV
934
        ui->row_info_label->setText("No row source selected");
×
UNCOV
935
        return;
×
936
    }
937

938
    // Parse the selected source to get type and name
939
    QString source_type;
18✔
940
    QString source_name;
18✔
941

942
    if (selected_source.startsWith("TimeFrame: ")) {
18✔
943
        source_type = "TimeFrame";
11✔
944
        source_name = selected_source.mid(11);// Remove "TimeFrame: " prefix
11✔
945
    } else if (selected_source.startsWith("Events: ")) {
7✔
UNCOV
946
        source_type = "Events";
×
UNCOV
947
        source_name = selected_source.mid(8);// Remove "Events: " prefix
×
948
    } else if (selected_source.startsWith("Intervals: ")) {
7✔
949
        source_type = "Intervals";
7✔
950
        source_name = selected_source.mid(11);// Remove "Intervals: " prefix
7✔
951
    }
952

953
    // Get additional information about the selected source
954
    QString info_text = QString("Selected: %1 (%2)").arg(source_name, source_type);
18✔
955

956
    if (!_data_manager) {
18✔
UNCOV
957
        ui->row_info_label->setText(info_text);
×
958
        return;
×
959
    }
960

961
    auto * reg3 = _data_manager->getTableRegistry();
18✔
962
    auto data_manager_extension = reg3 ? reg3->getDataManagerExtension() : nullptr;
18✔
963
    if (!data_manager_extension) {
18✔
UNCOV
964
        ui->row_info_label->setText(info_text);
×
UNCOV
965
        return;
×
966
    }
967

968
    auto const source_name_str = source_name.toStdString();
18✔
969

970
    // Add specific information based on source type
971
    if (source_type == "TimeFrame") {
18✔
972
        auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
11✔
973
        if (timeframe) {
11✔
974
            info_text += QString(" - %1 time points").arg(timeframe->getTotalFrameCount());
11✔
975
        }
976
    } else if (source_type == "Events") {
18✔
UNCOV
977
        auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
UNCOV
978
        if (event_series) {
×
UNCOV
979
            auto events = event_series->getEventSeries();
×
UNCOV
980
            info_text += QString(" - %1 events").arg(events.size());
×
UNCOV
981
        }
×
982
    } else if (source_type == "Intervals") {
7✔
983
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
7✔
984
        if (interval_series) {
7✔
985
            auto intervals = interval_series->getDigitalIntervalSeries();
7✔
986
            info_text += QString(" - %1 intervals").arg(intervals.size());
7✔
987

988
            // Add capture range and interval setting information
989
            if (isIntervalItselfSelected()) {
7✔
UNCOV
990
                info_text += QString("\nUsing intervals as-is (no capture range)");
×
991
            } else {
992
                int capture_range = getCaptureRange();
7✔
993
                QString interval_point = isIntervalBeginningSelected() ? "beginning" : "end";
7✔
994
                info_text += QString("\nCapture range: ±%1 samples around %2 of intervals").arg(capture_range).arg(interval_point);
7✔
995
            }
7✔
996
        }
7✔
997
    }
7✔
998

999
    ui->row_info_label->setText(info_text);
18✔
1000
}
18✔
1001

1002
std::unique_ptr<IRowSelector> TableDesignerWidget::createRowSelector(QString const & row_source) {
10✔
1003
    // Parse the row source to get type and name
1004
    QString source_type;
10✔
1005
    QString source_name;
10✔
1006

1007
    if (row_source.startsWith("TimeFrame: ")) {
10✔
UNCOV
1008
        source_type = "TimeFrame";
×
UNCOV
1009
        source_name = row_source.mid(11);// Remove "TimeFrame: " prefix
×
1010
    } else if (row_source.startsWith("Events: ")) {
10✔
1011
        source_type = "Events";
×
1012
        source_name = row_source.mid(8);// Remove "Events: " prefix
×
1013
    } else if (row_source.startsWith("Intervals: ")) {
10✔
1014
        source_type = "Intervals";
10✔
1015
        source_name = row_source.mid(11);// Remove "Intervals: " prefix
10✔
1016
    } else {
UNCOV
1017
        qDebug() << "Unknown row source format:" << row_source;
×
UNCOV
1018
        return nullptr;
×
1019
    }
1020

1021
    auto const source_name_str = source_name.toStdString();
10✔
1022

1023
    try {
1024
        if (source_type == "TimeFrame") {
10✔
1025
            // Create IntervalSelector using TimeFrame
UNCOV
1026
            auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
×
1027
            if (!timeframe) {
×
1028
                qDebug() << "TimeFrame not found:" << source_name;
×
1029
                return nullptr;
×
1030
            }
1031

1032
            // Use timestamps to select all rows
UNCOV
1033
            std::vector<TimeFrameIndex> timestamps;
×
UNCOV
1034
            for (int64_t i = 0; i < timeframe->getTotalFrameCount(); ++i) {
×
1035
                timestamps.push_back(TimeFrameIndex(i));
×
1036
            }
1037
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe);
×
1038

1039
        } else if (source_type == "Events") {
10✔
1040
            // Create TimestampSelector using DigitalEventSeries
1041
            auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
1042
            if (!event_series) {
×
1043
                qDebug() << "DigitalEventSeries not found:" << source_name;
×
1044
                return nullptr;
×
1045
            }
1046

UNCOV
1047
            auto events = event_series->getEventSeries();
×
UNCOV
1048
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
×
UNCOV
1049
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
×
1050
            if (!timeframe_obj) {
×
1051
                qDebug() << "TimeFrame not found for events:" << timeframe_key.str();
×
1052
                return nullptr;
×
1053
            }
1054

1055
            // Convert events to TimeFrameIndex
UNCOV
1056
            std::vector<TimeFrameIndex> timestamps;
×
UNCOV
1057
            for (auto const & event: events) {
×
UNCOV
1058
                timestamps.push_back(TimeFrameIndex(static_cast<int64_t>(event)));
×
1059
            }
1060

1061
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe_obj);
×
1062

1063
        } else if (source_type == "Intervals") {
10✔
1064
            // Create IntervalSelector using DigitalIntervalSeries with capture range
1065
            auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
10✔
1066
            if (!interval_series) {
10✔
UNCOV
1067
                qDebug() << "DigitalIntervalSeries not found:" << source_name;
×
UNCOV
1068
                return nullptr;
×
1069
            }
1070

1071
            auto intervals = interval_series->getDigitalIntervalSeries();
10✔
1072
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
10✔
1073
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
10✔
1074
            if (!timeframe_obj) {
10✔
UNCOV
1075
                qDebug() << "TimeFrame not found for intervals:" << timeframe_key.str();
×
UNCOV
1076
                return nullptr;
×
1077
            }
1078

1079
            // Get capture range and interval setting
1080
            int capture_range = getCaptureRange();
10✔
1081
            bool use_beginning = isIntervalBeginningSelected();
10✔
1082
            bool use_interval_itself = isIntervalItselfSelected();
10✔
1083

1084
            // Create intervals based on the selected option
1085
            std::vector<TimeFrameInterval> tf_intervals;
10✔
1086
            for (auto const & interval: intervals) {
50✔
1087
                if (use_interval_itself) {
40✔
1088
                    // Use the interval as-is
UNCOV
1089
                    tf_intervals.emplace_back(TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
×
1090
                } else {
1091
                    // Determine the reference point (beginning or end of interval)
1092
                    int64_t reference_point;
1093
                    if (use_beginning) {
40✔
1094
                        reference_point = interval.start;
40✔
1095
                    } else {
UNCOV
1096
                        reference_point = interval.end;
×
1097
                    }
1098

1099
                    // Create a new interval around the reference point
1100
                    int64_t start_point = reference_point - capture_range;
40✔
1101
                    int64_t end_point = reference_point + capture_range;
40✔
1102

1103
                    // Ensure bounds are within the timeframe
1104
                    start_point = std::max(start_point, int64_t(0));
40✔
1105
                    end_point = std::min(end_point, static_cast<int64_t>(timeframe_obj->getTotalFrameCount() - 1));
40✔
1106

1107
                    tf_intervals.emplace_back(TimeFrameIndex(start_point), TimeFrameIndex(end_point));
40✔
1108
                }
1109
            }
1110

1111
            return std::make_unique<IntervalSelector>(std::move(tf_intervals), timeframe_obj);
10✔
1112
        }
10✔
1113

1114
    } catch (std::exception const & e) {
×
UNCOV
1115
        qDebug() << "Exception creating row selector:" << e.what();
×
UNCOV
1116
        return nullptr;
×
1117
    }
×
1118

1119
    qDebug() << "Unsupported row source type:" << source_type;
×
1120
    return nullptr;
×
1121
}
10✔
1122

UNCOV
1123
bool TableDesignerWidget::addColumnToBuilder(TableViewBuilder & builder, ColumnInfo const & column_info) {
×
1124
    // Use the simplified TableRegistry method that handles all the type checking internally
1125
    auto * reg = _data_manager->getTableRegistry();
×
1126
    if (!reg) {
×
1127
        qDebug() << "TableRegistry not available";
×
UNCOV
1128
        return false;
×
1129
    }
1130

UNCOV
1131
    bool success = reg->addColumnToBuilder(builder, column_info);
×
UNCOV
1132
    if (!success) {
×
UNCOV
1133
        qDebug() << "Failed to add column to builder:" << QString::fromStdString(column_info.name);
×
1134
    }
1135

UNCOV
1136
    return success;
×
1137
}
1138

1139
void TableDesignerWidget::updateIntervalSettingsVisibility() {
18✔
1140
    if (!ui->interval_settings_group) {
18✔
1141
        return;
×
1142
    }
1143

1144
    QString selected_key = ui->row_data_source_combo->currentText();
18✔
1145
    if (selected_key.isEmpty()) {
18✔
UNCOV
1146
        ui->interval_settings_group->setVisible(false);
×
UNCOV
1147
        if (ui->capture_range_spinbox) {
×
1148
            ui->capture_range_spinbox->setEnabled(false);
×
1149
        }
1150
        return;
×
1151
    }
1152

1153
    if (!_data_manager) {
18✔
UNCOV
1154
        ui->interval_settings_group->setVisible(false);
×
UNCOV
1155
        if (ui->capture_range_spinbox) {
×
UNCOV
1156
            ui->capture_range_spinbox->setEnabled(false);
×
1157
        }
UNCOV
1158
        return;
×
1159
    }
1160

1161
    // Check if the selected source is an interval series
1162
    if (selected_key.startsWith("Intervals: ")) {
18✔
1163
        ui->interval_settings_group->setVisible(true);
7✔
1164

1165
        // Enable/disable capture range based on interval setting
1166
        if (ui->capture_range_spinbox) {
7✔
1167
            bool use_interval_itself = isIntervalItselfSelected();
7✔
1168
            ui->capture_range_spinbox->setEnabled(!use_interval_itself);
7✔
1169
        }
1170
    } else {
1171
        ui->interval_settings_group->setVisible(false);
11✔
1172
        if (ui->capture_range_spinbox) {
11✔
1173
            ui->capture_range_spinbox->setEnabled(false);
11✔
1174
        }
1175
    }
1176
}
18✔
1177

1178
int TableDesignerWidget::getCaptureRange() const {
17✔
1179
    if (ui->capture_range_spinbox) {
17✔
1180
        return ui->capture_range_spinbox->value();
17✔
1181
    }
UNCOV
1182
    return 30000;// Default value
×
1183
}
1184

1185
void TableDesignerWidget::setCaptureRange(int value) {
21✔
1186
    if (ui->capture_range_spinbox) {
21✔
1187
        ui->capture_range_spinbox->blockSignals(true);
21✔
1188
        ui->capture_range_spinbox->setValue(value);
21✔
1189
        ui->capture_range_spinbox->blockSignals(false);
21✔
1190
    }
1191
}
21✔
1192

1193
bool TableDesignerWidget::isIntervalBeginningSelected() const {
17✔
1194
    if (ui->interval_beginning_radio) {
17✔
1195
        return ui->interval_beginning_radio->isChecked();
17✔
1196
    }
UNCOV
1197
    return true;// Default to beginning
×
1198
}
1199

1200
bool TableDesignerWidget::isIntervalItselfSelected() const {
24✔
1201
    if (ui->interval_itself_radio) {
24✔
1202
        return ui->interval_itself_radio->isChecked();
24✔
1203
    }
UNCOV
1204
    return false;// Default to not selected
×
1205
}
1206

1207
void TableDesignerWidget::triggerPreviewDebounced() {
67✔
1208
    if (_preview_debounce_timer) _preview_debounce_timer->start();
67✔
1209
    // Also trigger an immediate rebuild to support non-interactive/test contexts
1210
    rebuildPreviewNow();
67✔
1211
}
67✔
1212

1213
void TableDesignerWidget::rebuildPreviewNow() {
67✔
1214
    if (!_data_manager || !_table_viewer) return;
67✔
1215
    if (_current_table_id.isEmpty()) {
67✔
1216
        _table_viewer->clearTable();
40✔
1217
        return;
40✔
1218
    }
1219

1220
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
27✔
1221
    if (row_source.isEmpty()) {
27✔
1222
        _table_viewer->clearTable();
6✔
1223
        return;
6✔
1224
    }
1225

1226
    // Get enabled column infos from the computers tree
1227
    auto column_infos = getEnabledColumnInfos();
21✔
1228
    if (column_infos.empty()) {
21✔
1229
        _table_viewer->clearTable();
12✔
1230
        return;
12✔
1231
    }
1232

1233
    // Create row selector for the entire dataset
1234
    auto selector = createRowSelector(row_source);
9✔
1235
    if (!selector) {
9✔
UNCOV
1236
        _table_viewer->clearTable();
×
UNCOV
1237
        return;
×
1238
    }
1239

1240
    // Apply any saved column order for this table id
1241
    auto desiredOrder = _table_column_order.value(_current_table_id);
9✔
1242
    if (!desiredOrder.isEmpty()) {
9✔
1243
        std::vector<ColumnInfo> reordered;
6✔
1244
        reordered.reserve(column_infos.size());
6✔
1245
        for (auto const & name : desiredOrder) {
17✔
1246
            auto it = std::find_if(column_infos.begin(), column_infos.end(), [&](ColumnInfo const & ci){ return QString::fromStdString(ci.name) == name; });
29✔
1247
            if (it != column_infos.end()) {
11✔
1248
                reordered.push_back(*it);
11✔
1249
            }
1250
        }
1251
        for (auto const & ci : column_infos) {
21✔
1252
            if (std::find_if(reordered.begin(), reordered.end(), [&](ColumnInfo const & x){ return x.name == ci.name; }) == reordered.end()) {
38✔
1253
                reordered.push_back(ci);
4✔
1254
            }
1255
        }
1256
        column_infos = std::move(reordered);
6✔
1257
    }
6✔
1258

1259
    // Set up the table viewer with pagination
1260
    _table_viewer->setTableConfiguration(
45✔
1261
            std::move(selector),
9✔
1262
            std::move(column_infos),
9✔
1263
            _data_manager,
9✔
1264
            QString("Preview: %1").arg(_current_table_id));
18✔
1265

1266
    // Capture the current visual order from the viewer
1267
    QStringList currentOrder;
9✔
1268
    if (_table_viewer) {
9✔
1269
        auto * tv = _table_viewer->findChild<QTableView*>();
9✔
1270
        if (tv && tv->model()) {
9✔
1271
            auto * header = tv->horizontalHeader();
9✔
1272
            int cols = tv->model()->columnCount();
9✔
1273
            for (int v = 0; header && v < cols; ++v) {
27✔
1274
                int logical = header->logicalIndex(v);
18✔
1275
                auto name = tv->model()->headerData(logical, Qt::Horizontal, Qt::DisplayRole).toString();
18✔
1276
                currentOrder.push_back(name);
18✔
1277
            }
18✔
1278
        }
1279
    }
1280
    if (!currentOrder.isEmpty()) {
9✔
1281
        _table_column_order[_current_table_id] = currentOrder;
9✔
1282
    }
1283
}
39✔
1284

1285
void TableDesignerWidget::refreshComputersTree() {
32✔
1286
    if (!_data_manager) return;
32✔
1287

1288
    _updating_computers_tree = true;
32✔
1289

1290
    // Preserve previous checkbox states and custom column names
1291
    std::map<std::string, std::pair<Qt::CheckState, QString>> previous_states;
32✔
1292
    if (ui->computers_tree && ui->computers_tree->topLevelItemCount() > 0) {
32✔
1293
        for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
202✔
1294
            auto * data_source_item_old = ui->computers_tree->topLevelItem(i);
182✔
1295
            for (int j = 0; j < data_source_item_old->childCount(); ++j) {
768✔
1296
                auto * computer_item_old = data_source_item_old->child(j);
586✔
1297
                QString ds = computer_item_old->data(0, Qt::UserRole).toString();
586✔
1298
                QString cn = computer_item_old->data(1, Qt::UserRole).toString();
586✔
1299
                std::string key = (ds + "||" + cn).toStdString();
586✔
1300
                previous_states[key] = {computer_item_old->checkState(1), computer_item_old->text(2)};
586✔
1301
            }
586✔
1302
        }
1303
    }
1304

1305
    ui->computers_tree->clear();
32✔
1306
    ui->computers_tree->setHeaderLabels({"Data Source / Computer", "Enabled", "Column Name"});
160✔
1307

1308
    auto * registry = _data_manager->getTableRegistry();
32✔
1309
    if (!registry) {
32✔
1310
        _updating_computers_tree = false;
×
1311
        return;
×
1312
    }
1313

1314
    auto data_manager_extension = registry->getDataManagerExtension();
32✔
1315
    if (!data_manager_extension) {
32✔
UNCOV
1316
        _updating_computers_tree = false;
×
UNCOV
1317
        return;
×
1318
    }
1319

1320
    auto & computer_registry = registry->getComputerRegistry();
32✔
1321

1322
    // Get available data sources
1323
    auto data_sources = getAvailableDataSources();
32✔
1324

1325
    // Create tree structure: Data Source -> Computers
1326
    for (QString const & data_source: data_sources) {
322✔
1327
        auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
290✔
1328
        data_source_item->setText(0, data_source);
290✔
1329
        data_source_item->setFlags(Qt::ItemIsEnabled);
290✔
1330
        data_source_item->setExpanded(false);// Start collapsed
290✔
1331

1332
        // Convert data source string to DataSourceVariant and determine RowSelectorType
1333
        auto [data_source_variant, row_selector_type] = createDataSourceVariant(data_source, data_manager_extension);
290✔
1334

1335
        if (!data_source_variant.has_value()) {
290✔
1336
            qDebug() << "Failed to create data source variant for:" << data_source;
128✔
1337
            continue;
128✔
1338
        }
1339

1340
        // Get available computers for this specific data source and row selector combination
1341
        auto available_computers = computer_registry.getAvailableComputers(row_selector_type, data_source_variant.value());
162✔
1342

1343
        // Add compatible computers as children
1344
        for (auto const & computer_info: available_computers) {
1,096✔
1345
            auto * computer_item = new QTreeWidgetItem(data_source_item);
934✔
1346
            computer_item->setText(0, QString::fromStdString(computer_info.name));
934✔
1347
            computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
934✔
1348
            computer_item->setCheckState(1, Qt::Unchecked);
934✔
1349

1350
            // Generate default column name
1351
            QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
934✔
1352
            computer_item->setText(2, default_name);
934✔
1353
            computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
934✔
1354

1355
            // Store data source and computer name for later use
1356
            computer_item->setData(0, Qt::UserRole, data_source);
934✔
1357
            computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
934✔
1358

1359
            // Restore previous state if present
1360
            std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
934✔
1361
            auto it_prev = previous_states.find(prev_key);
934✔
1362
            if (it_prev != previous_states.end()) {
934✔
1363
                computer_item->setCheckState(1, it_prev->second.first);
583✔
1364
                if (!it_prev->second.second.isEmpty()) {
583✔
1365
                    computer_item->setText(2, it_prev->second.second);
583✔
1366
                }
1367
            }
1368
        }
934✔
1369
    }
290✔
1370

1371
    // Resize columns to content
1372
    ui->computers_tree->resizeColumnToContents(0);
32✔
1373
    ui->computers_tree->resizeColumnToContents(1);
32✔
1374
    ui->computers_tree->resizeColumnToContents(2);
32✔
1375

1376
    _updating_computers_tree = false;
32✔
1377

1378
    // Update preview after refresh
1379
    triggerPreviewDebounced();
32✔
1380
}
64✔
1381

1382
void TableDesignerWidget::onComputersTreeItemChanged() {
7,138✔
1383
    if (_updating_computers_tree) return;
7,138✔
1384

1385
    // Trigger preview update when checkbox states change
1386
    triggerPreviewDebounced();
13✔
1387
}
1388

1389
void TableDesignerWidget::onComputersTreeItemEdited(QTreeWidgetItem * item, int column) {
7,138✔
1390
    if (_updating_computers_tree) return;
7,138✔
1391

1392
    // Only respond to column name edits (column 2)
1393
    if (column == 2) {
13✔
1394
        // Column name was edited, trigger preview update
1395
        triggerPreviewDebounced();
1✔
1396
    }
1397
}
1398

1399
std::vector<ColumnInfo> TableDesignerWidget::getEnabledColumnInfos() const {
26✔
1400
    std::vector<ColumnInfo> column_infos;
26✔
1401

1402
    if (!ui->computers_tree) return column_infos;
26✔
1403

1404
    // Iterate through all data source items
1405
    for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
260✔
1406
        auto * data_source_item = ui->computers_tree->topLevelItem(i);
234✔
1407

1408
        // Iterate through computer items under each data source
1409
        for (int j = 0; j < data_source_item->childCount(); ++j) {
988✔
1410
            auto * computer_item = data_source_item->child(j);
754✔
1411

1412
            // Check if this computer is enabled
1413
            if (computer_item->checkState(1) == Qt::Checked) {
754✔
1414
                QString data_source = computer_item->data(0, Qt::UserRole).toString();
28✔
1415
                QString computer_name = computer_item->data(1, Qt::UserRole).toString();
28✔
1416
                QString column_name = computer_item->text(2);
28✔
1417

1418
                if (column_name.isEmpty()) {
28✔
UNCOV
1419
                    column_name = generateDefaultColumnName(data_source, computer_name);
×
1420
                }
1421

1422
                // Create ColumnInfo (use raw key without UI prefixes)
1423
                QString source_key = data_source;
28✔
1424
                if (source_key.startsWith("Events: ")) {
28✔
1425
                    source_key = source_key.mid(8);
28✔
UNCOV
1426
                } else if (source_key.startsWith("Intervals: ")) {
×
UNCOV
1427
                    source_key = source_key.mid(11);
×
UNCOV
1428
                } else if (source_key.startsWith("analog:")) {
×
UNCOV
1429
                    source_key = source_key.mid(7);
×
UNCOV
1430
                } else if (source_key.startsWith("TimeFrame: ")) {
×
UNCOV
1431
                    source_key = source_key.mid(11);
×
1432
                }
1433

1434
                ColumnInfo info(column_name.toStdString(),
84✔
1435
                                QString("Column from %1 using %2").arg(data_source, computer_name).toStdString(),
56✔
1436
                                source_key.toStdString(),
56✔
1437
                                computer_name.toStdString());
140✔
1438

1439
                // Set output type based on computer info
1440
                if (auto * registry = _data_manager->getTableRegistry()) {
28✔
1441
                    auto & computer_registry = registry->getComputerRegistry();
28✔
1442
                    auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
28✔
1443
                    if (computer_info) {
28✔
1444
                        info.outputType = computer_info->outputType;
28✔
1445
                        info.outputTypeName = computer_info->outputTypeName;
28✔
1446
                        info.isVectorType = computer_info->isVectorType;
28✔
1447
                        if (info.isVectorType) {
28✔
1448
                            info.elementType = computer_info->elementType;
5✔
1449
                            info.elementTypeName = computer_info->elementTypeName;
5✔
1450
                        }
1451
                    }
1452
                }
1453

1454
                column_infos.push_back(std::move(info));
28✔
1455
            }
28✔
1456
        }
1457
    }
1458

1459
    return column_infos;
26✔
1460
}
×
1461

1462
bool TableDesignerWidget::isComputerCompatibleWithDataSource(std::string const & computer_name, QString const & data_source) const {
×
1463
    if (!_data_manager) return false;
×
1464

UNCOV
1465
    auto * registry = _data_manager->getTableRegistry();
×
UNCOV
1466
    if (!registry) return false;
×
1467

UNCOV
1468
    auto & computer_registry = registry->getComputerRegistry();
×
1469
    auto computer_info = computer_registry.findComputerInfo(computer_name);
×
1470
    if (!computer_info) return false;
×
1471

1472
    // Basic compatibility check based on data source type and common computer patterns
1473
    if (data_source.startsWith("Events: ")) {
×
1474
        // Event-based computers typically have "Event" in their name
UNCOV
1475
        return computer_name.find("Event") != std::string::npos;
×
1476
    } else if (data_source.startsWith("Intervals: ")) {
×
1477
        // Interval-based computers typically work with intervals or events
UNCOV
1478
        return computer_name.find("Event") != std::string::npos ||
×
1479
               computer_name.find("Interval") != std::string::npos;
×
1480
    } else if (data_source.startsWith("analog:")) {
×
1481
        // Analog-based computers typically have "Analog" in their name
UNCOV
1482
        return computer_name.find("Analog") != std::string::npos;
×
UNCOV
1483
    } else if (data_source.startsWith("TimeFrame: ")) {
×
1484
        // TimeFrame-based computers - generally most computers can work with timestamps
UNCOV
1485
        return computer_name.find("Timestamp") != std::string::npos ||
×
UNCOV
1486
               computer_name.find("Time") != std::string::npos;
×
1487
    }
1488

1489
    // Default: assume compatibility for unrecognized patterns
UNCOV
1490
    return true;
×
1491
}
1492

1493
QString TableDesignerWidget::generateDefaultColumnName(QString const & data_source, QString const & computer_name) const {
934✔
1494
    QString source_name = data_source;
934✔
1495

1496
    // Extract the actual name from prefixed data sources
1497
    if (source_name.startsWith("Events: ")) {
934✔
1498
        source_name = source_name.mid(8);
198✔
1499
    } else if (source_name.startsWith("Intervals: ")) {
736✔
1500
        source_name = source_name.mid(11);
224✔
1501
    } else if (source_name.startsWith("analog:")) {
512✔
1502
        source_name = source_name.mid(7);
512✔
UNCOV
1503
    } else if (source_name.startsWith("TimeFrame: ")) {
×
UNCOV
1504
        source_name = source_name.mid(11);
×
1505
    }
1506

1507
    // Create a concise name
1508
    return QString("%1_%2").arg(source_name, computer_name);
2,802✔
1509
}
934✔
1510

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