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

paulmthompson / WhiskerToolbox / 17560376015

08 Sep 2025 06:27PM UTC coverage: 71.465% (-0.06%) from 71.521%
17560376015

push

github

paulmthompson
create tabletransformwidget

0 of 41 new or added lines in 1 file covered. (0.0%)

173 existing lines in 1 file now uncovered.

36443 of 50994 relevant lines covered (71.47%)

1282.94 hits per line

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

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

24

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

40
#include <QFutureWatcher>
41
#include <QTimer>
42
#include <QtConcurrent>
43

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

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

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

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

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

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

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

97
    // Add observer to automatically refresh dropdowns when DataManager changes
98
    if (_data_manager) {
9✔
99
        _data_manager->addObserver([this]() {
9✔
100
            refreshAllDataSources();
2✔
101
        });
2✔
102
    }
103

104
    qDebug() << "TableDesignerWidget initialized with TableViewerWidget for efficient pagination";
9✔
105
}
9✔
106

107
TableDesignerWidget::~TableDesignerWidget() {
9✔
108
    delete ui;
9✔
109
}
9✔
110

111
void TableDesignerWidget::refreshAllDataSources() {
2✔
112
    qDebug() << "Manually refreshing all data sources...";
2✔
113
    refreshRowDataSourceCombo();
2✔
114
    refreshComputersTree();
2✔
115

116
    // If we have a selected table, refresh its info
117
    if (!_current_table_id.isEmpty()) {
2✔
UNCOV
118
        loadTableInfo(_current_table_id);
×
119
    }
120
}
2✔
121

122

123
void TableDesignerWidget::connectSignals() {
9✔
124
    // Table selection signals
125
    connect(ui->table_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
27✔
126
            this, &TableDesignerWidget::onTableSelectionChanged);
18✔
127
    connect(ui->new_table_btn, &QPushButton::clicked,
27✔
128
            this, &TableDesignerWidget::onCreateNewTable);
18✔
129
    connect(ui->delete_table_btn, &QPushButton::clicked,
27✔
130
            this, &TableDesignerWidget::onDeleteTable);
18✔
131

132
    // Table info signals are connected via TableInfoWidget
133

134
    // Row source signals
135
    connect(ui->row_data_source_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
27✔
136
            this, &TableDesignerWidget::onRowDataSourceChanged);
18✔
137
    connect(ui->capture_range_spinbox, QOverload<int>::of(&QSpinBox::valueChanged),
27✔
138
            this, &TableDesignerWidget::onCaptureRangeChanged);
18✔
139
    connect(ui->interval_beginning_radio, &QRadioButton::toggled,
27✔
140
            this, &TableDesignerWidget::onIntervalSettingChanged);
18✔
141
    connect(ui->interval_end_radio, &QRadioButton::toggled,
27✔
142
            this, &TableDesignerWidget::onIntervalSettingChanged);
18✔
143
    connect(ui->interval_itself_radio, &QRadioButton::toggled,
27✔
144
            this, &TableDesignerWidget::onIntervalSettingChanged);
18✔
145

146
    // Column design signals (tree-based)
147
    connect(ui->computers_tree, &QTreeWidget::itemChanged,
27✔
148
            this, &TableDesignerWidget::onComputersTreeItemChanged);
18✔
149
    connect(ui->computers_tree, &QTreeWidget::itemChanged,
27✔
150
            this, &TableDesignerWidget::onComputersTreeItemEdited);
18✔
151

152
    // Build signals
153
    connect(ui->build_table_btn, &QPushButton::clicked,
27✔
154
            this, &TableDesignerWidget::onBuildTable);
18✔
155
    if (ui->apply_transform_btn) {
9✔
156
        connect(ui->apply_transform_btn, &QPushButton::clicked,
27✔
157
                this, &TableDesignerWidget::onApplyTransform);
18✔
158
    }
159
    if (ui->export_csv_btn) {
9✔
160
        connect(ui->export_csv_btn, &QPushButton::clicked,
27✔
161
                this, &TableDesignerWidget::onExportCsv);
18✔
162
    }
163

164
    // Subscribe to DataManager table observer
165
    if (_data_manager) {
9✔
166
        auto token = _data_manager->addTableObserver([this](TableEvent const & ev) {
18✔
167
            switch (ev.type) {
8✔
168
                case TableEventType::Created:
3✔
169
                    this->onTableManagerTableCreated(QString::fromStdString(ev.tableId));
3✔
170
                    break;
3✔
UNCOV
171
                case TableEventType::Removed:
×
UNCOV
172
                    this->onTableManagerTableRemoved(QString::fromStdString(ev.tableId));
×
UNCOV
173
                    break;
×
174
                case TableEventType::InfoUpdated:
4✔
175
                    this->onTableManagerTableInfoUpdated(QString::fromStdString(ev.tableId));
4✔
176
                    break;
4✔
177
                case TableEventType::DataChanged:
1✔
178
                    // No direct UI change needed here for designer list
179
                    break;
1✔
180
            }
181
        });
17✔
182
        (void) token;// Optionally store and remove on dtor
183
    }
184
}
9✔
185

186
void TableDesignerWidget::onTableSelectionChanged() {
15✔
187
    int current_index = ui->table_combo->currentIndex();
15✔
188
    if (current_index < 0) {
15✔
189
        clearUI();
3✔
190
        return;
3✔
191
    }
192

193
    QString table_id = ui->table_combo->itemData(current_index).toString();
12✔
194
    if (table_id.isEmpty()) {
12✔
195
        clearUI();
9✔
196
        return;
9✔
197
    }
198

199
    _current_table_id = table_id;
3✔
200
    loadTableInfo(table_id);
3✔
201

202
    // Enable/disable controls
203
    ui->delete_table_btn->setEnabled(true);
3✔
204
    // Table info section is controlled separately
205
    ui->build_table_btn->setEnabled(true);
3✔
206
    if (auto gb = this->findChild<QGroupBox*>("row_source_group")) gb->setEnabled(true);
6✔
207
    if (auto gb = this->findChild<QGroupBox*>("column_design_group")) gb->setEnabled(true);
6✔
208
    // Enable save info within TableInfoWidget
209
    if (_table_info_section) _table_info_section->setEnabled(true);
3✔
210

211
    updateBuildStatus("Table selected: " + table_id);
3✔
212

213
    qDebug() << "Selected table:" << table_id;
3✔
214
}
12✔
215

UNCOV
216
void TableDesignerWidget::onCreateNewTable() {
×
UNCOV
217
    bool ok;
×
UNCOV
218
    QString name = QInputDialog::getText(this, "New Table", "Enter table name:", QLineEdit::Normal, "New Table", &ok);
×
219

UNCOV
220
    if (!ok || name.isEmpty()) {
×
UNCOV
221
        return;
×
222
    }
223

224
    auto * registry = _data_manager->getTableRegistry();
×
225
    if (!registry) { return; }
×
UNCOV
226
    auto table_id = registry->generateUniqueTableId("Table");
×
227

228
    if (registry->createTable(table_id, name.toStdString())) {
×
229
        // The combo will be refreshed by the signal handler
230
        // Set the new table as selected
231
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
232
            if (ui->table_combo->itemData(i).toString().toStdString() == table_id) {
×
233
                ui->table_combo->setCurrentIndex(i);
×
UNCOV
234
                break;
×
235
            }
236
        }
237
    } else {
238
        QMessageBox::warning(this, "Error", "Failed to create table with ID: " + QString::fromStdString(table_id));
×
239
    }
240
}
×
241

UNCOV
242
void TableDesignerWidget::onDeleteTable() {
×
UNCOV
243
    if (_current_table_id.isEmpty()) {
×
UNCOV
244
        return;
×
245
    }
246

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

251
    if (reply == QMessageBox::Yes) {
×
UNCOV
252
        auto * registry = _data_manager->getTableRegistry();
×
UNCOV
253
        if (registry && registry->removeTable(_current_table_id.toStdString())) {
×
254
            // The combo will be refreshed by the signal handler
255
            clearUI();
×
256
        } else {
UNCOV
257
            QMessageBox::warning(this, "Error", "Failed to delete table: " + _current_table_id);
×
258
        }
259
    }
260
}
261

262
void TableDesignerWidget::onRowDataSourceChanged() {
19✔
263
    QString selected = ui->row_data_source_combo->currentText();
19✔
264
    if (selected.isEmpty()) {
19✔
265
        ui->row_info_label->setText("No row source selected");
5✔
266
        return;
5✔
267
    }
268

269
    // Save the row source selection to the current table
270
    // Only save if we have a current table and we're not loading table info
271
    if (!_current_table_id.isEmpty() && _data_manager) {
14✔
272
        if (auto * reg = _data_manager->getTableRegistry()) {
3✔
273
            reg->updateTableRowSource(_current_table_id.toStdString(), selected.toStdString());
3✔
274
        }
275
    }
276

277
    // Update the info label
278
    updateRowInfoLabel(selected);
14✔
279

280
    // Update interval settings visibility
281
    updateIntervalSettingsVisibility();
14✔
282

283
    // Refresh computers tree since available computers depend on row selector type
284
    refreshComputersTree();
14✔
285

286
    qDebug() << "Row data source changed to:" << selected;
14✔
287
    triggerPreviewDebounced();
14✔
288
}
19✔
289

UNCOV
290
void TableDesignerWidget::onCaptureRangeChanged() {
×
291
    // Update the info label to reflect the new capture range
UNCOV
292
    QString selected = ui->row_data_source_combo->currentText();
×
UNCOV
293
    if (!selected.isEmpty()) {
×
UNCOV
294
        updateRowInfoLabel(selected);
×
295
    }
UNCOV
296
    triggerPreviewDebounced();
×
297
}
×
298

299
void TableDesignerWidget::onIntervalSettingChanged() {
×
300
    // Update the info label to reflect the new interval setting
301
    QString selected = ui->row_data_source_combo->currentText();
×
UNCOV
302
    if (!selected.isEmpty()) {
×
303
        updateRowInfoLabel(selected);
×
304
    }
305

306
    // Update capture range visibility based on interval setting
UNCOV
307
    updateIntervalSettingsVisibility();
×
308
    triggerPreviewDebounced();
×
309
}
×
310

311

UNCOV
312
void TableDesignerWidget::onBuildTable() {
×
UNCOV
313
    if (_current_table_id.isEmpty()) {
×
314
        updateBuildStatus("No table selected", true);
×
315
        return;
×
316
    }
317

UNCOV
318
    QString row_source = ui->row_data_source_combo->currentText();
×
319
    if (row_source.isEmpty()) {
×
320
        updateBuildStatus("No row data source selected", true);
×
321
        return;
×
322
    }
323

324
    // Get enabled column infos from the tree
325
    auto column_infos = getEnabledColumnInfos();
×
326
    if (column_infos.empty()) {
×
327
        updateBuildStatus("No computers enabled. Check boxes in the tree to enable computers.", true);
×
328
        return;
×
329
    }
330

331
    try {
332
        // Create the row selector
333
        auto row_selector = createRowSelector(row_source);
×
334
        if (!row_selector) {
×
335
            updateBuildStatus("Failed to create row selector", true);
×
UNCOV
336
            return;
×
337
        }
338

339
        // Get the data manager extension
340
        auto * reg = _data_manager->getTableRegistry();
×
341
        auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
×
342
        if (!data_manager_extension) {
×
343
            updateBuildStatus("DataManager extension not available", true);
×
UNCOV
344
            return;
×
345
        }
346

347
        // Create the TableViewBuilder
348
        TableViewBuilder builder(data_manager_extension);
×
349
        builder.setRowSelector(std::move(row_selector));
×
350

351
        // Add all enabled columns from the tree
UNCOV
352
        bool all_columns_valid = true;
×
UNCOV
353
        for (auto const & column_info: column_infos) {
×
UNCOV
354
            if (!reg->addColumnToBuilder(builder, column_info)) {
×
355
                updateBuildStatus(QString("Failed to create column: %1").arg(QString::fromStdString(column_info.name)), true);
×
356
                all_columns_valid = false;
×
UNCOV
357
                break;
×
358
            }
359
        }
360

361
        if (!all_columns_valid) {
×
362
            return;
×
363
        }
364

365
        // Build the table
UNCOV
366
        auto table_view = builder.build();
×
367

368
        // Store the built table in the TableManager and update table info with current columns
369
        if (reg) {
×
370
            // Update table info with current column configuration
UNCOV
371
            auto table_info = reg->getTableInfo(_current_table_id.toStdString());
×
UNCOV
372
            table_info.columns = column_infos;// Store the current enabled columns
×
373
            reg->updateTableInfo(_current_table_id.toStdString(),
×
374
                                 table_info.name, table_info.description);
375

376
            // Store the built table
UNCOV
377
            if (reg->storeBuiltTable(_current_table_id.toStdString(), std::move(table_view))) {
×
378
                updateBuildStatus(QString("Table built successfully with %1 columns!").arg(column_infos.size()));
×
379
                qDebug() << "Successfully built table:" << _current_table_id << "with" << column_infos.size() << "columns";
×
380
            } else {
UNCOV
381
                updateBuildStatus("Failed to store built table", true);
×
382
            }
UNCOV
383
        } else {
×
384
            updateBuildStatus("Registry unavailable", true);
×
385
        }
386

UNCOV
387
    } catch (std::exception const & e) {
×
388
        updateBuildStatus(QString("Error building table: %1").arg(e.what()), true);
×
UNCOV
389
        qDebug() << "Exception during table building:" << e.what();
×
390
    }
×
391
}
×
392

393
bool TableDesignerWidget::buildTableFromTree() {
1✔
394
    // This is essentially the same as onBuildTable but returns success status
395
    if (_current_table_id.isEmpty()) {
1✔
396
        updateBuildStatus("No table selected", true);
×
397
        return false;
×
398
    }
399

400
    QString row_source = ui->row_data_source_combo->currentText();
1✔
401
    if (row_source.isEmpty()) {
1✔
UNCOV
402
        updateBuildStatus("No row data source selected", true);
×
403
        return false;
×
404
    }
405

406
    // Get enabled column infos from the tree
407
    auto column_infos = getEnabledColumnInfos();
1✔
408
    if (column_infos.empty()) {
1✔
409
        updateBuildStatus("No computers enabled. Check boxes in the tree to enable computers.", true);
×
410
        return false;
×
411
    }
412

413
    try {
414
        // Create the row selector
415
        auto row_selector = createRowSelector(row_source);
1✔
416
        if (!row_selector) {
1✔
417
            updateBuildStatus("Failed to create row selector", true);
×
UNCOV
418
            return false;
×
419
        }
420

421
        // Get the data manager extension
422
        auto * reg = _data_manager->getTableRegistry();
1✔
423
        auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
1✔
424
        if (!data_manager_extension) {
1✔
425
            updateBuildStatus("DataManager extension not available", true);
×
UNCOV
426
            return false;
×
427
        }
428

429
        // Create the TableViewBuilder
430
        TableViewBuilder builder(data_manager_extension);
1✔
431
        builder.setRowSelector(std::move(row_selector));
1✔
432

433
        // Add all enabled columns from the tree
434
        bool all_columns_valid = true;
1✔
435
        for (auto const & column_info: column_infos) {
4✔
436
            if (!reg->addColumnToBuilder(builder, column_info)) {
3✔
UNCOV
437
                updateBuildStatus(QString("Failed to create column: %1").arg(QString::fromStdString(column_info.name)), true);
×
UNCOV
438
                all_columns_valid = false;
×
UNCOV
439
                break;
×
440
            }
441
        }
442

443
        if (!all_columns_valid) {
1✔
444
            return false;
×
445
        }
446

447
        // Build the table
448
        auto table_view = builder.build();
1✔
449

450
        // Store the built table in the TableManager and update table info with current columns
451
        if (reg) {
1✔
452
            // Update table info with current column configuration
453
            auto table_info = reg->getTableInfo(_current_table_id.toStdString());
1✔
454
            table_info.columns = column_infos;// Store the current enabled columns
1✔
455
            reg->updateTableInfo(_current_table_id.toStdString(),
1✔
456
                                 table_info.name,
457
                                 table_info.description);
458

459
            // Store the built table
460
            if (reg->storeBuiltTable(_current_table_id.toStdString(), std::move(table_view))) {
1✔
461
                updateBuildStatus(QString("Table built successfully with %1 columns!").arg(column_infos.size()));
1✔
462
                qDebug() << "Successfully built table:" << _current_table_id << "with" << column_infos.size() << "columns";
1✔
463
                return true;
1✔
464
            } else {
UNCOV
465
                updateBuildStatus("Failed to store built table", true);
×
UNCOV
466
                return false;
×
467
            }
468
        } else {
1✔
UNCOV
469
            updateBuildStatus("Registry unavailable", true);
×
UNCOV
470
            return false;
×
471
        }
472

473
    } catch (std::exception const & e) {
1✔
UNCOV
474
        updateBuildStatus(QString("Error building table: %1").arg(e.what()), true);
×
UNCOV
475
        qDebug() << "Exception during table building:" << e.what();
×
476
        return false;
×
477
    }
×
478
}
1✔
479

UNCOV
480
void TableDesignerWidget::onApplyTransform() {
×
481
    if (_current_table_id.isEmpty() || !_data_manager) {
×
482
        updateBuildStatus("No base table selected", true);
×
483
        return;
×
484
    }
485

486
    // Fetch the built base table
487
    auto * reg = _data_manager->getTableRegistry();
×
488
    if (!reg) {
×
489
        updateBuildStatus("Registry unavailable", true);
×
490
        return;
×
491
    }
UNCOV
492
    auto base_view = reg->getBuiltTable(_current_table_id.toStdString());
×
UNCOV
493
    if (!base_view) {
×
494
        updateBuildStatus("Build the base table first", true);
×
495
        return;
×
496
    }
497

498
    // Currently only PCA option is exposed
499
    QString transform = ui->transform_type_combo ? ui->transform_type_combo->currentText() : QString();
×
500
    if (transform != "PCA") {
×
501
        updateBuildStatus("Unsupported transform", true);
×
502
        return;
×
503
    }
504

505
    // Configure PCA
506
    PCAConfig cfg;
×
507
    cfg.center = ui->transform_center_checkbox && ui->transform_center_checkbox->isChecked();
×
508
    cfg.standardize = ui->transform_standardize_checkbox && ui->transform_standardize_checkbox->isChecked();
×
509
    if (ui->transform_include_edit) {
×
UNCOV
510
        for (auto const & s: parseCommaSeparatedList(ui->transform_include_edit->text())) cfg.include.push_back(s);
×
511
    }
UNCOV
512
    if (ui->transform_exclude_edit) {
×
513
        for (auto const & s: parseCommaSeparatedList(ui->transform_exclude_edit->text())) cfg.exclude.push_back(s);
×
514
    }
515

516
    try {
517
        PCATransform pca(cfg);
×
UNCOV
518
        TableView derived = pca.apply(*base_view);
×
519

520
        // Determine output id/name
UNCOV
521
        QString out_name = ui->transform_output_name_edit ? ui->transform_output_name_edit->text().trimmed()
×
UNCOV
522
                                                          : QString();
×
UNCOV
523
        if (out_name.isEmpty()) {
×
524
            QString base = _table_info_widget ? _table_info_widget->getName() : QString();
×
525
            out_name = base.isEmpty() ? QString("(PCA)") : QString("%1 (PCA)").arg(base);
×
UNCOV
526
        }
×
527

528
        std::string out_id = reg->generateUniqueTableId((_current_table_id + "_pca").toStdString());
×
529
        if (!reg->createTable(out_id, out_name.toStdString())) {
×
530
            reg->updateTableInfo(out_id, out_name.toStdString(), "");
×
531
        }
532
        if (reg->storeBuiltTable(out_id, std::move(derived))) {
×
533
            updateBuildStatus(QString("Created transformed table: %1").arg(out_name));
×
UNCOV
534
            refreshTableCombo();
×
535
        } else {
536
            updateBuildStatus("Failed to store transformed table", true);
×
537
        }
UNCOV
538
    } catch (std::exception const & e) {
×
539
        updateBuildStatus(QString("Transform failed: %1").arg(e.what()), true);
×
540
    }
×
541
}
×
542

543
std::vector<std::string> TableDesignerWidget::parseCommaSeparatedList(QString const & text) const {
×
UNCOV
544
    std::vector<std::string> out;
×
545
    for (QString s: text.split(",", Qt::SkipEmptyParts)) {
×
546
        s = s.trimmed();
×
547
        if (!s.isEmpty()) out.push_back(s.toStdString());
×
548
    }
×
UNCOV
549
    return out;
×
550
}
×
551

552
void TableDesignerWidget::onExportCsv() {
×
553
    if (_current_table_id.isEmpty() || !_data_manager) {
×
554
        updateBuildStatus("No table selected", true);
×
555
        return;
×
556
    }
557

UNCOV
558
    auto * reg = _data_manager->getTableRegistry();
×
559
    if (!reg) {
×
560
        updateBuildStatus("Registry unavailable", true);
×
561
        return;
×
562
    }
UNCOV
563
    auto view = reg->getBuiltTable(_current_table_id.toStdString());
×
UNCOV
564
    if (!view) {
×
565
        updateBuildStatus("Build the table first", true);
×
566
        return;
×
567
    }
568

UNCOV
569
    QString filename = promptSaveCsvFilename();
×
570
    if (filename.isEmpty()) return;
×
571
    if (!filename.endsWith(".csv", Qt::CaseInsensitive)) filename += ".csv";
×
572

573
    // CSV options
UNCOV
574
    QString delimiter = ui->export_delimiter_combo ? ui->export_delimiter_combo->currentText() : "Comma";
×
UNCOV
575
    QString lineEnding = ui->export_line_ending_combo ? ui->export_line_ending_combo->currentText() : "LF (\\n)";
×
576
    int precision = ui->export_precision_spinbox ? ui->export_precision_spinbox->value() : 3;
×
577
    bool includeHeader = ui->export_header_checkbox && ui->export_header_checkbox->isChecked();
×
578

UNCOV
579
    std::string delim = ",";
×
UNCOV
580
    if (delimiter == "Space") delim = " ";
×
581
    else if (delimiter == "Tab")
×
582
        delim = "\t";
×
583
    std::string eol = "\n";
×
584
    if (lineEnding.startsWith("CRLF")) eol = "\r\n";
×
585

586
    try {
587
        std::ofstream file(filename.toStdString());
×
588
        if (!file.is_open()) {
×
589
            updateBuildStatus("Could not open file for writing", true);
×
590
            return;
×
591
        }
UNCOV
592
        file << std::fixed << std::setprecision(precision);
×
593

594
        auto names = view->getColumnNames();
×
595
        if (includeHeader) {
×
596
            for (size_t i = 0; i < names.size(); ++i) {
×
597
                if (i > 0) file << delim;
×
UNCOV
598
                file << names[i];
×
599
            }
UNCOV
600
            file << eol;
×
601
        }
602
        size_t rows = view->getRowCount();
×
603
        for (size_t r = 0; r < rows; ++r) {
×
604
            for (size_t c = 0; c < names.size(); ++c) {
×
605
                if (c > 0) file << delim;
×
606
                try {
607
                    auto const & vals = view->getColumnValues<double>(names[c].c_str());
×
UNCOV
608
                    if (r < vals.size()) file << vals[r];
×
609
                    else
610
                        file << "NaN";
×
611
                } catch (...) {
×
612
                    file << "NaN";
×
UNCOV
613
                }
×
614
            }
615
            file << eol;
×
616
        }
617
        file.close();
×
618
        updateBuildStatus(QString("Exported CSV: %1").arg(filename));
×
619
    } catch (std::exception const & e) {
×
620
        updateBuildStatus(QString("Export failed: %1").arg(e.what()), true);
×
UNCOV
621
    }
×
622
}
×
623

624
QString TableDesignerWidget::promptSaveCsvFilename() const {
×
625
    return QFileDialog::getSaveFileName(const_cast<TableDesignerWidget *>(this), "Export Table to CSV", QString(), "CSV Files (*.csv)");
×
626
}
627

628
void TableDesignerWidget::onSaveTableInfo() {
×
629
    if (_current_table_id.isEmpty()) {
×
UNCOV
630
        return;
×
631
    }
632

UNCOV
633
    QString name = _table_info_widget ? _table_info_widget->getName() : QString();
×
UNCOV
634
    QString description = _table_info_widget ? _table_info_widget->getDescription() : QString();
×
635

636
    if (name.isEmpty()) {
×
637
        QMessageBox::warning(this, "Error", "Table name cannot be empty");
×
UNCOV
638
        return;
×
639
    }
640

641
    if (auto * reg = _data_manager->getTableRegistry(); reg && reg->updateTableInfo(_current_table_id.toStdString(), name.toStdString(), description.toStdString())) {
×
UNCOV
642
        updateBuildStatus("Table information saved");
×
643
        // Refresh the combo to show updated name
644
        refreshTableCombo();
×
645
        // Restore selection
UNCOV
646
        for (int i = 0; i < ui->table_combo->count(); ++i) {
×
UNCOV
647
            if (ui->table_combo->itemData(i).toString() == _current_table_id) {
×
648
                ui->table_combo->setCurrentIndex(i);
×
649
                break;
×
650
            }
651
        }
652
    } else {
653
        QMessageBox::warning(this, "Error", "Failed to save table information");
×
654
    }
655
}
×
656

657
void TableDesignerWidget::onTableManagerTableCreated(QString const & table_id) {
3✔
658
    refreshTableCombo();
3✔
659
    qDebug() << "Table created signal received:" << table_id;
3✔
660
}
3✔
661

662
void TableDesignerWidget::onTableManagerTableRemoved(QString const & table_id) {
×
UNCOV
663
    refreshTableCombo();
×
UNCOV
664
    if (_current_table_id == table_id) {
×
UNCOV
665
        _current_table_id.clear();
×
UNCOV
666
        clearUI();
×
667
    }
UNCOV
668
    qDebug() << "Table removed signal received:" << table_id;
×
669
}
×
670

671
void TableDesignerWidget::onTableManagerTableInfoUpdated(QString const & table_id) {
4✔
672
    if (_current_table_id == table_id && !_loading_column_configuration) {
4✔
673
        loadTableInfo(table_id);
4✔
674
    }
675
    qDebug() << "Table info updated signal received:" << table_id;
4✔
676
}
4✔
677

678
void TableDesignerWidget::refreshTableCombo() {
12✔
679
    ui->table_combo->clear();
12✔
680

681
    auto * reg = _data_manager->getTableRegistry();
12✔
682
    auto table_infos = reg ? reg->getAllTableInfo() : std::vector<TableInfo>{};
12✔
683
    for (auto const & info: table_infos) {
15✔
684
        ui->table_combo->addItem(QString::fromStdString(info.name), QString::fromStdString(info.id));
3✔
685
    }
686

687
    if (ui->table_combo->count() == 0) {
12✔
688
        ui->table_combo->addItem("(No tables available)", "");
9✔
689
    }
690
}
24✔
691

692
void TableDesignerWidget::refreshRowDataSourceCombo() {
11✔
693
    ui->row_data_source_combo->clear();
11✔
694

695
    if (!_data_manager) {
11✔
UNCOV
696
        qDebug() << "refreshRowDataSourceCombo: No table manager";
×
UNCOV
697
        return;
×
698
    }
699

700
    auto data_sources = getAvailableDataSources();
11✔
701
    qDebug() << "refreshRowDataSourceCombo: Found" << data_sources.size() << "data sources:" << data_sources;
11✔
702

703
    for (QString const & source: data_sources) {
111✔
704
        // Only include valid row sources in this combo
705
        if (source.startsWith("TimeFrame: ") || source.startsWith("Events: ") || source.startsWith("Intervals: ")) {
100✔
706
            ui->row_data_source_combo->addItem(source);
78✔
707
        }
708
    }
709

710
    if (ui->row_data_source_combo->count() == 0) {
11✔
UNCOV
711
        ui->row_data_source_combo->addItem("(No data sources available)");
×
UNCOV
712
        qDebug() << "refreshRowDataSourceCombo: No data sources available";
×
713
    }
714
}
11✔
715

716

717
void TableDesignerWidget::loadTableInfo(QString const & table_id) {
7✔
718
    if (table_id.isEmpty() || !_data_manager) {
7✔
719
        clearUI();
×
UNCOV
720
        return;
×
721
    }
722

723
    auto * reg = _data_manager->getTableRegistry();
7✔
724
    auto info = reg ? reg->getTableInfo(table_id.toStdString()) : TableInfo{};
7✔
725
    if (info.id.empty()) {
7✔
726
        clearUI();
×
727
        return;
×
728
    }
729

730
    // Load table information
731
    if (_table_info_widget) {
7✔
732
        _table_info_widget->setName(QString::fromStdString(info.name));
7✔
733
        _table_info_widget->setDescription(QString::fromStdString(info.description));
7✔
734
    }
735

736
    // Load row source if available
737
    if (!info.rowSourceName.empty()) {
7✔
738
        int row_index = ui->row_data_source_combo->findText(QString::fromStdString(info.rowSourceName));
4✔
739
        if (row_index >= 0) {
4✔
740
            // Block signals to prevent circular dependency when loading table info
741
            ui->row_data_source_combo->blockSignals(true);
4✔
742
            ui->row_data_source_combo->setCurrentIndex(row_index);
4✔
743
            ui->row_data_source_combo->blockSignals(false);
4✔
744

745
            // Manually update the info label without triggering the signal handler
746
            updateRowInfoLabel(QString::fromStdString(info.rowSourceName));
4✔
747

748
            // Update interval settings visibility
749
            updateIntervalSettingsVisibility();
4✔
750

751
            // Since signals were blocked, this will ensure the tree is refreshed
752
            // when the computers tree is populated later in this function
753
        }
754
    }
755

756
    // Clear old column list (deprecated)
757
    // The computers tree will be populated based on available data sources
758
    refreshComputersTree();
7✔
759

760
    updateBuildStatus(QString("Loaded table: %1").arg(QString::fromStdString(info.name)));
7✔
761
    triggerPreviewDebounced();
7✔
762
}
7✔
763

764
void TableDesignerWidget::clearUI() {
21✔
765
    _current_table_id.clear();
21✔
766

767
    // Clear table info
768
    if (_table_info_widget) {
21✔
769
        _table_info_widget->setName("");
21✔
770
        _table_info_widget->setDescription("");
21✔
771
    }
772

773
    // Clear row source
774
    ui->row_data_source_combo->setCurrentIndex(-1);
21✔
775
    ui->row_info_label->setText("No row source selected");
21✔
776

777
    // Reset capture range and interval settings
778
    setCaptureRange(30000);// Default value
21✔
779
    if (ui->interval_beginning_radio) {
21✔
780
        ui->interval_beginning_radio->setChecked(true);
21✔
781
    }
782
    if (ui->interval_itself_radio) {
21✔
783
        ui->interval_itself_radio->setChecked(false);
21✔
784
    }
785
    if (ui->interval_settings_group) {
21✔
786
        ui->interval_settings_group->setVisible(false);
21✔
787
    }
788

789
    // Clear computers tree
790
    if (ui->computers_tree) {
21✔
791
        ui->computers_tree->clear();
21✔
792
    }
793

794
    // Disable controls
795
    ui->delete_table_btn->setEnabled(false);
21✔
796
    // Table info section is controlled separately
797
    ui->build_table_btn->setEnabled(false);
21✔
798
    if (auto gb = this->findChild<QGroupBox*>("row_source_group")) gb->setEnabled(false);
42✔
799
    if (auto gb = this->findChild<QGroupBox*>("column_design_group")) gb->setEnabled(false);
42✔
800
    if (_table_info_section) _table_info_section->setEnabled(false);
21✔
801

802
    updateBuildStatus("No table selected");
21✔
803
    if (_table_viewer) _table_viewer->clearTable();
21✔
804
}
21✔
805

806
void TableDesignerWidget::updateBuildStatus(QString const & message, bool is_error) {
32✔
807
    ui->build_status_label->setText(message);
32✔
808

809
    if (is_error) {
32✔
UNCOV
810
        ui->build_status_label->setStyleSheet("QLabel { color: red; font-weight: bold; }");
×
811
    } else {
812
        ui->build_status_label->setStyleSheet("QLabel { color: green; }");
32✔
813
    }
814
}
32✔
815

816
QStringList TableDesignerWidget::getAvailableDataSources() const {
43✔
817
    QStringList sources;
43✔
818

819
    if (!_data_manager) {
43✔
UNCOV
820
        qDebug() << "getAvailableDataSources: No table manager";
×
UNCOV
821
        return sources;
×
822
    }
823

824
    auto * reg = _data_manager->getTableRegistry();
43✔
825
    auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
43✔
826
    if (!data_manager_extension) {
43✔
827
        qDebug() << "getAvailableDataSources: No data manager extension";
×
828
        return sources;
×
829
    }
830

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

836
    // Add TimeFrame keys as potential row sources
837
    // TimeFrames can define intervals for analysis
838
    auto timeframe_keys = _data_manager->getTimeFrameKeys();
43✔
839
    qDebug() << "getAvailableDataSources: TimeFrame keys:" << timeframe_keys.size();
43✔
840
    for (auto const & key: timeframe_keys) {
215✔
841
        QString source = QString("TimeFrame: %1").arg(QString::fromStdString(key.str()));
172✔
842
        sources << source;
172✔
843
        qDebug() << "  Added TimeFrame:" << source;
172✔
844
    }
172✔
845

846
    // Add DigitalEventSeries keys as potential row sources
847
    // Events can be used to define analysis windows or timestamps
848
    auto event_keys = _data_manager->getKeys<DigitalEventSeries>();
43✔
849
    qDebug() << "getAvailableDataSources: Event keys:" << event_keys.size();
43✔
850
    for (auto const & key: event_keys) {
132✔
851
        QString source = QString("Events: %1").arg(QString::fromStdString(key));
89✔
852
        sources << source;
89✔
853
        qDebug() << "  Added Events:" << source;
89✔
854
    }
89✔
855

856
    // Add DigitalIntervalSeries keys as potential row sources
857
    // Intervals directly define analysis windows
858
    auto interval_keys = _data_manager->getKeys<DigitalIntervalSeries>();
43✔
859
    qDebug() << "getAvailableDataSources: Interval keys:" << interval_keys.size();
43✔
860
    for (auto const & key: interval_keys) {
86✔
861
        QString source = QString("Intervals: %1").arg(QString::fromStdString(key));
43✔
862
        sources << source;
43✔
863
        qDebug() << "  Added Intervals:" << source;
43✔
864
    }
43✔
865

866
    // Add AnalogTimeSeries keys as data sources (for computers; not row selectors)
867
    auto analog_keys = _data_manager->getKeys<AnalogTimeSeries>();
43✔
868
    qDebug() << "getAvailableDataSources: Analog keys:" << analog_keys.size();
43✔
869
    for (auto const & key: analog_keys) {
129✔
870
        QString source = QString("analog:%1").arg(QString::fromStdString(key));
86✔
871
        sources << source;
86✔
872
        qDebug() << "  Added Analog:" << source;
86✔
873
    }
86✔
874

875
    qDebug() << "getAvailableDataSources: Total sources found:" << sources.size();
43✔
876

877
    return sources;
43✔
878
}
43✔
879

880
std::pair<std::optional<DataSourceVariant>, RowSelectorType>
881
TableDesignerWidget::createDataSourceVariant(QString const & data_source_string,
290✔
882
                                             std::shared_ptr<DataManagerExtension> data_manager_extension) const {
883
    std::optional<DataSourceVariant> result;
290✔
884
    RowSelectorType row_selector_type = RowSelectorType::IntervalBased;
290✔
885

886
    if (data_source_string.startsWith("TimeFrame: ")) {
290✔
887
        // TimeFrames are used with TimestampSelector for rows; no concrete data source needed
888
        row_selector_type = RowSelectorType::Timestamp;
128✔
889

890
    } else if (data_source_string.startsWith("Events: ")) {
162✔
891
        QString source_name = data_source_string.mid(8);// Remove "Events: " prefix
66✔
892
        // Event-based computers in the registry operate with interval rows
893
        row_selector_type = RowSelectorType::IntervalBased;
66✔
894

895
        if (auto event_source = data_manager_extension->getEventSource(source_name.toStdString())) {
66✔
896
            result = event_source;
66✔
897
        }
66✔
898

899
    } else if (data_source_string.startsWith("Intervals: ")) {
162✔
900
        QString source_name = data_source_string.mid(11);// Remove "Intervals: " prefix
32✔
901
        row_selector_type = RowSelectorType::IntervalBased;
32✔
902

903
        if (auto interval_source = data_manager_extension->getIntervalSource(source_name.toStdString())) {
32✔
904
            result = interval_source;
32✔
905
        }
32✔
906
    } else if (data_source_string.startsWith("analog:")) {
96✔
907
        QString source_name = data_source_string.mid(7);// Remove "analog:" prefix
64✔
908
        row_selector_type = RowSelectorType::IntervalBased;
64✔
909

910
        if (auto analog_source = data_manager_extension->getAnalogSource(source_name.toStdString())) {
64✔
911
            result = analog_source;
64✔
912
        }
64✔
913
    }
64✔
914

915
    return {result, row_selector_type};
580✔
916
}
290✔
917

918
void TableDesignerWidget::updateRowInfoLabel(QString const & selected_source) {
18✔
919
    if (selected_source.isEmpty()) {
18✔
UNCOV
920
        ui->row_info_label->setText("No row source selected");
×
UNCOV
921
        return;
×
922
    }
923

924
    // Parse the selected source to get type and name
925
    QString source_type;
18✔
926
    QString source_name;
18✔
927

928
    if (selected_source.startsWith("TimeFrame: ")) {
18✔
929
        source_type = "TimeFrame";
11✔
930
        source_name = selected_source.mid(11);// Remove "TimeFrame: " prefix
11✔
931
    } else if (selected_source.startsWith("Events: ")) {
7✔
UNCOV
932
        source_type = "Events";
×
UNCOV
933
        source_name = selected_source.mid(8);// Remove "Events: " prefix
×
934
    } else if (selected_source.startsWith("Intervals: ")) {
7✔
935
        source_type = "Intervals";
7✔
936
        source_name = selected_source.mid(11);// Remove "Intervals: " prefix
7✔
937
    }
938

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

942
    if (!_data_manager) {
18✔
UNCOV
943
        ui->row_info_label->setText(info_text);
×
UNCOV
944
        return;
×
945
    }
946

947
    auto * reg3 = _data_manager->getTableRegistry();
18✔
948
    auto data_manager_extension = reg3 ? reg3->getDataManagerExtension() : nullptr;
18✔
949
    if (!data_manager_extension) {
18✔
950
        ui->row_info_label->setText(info_text);
×
951
        return;
×
952
    }
953

954
    auto const source_name_str = source_name.toStdString();
18✔
955

956
    // Add specific information based on source type
957
    if (source_type == "TimeFrame") {
18✔
958
        auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
11✔
959
        if (timeframe) {
11✔
960
            info_text += QString(" - %1 time points").arg(timeframe->getTotalFrameCount());
11✔
961
        }
962
    } else if (source_type == "Events") {
18✔
UNCOV
963
        auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
UNCOV
964
        if (event_series) {
×
UNCOV
965
            auto events = event_series->getEventSeries();
×
UNCOV
966
            info_text += QString(" - %1 events").arg(events.size());
×
UNCOV
967
        }
×
968
    } else if (source_type == "Intervals") {
7✔
969
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
7✔
970
        if (interval_series) {
7✔
971
            auto intervals = interval_series->getDigitalIntervalSeries();
7✔
972
            info_text += QString(" - %1 intervals").arg(intervals.size());
7✔
973

974
            // Add capture range and interval setting information
975
            if (isIntervalItselfSelected()) {
7✔
UNCOV
976
                info_text += QString("\nUsing intervals as-is (no capture range)");
×
977
            } else {
978
                int capture_range = getCaptureRange();
7✔
979
                QString interval_point = isIntervalBeginningSelected() ? "beginning" : "end";
7✔
980
                info_text += QString("\nCapture range: ±%1 samples around %2 of intervals").arg(capture_range).arg(interval_point);
7✔
981
            }
7✔
982
        }
7✔
983
    }
7✔
984

985
    ui->row_info_label->setText(info_text);
18✔
986
}
18✔
987

988
std::unique_ptr<IRowSelector> TableDesignerWidget::createRowSelector(QString const & row_source) {
10✔
989
    // Parse the row source to get type and name
990
    QString source_type;
10✔
991
    QString source_name;
10✔
992

993
    if (row_source.startsWith("TimeFrame: ")) {
10✔
UNCOV
994
        source_type = "TimeFrame";
×
UNCOV
995
        source_name = row_source.mid(11);// Remove "TimeFrame: " prefix
×
996
    } else if (row_source.startsWith("Events: ")) {
10✔
UNCOV
997
        source_type = "Events";
×
UNCOV
998
        source_name = row_source.mid(8);// Remove "Events: " prefix
×
999
    } else if (row_source.startsWith("Intervals: ")) {
10✔
1000
        source_type = "Intervals";
10✔
1001
        source_name = row_source.mid(11);// Remove "Intervals: " prefix
10✔
1002
    } else {
UNCOV
1003
        qDebug() << "Unknown row source format:" << row_source;
×
1004
        return nullptr;
×
1005
    }
1006

1007
    auto const source_name_str = source_name.toStdString();
10✔
1008

1009
    try {
1010
        if (source_type == "TimeFrame") {
10✔
1011
            // Create IntervalSelector using TimeFrame
UNCOV
1012
            auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
×
UNCOV
1013
            if (!timeframe) {
×
UNCOV
1014
                qDebug() << "TimeFrame not found:" << source_name;
×
UNCOV
1015
                return nullptr;
×
1016
            }
1017

1018
            // Use timestamps to select all rows
1019
            std::vector<TimeFrameIndex> timestamps;
×
1020
            for (int64_t i = 0; i < timeframe->getTotalFrameCount(); ++i) {
×
1021
                timestamps.push_back(TimeFrameIndex(i));
×
1022
            }
UNCOV
1023
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe);
×
1024

1025
        } else if (source_type == "Events") {
10✔
1026
            // Create TimestampSelector using DigitalEventSeries
1027
            auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
1028
            if (!event_series) {
×
UNCOV
1029
                qDebug() << "DigitalEventSeries not found:" << source_name;
×
1030
                return nullptr;
×
1031
            }
1032

UNCOV
1033
            auto events = event_series->getEventSeries();
×
1034
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
×
1035
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
×
1036
            if (!timeframe_obj) {
×
1037
                qDebug() << "TimeFrame not found for events:" << timeframe_key.str();
×
UNCOV
1038
                return nullptr;
×
1039
            }
1040

1041
            // Convert events to TimeFrameIndex
1042
            std::vector<TimeFrameIndex> timestamps;
×
1043
            for (auto const & event: events) {
×
1044
                timestamps.push_back(TimeFrameIndex(static_cast<int64_t>(event)));
×
1045
            }
1046

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

1049
        } else if (source_type == "Intervals") {
10✔
1050
            // Create IntervalSelector using DigitalIntervalSeries with capture range
1051
            auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
10✔
1052
            if (!interval_series) {
10✔
UNCOV
1053
                qDebug() << "DigitalIntervalSeries not found:" << source_name;
×
1054
                return nullptr;
×
1055
            }
1056

1057
            auto intervals = interval_series->getDigitalIntervalSeries();
10✔
1058
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
10✔
1059
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
10✔
1060
            if (!timeframe_obj) {
10✔
1061
                qDebug() << "TimeFrame not found for intervals:" << timeframe_key.str();
×
UNCOV
1062
                return nullptr;
×
1063
            }
1064

1065
            // Get capture range and interval setting
1066
            int capture_range = getCaptureRange();
10✔
1067
            bool use_beginning = isIntervalBeginningSelected();
10✔
1068
            bool use_interval_itself = isIntervalItselfSelected();
10✔
1069

1070
            // Create intervals based on the selected option
1071
            std::vector<TimeFrameInterval> tf_intervals;
10✔
1072
            for (auto const & interval: intervals) {
50✔
1073
                if (use_interval_itself) {
40✔
1074
                    // Use the interval as-is
UNCOV
1075
                    tf_intervals.emplace_back(TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
×
1076
                } else {
1077
                    // Determine the reference point (beginning or end of interval)
1078
                    int64_t reference_point;
1079
                    if (use_beginning) {
40✔
1080
                        reference_point = interval.start;
40✔
1081
                    } else {
1082
                        reference_point = interval.end;
×
1083
                    }
1084

1085
                    // Create a new interval around the reference point
1086
                    int64_t start_point = reference_point - capture_range;
40✔
1087
                    int64_t end_point = reference_point + capture_range;
40✔
1088

1089
                    // Ensure bounds are within the timeframe
1090
                    start_point = std::max(start_point, int64_t(0));
40✔
1091
                    end_point = std::min(end_point, static_cast<int64_t>(timeframe_obj->getTotalFrameCount() - 1));
40✔
1092

1093
                    tf_intervals.emplace_back(TimeFrameIndex(start_point), TimeFrameIndex(end_point));
40✔
1094
                }
1095
            }
1096

1097
            return std::make_unique<IntervalSelector>(std::move(tf_intervals), timeframe_obj);
10✔
1098
        }
10✔
1099

UNCOV
1100
    } catch (std::exception const & e) {
×
UNCOV
1101
        qDebug() << "Exception creating row selector:" << e.what();
×
UNCOV
1102
        return nullptr;
×
UNCOV
1103
    }
×
1104

UNCOV
1105
    qDebug() << "Unsupported row source type:" << source_type;
×
UNCOV
1106
    return nullptr;
×
1107
}
10✔
1108

1109
bool TableDesignerWidget::addColumnToBuilder(TableViewBuilder & builder, ColumnInfo const & column_info) {
×
1110
    // Use the simplified TableRegistry method that handles all the type checking internally
UNCOV
1111
    auto * reg = _data_manager->getTableRegistry();
×
1112
    if (!reg) {
×
1113
        qDebug() << "TableRegistry not available";
×
UNCOV
1114
        return false;
×
1115
    }
1116

UNCOV
1117
    bool success = reg->addColumnToBuilder(builder, column_info);
×
1118
    if (!success) {
×
1119
        qDebug() << "Failed to add column to builder:" << QString::fromStdString(column_info.name);
×
1120
    }
1121

UNCOV
1122
    return success;
×
1123
}
1124

1125
void TableDesignerWidget::updateIntervalSettingsVisibility() {
18✔
1126
    if (!ui->interval_settings_group) {
18✔
UNCOV
1127
        return;
×
1128
    }
1129

1130
    QString selected_key = ui->row_data_source_combo->currentText();
18✔
1131
    if (selected_key.isEmpty()) {
18✔
UNCOV
1132
        ui->interval_settings_group->setVisible(false);
×
UNCOV
1133
        if (ui->capture_range_spinbox) {
×
1134
            ui->capture_range_spinbox->setEnabled(false);
×
1135
        }
UNCOV
1136
        return;
×
1137
    }
1138

1139
    if (!_data_manager) {
18✔
1140
        ui->interval_settings_group->setVisible(false);
×
1141
        if (ui->capture_range_spinbox) {
×
UNCOV
1142
            ui->capture_range_spinbox->setEnabled(false);
×
1143
        }
UNCOV
1144
        return;
×
1145
    }
1146

1147
    // Check if the selected source is an interval series
1148
    if (selected_key.startsWith("Intervals: ")) {
18✔
1149
        ui->interval_settings_group->setVisible(true);
7✔
1150

1151
        // Enable/disable capture range based on interval setting
1152
        if (ui->capture_range_spinbox) {
7✔
1153
            bool use_interval_itself = isIntervalItselfSelected();
7✔
1154
            ui->capture_range_spinbox->setEnabled(!use_interval_itself);
7✔
1155
        }
1156
    } else {
1157
        ui->interval_settings_group->setVisible(false);
11✔
1158
        if (ui->capture_range_spinbox) {
11✔
1159
            ui->capture_range_spinbox->setEnabled(false);
11✔
1160
        }
1161
    }
1162
}
18✔
1163

1164
int TableDesignerWidget::getCaptureRange() const {
17✔
1165
    if (ui->capture_range_spinbox) {
17✔
1166
        return ui->capture_range_spinbox->value();
17✔
1167
    }
UNCOV
1168
    return 30000;// Default value
×
1169
}
1170

1171
void TableDesignerWidget::setCaptureRange(int value) {
21✔
1172
    if (ui->capture_range_spinbox) {
21✔
1173
        ui->capture_range_spinbox->blockSignals(true);
21✔
1174
        ui->capture_range_spinbox->setValue(value);
21✔
1175
        ui->capture_range_spinbox->blockSignals(false);
21✔
1176
    }
1177
}
21✔
1178

1179
bool TableDesignerWidget::isIntervalBeginningSelected() const {
17✔
1180
    if (ui->interval_beginning_radio) {
17✔
1181
        return ui->interval_beginning_radio->isChecked();
17✔
1182
    }
UNCOV
1183
    return true;// Default to beginning
×
1184
}
1185

1186
bool TableDesignerWidget::isIntervalItselfSelected() const {
24✔
1187
    if (ui->interval_itself_radio) {
24✔
1188
        return ui->interval_itself_radio->isChecked();
24✔
1189
    }
1190
    return false;// Default to not selected
×
1191
}
1192

1193
void TableDesignerWidget::triggerPreviewDebounced() {
67✔
1194
    if (_preview_debounce_timer) _preview_debounce_timer->start();
67✔
1195
    // Also trigger an immediate rebuild to support non-interactive/test contexts
1196
    rebuildPreviewNow();
67✔
1197
}
67✔
1198

1199
void TableDesignerWidget::rebuildPreviewNow() {
67✔
1200
    if (!_data_manager || !_table_viewer) return;
67✔
1201
    if (_current_table_id.isEmpty()) {
67✔
1202
        _table_viewer->clearTable();
40✔
1203
        return;
40✔
1204
    }
1205

1206
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
27✔
1207
    if (row_source.isEmpty()) {
27✔
1208
        _table_viewer->clearTable();
6✔
1209
        return;
6✔
1210
    }
1211

1212
    // Get enabled column infos from the computers tree
1213
    auto column_infos = getEnabledColumnInfos();
21✔
1214
    if (column_infos.empty()) {
21✔
1215
        _table_viewer->clearTable();
12✔
1216
        return;
12✔
1217
    }
1218

1219
    // Create row selector for the entire dataset
1220
    auto selector = createRowSelector(row_source);
9✔
1221
    if (!selector) {
9✔
UNCOV
1222
        _table_viewer->clearTable();
×
UNCOV
1223
        return;
×
1224
    }
1225

1226
    // Apply any saved column order for this table id
1227
    auto desiredOrder = _table_column_order.value(_current_table_id);
9✔
1228
    if (!desiredOrder.isEmpty()) {
9✔
1229
        std::vector<ColumnInfo> reordered;
6✔
1230
        reordered.reserve(column_infos.size());
6✔
1231
        for (auto const & name : desiredOrder) {
17✔
1232
            auto it = std::find_if(column_infos.begin(), column_infos.end(), [&](ColumnInfo const & ci){ return QString::fromStdString(ci.name) == name; });
29✔
1233
            if (it != column_infos.end()) {
11✔
1234
                reordered.push_back(*it);
11✔
1235
            }
1236
        }
1237
        for (auto const & ci : column_infos) {
21✔
1238
            if (std::find_if(reordered.begin(), reordered.end(), [&](ColumnInfo const & x){ return x.name == ci.name; }) == reordered.end()) {
38✔
1239
                reordered.push_back(ci);
4✔
1240
            }
1241
        }
1242
        column_infos = std::move(reordered);
6✔
1243
    }
6✔
1244

1245
    // Set up the table viewer with pagination
1246
    _table_viewer->setTableConfiguration(
45✔
1247
            std::move(selector),
9✔
1248
            std::move(column_infos),
9✔
1249
            _data_manager,
9✔
1250
            QString("Preview: %1").arg(_current_table_id));
18✔
1251

1252
    // Capture the current visual order from the viewer
1253
    QStringList currentOrder;
9✔
1254
    if (_table_viewer) {
9✔
1255
        auto * tv = _table_viewer->findChild<QTableView*>();
9✔
1256
        if (tv && tv->model()) {
9✔
1257
            auto * header = tv->horizontalHeader();
9✔
1258
            int cols = tv->model()->columnCount();
9✔
1259
            for (int v = 0; header && v < cols; ++v) {
27✔
1260
                int logical = header->logicalIndex(v);
18✔
1261
                auto name = tv->model()->headerData(logical, Qt::Horizontal, Qt::DisplayRole).toString();
18✔
1262
                currentOrder.push_back(name);
18✔
1263
            }
18✔
1264
        }
1265
    }
1266
    if (!currentOrder.isEmpty()) {
9✔
1267
        _table_column_order[_current_table_id] = currentOrder;
9✔
1268
    }
1269
}
39✔
1270

1271
void TableDesignerWidget::refreshComputersTree() {
32✔
1272
    if (!_data_manager) return;
32✔
1273

1274
    _updating_computers_tree = true;
32✔
1275

1276
    // Preserve previous checkbox states and custom column names
1277
    std::map<std::string, std::pair<Qt::CheckState, QString>> previous_states;
32✔
1278
    if (ui->computers_tree && ui->computers_tree->topLevelItemCount() > 0) {
32✔
1279
        for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
202✔
1280
            auto * data_source_item_old = ui->computers_tree->topLevelItem(i);
182✔
1281
            for (int j = 0; j < data_source_item_old->childCount(); ++j) {
768✔
1282
                auto * computer_item_old = data_source_item_old->child(j);
586✔
1283
                QString ds = computer_item_old->data(0, Qt::UserRole).toString();
586✔
1284
                QString cn = computer_item_old->data(1, Qt::UserRole).toString();
586✔
1285
                std::string key = (ds + "||" + cn).toStdString();
586✔
1286
                previous_states[key] = {computer_item_old->checkState(1), computer_item_old->text(2)};
586✔
1287
            }
586✔
1288
        }
1289
    }
1290

1291
    ui->computers_tree->clear();
32✔
1292
    ui->computers_tree->setHeaderLabels({"Data Source / Computer", "Enabled", "Column Name"});
160✔
1293

1294
    auto * registry = _data_manager->getTableRegistry();
32✔
1295
    if (!registry) {
32✔
UNCOV
1296
        _updating_computers_tree = false;
×
UNCOV
1297
        return;
×
1298
    }
1299

1300
    auto data_manager_extension = registry->getDataManagerExtension();
32✔
1301
    if (!data_manager_extension) {
32✔
UNCOV
1302
        _updating_computers_tree = false;
×
1303
        return;
×
1304
    }
1305

1306
    auto & computer_registry = registry->getComputerRegistry();
32✔
1307

1308
    // Get available data sources
1309
    auto data_sources = getAvailableDataSources();
32✔
1310

1311
    // Create tree structure: Data Source -> Computers
1312
    for (QString const & data_source: data_sources) {
322✔
1313
        auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
290✔
1314
        data_source_item->setText(0, data_source);
290✔
1315
        data_source_item->setFlags(Qt::ItemIsEnabled);
290✔
1316
        data_source_item->setExpanded(false);// Start collapsed
290✔
1317

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

1321
        if (!data_source_variant.has_value()) {
290✔
1322
            qDebug() << "Failed to create data source variant for:" << data_source;
128✔
1323
            continue;
128✔
1324
        }
1325

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

1329
        // Add compatible computers as children
1330
        for (auto const & computer_info: available_computers) {
1,096✔
1331
            auto * computer_item = new QTreeWidgetItem(data_source_item);
934✔
1332
            computer_item->setText(0, QString::fromStdString(computer_info.name));
934✔
1333
            computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
934✔
1334
            computer_item->setCheckState(1, Qt::Unchecked);
934✔
1335

1336
            // Generate default column name
1337
            QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
934✔
1338
            computer_item->setText(2, default_name);
934✔
1339
            computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
934✔
1340

1341
            // Store data source and computer name for later use
1342
            computer_item->setData(0, Qt::UserRole, data_source);
934✔
1343
            computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
934✔
1344

1345
            // Restore previous state if present
1346
            std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
934✔
1347
            auto it_prev = previous_states.find(prev_key);
934✔
1348
            if (it_prev != previous_states.end()) {
934✔
1349
                computer_item->setCheckState(1, it_prev->second.first);
583✔
1350
                if (!it_prev->second.second.isEmpty()) {
583✔
1351
                    computer_item->setText(2, it_prev->second.second);
583✔
1352
                }
1353
            }
1354
        }
934✔
1355
    }
290✔
1356

1357
    // Resize columns to content
1358
    ui->computers_tree->resizeColumnToContents(0);
32✔
1359
    ui->computers_tree->resizeColumnToContents(1);
32✔
1360
    ui->computers_tree->resizeColumnToContents(2);
32✔
1361

1362
    _updating_computers_tree = false;
32✔
1363

1364
    // Update preview after refresh
1365
    triggerPreviewDebounced();
32✔
1366
}
64✔
1367

1368
void TableDesignerWidget::onComputersTreeItemChanged() {
7,138✔
1369
    if (_updating_computers_tree) return;
7,138✔
1370

1371
    // Trigger preview update when checkbox states change
1372
    triggerPreviewDebounced();
13✔
1373
}
1374

1375
void TableDesignerWidget::onComputersTreeItemEdited(QTreeWidgetItem * item, int column) {
7,138✔
1376
    if (_updating_computers_tree) return;
7,138✔
1377

1378
    // Only respond to column name edits (column 2)
1379
    if (column == 2) {
13✔
1380
        // Column name was edited, trigger preview update
1381
        triggerPreviewDebounced();
1✔
1382
    }
1383
}
1384

1385
std::vector<ColumnInfo> TableDesignerWidget::getEnabledColumnInfos() const {
26✔
1386
    std::vector<ColumnInfo> column_infos;
26✔
1387

1388
    if (!ui->computers_tree) return column_infos;
26✔
1389

1390
    // Iterate through all data source items
1391
    for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
260✔
1392
        auto * data_source_item = ui->computers_tree->topLevelItem(i);
234✔
1393

1394
        // Iterate through computer items under each data source
1395
        for (int j = 0; j < data_source_item->childCount(); ++j) {
988✔
1396
            auto * computer_item = data_source_item->child(j);
754✔
1397

1398
            // Check if this computer is enabled
1399
            if (computer_item->checkState(1) == Qt::Checked) {
754✔
1400
                QString data_source = computer_item->data(0, Qt::UserRole).toString();
28✔
1401
                QString computer_name = computer_item->data(1, Qt::UserRole).toString();
28✔
1402
                QString column_name = computer_item->text(2);
28✔
1403

1404
                if (column_name.isEmpty()) {
28✔
UNCOV
1405
                    column_name = generateDefaultColumnName(data_source, computer_name);
×
1406
                }
1407

1408
                // Create ColumnInfo (use raw key without UI prefixes)
1409
                QString source_key = data_source;
28✔
1410
                if (source_key.startsWith("Events: ")) {
28✔
1411
                    source_key = source_key.mid(8);
28✔
1412
                } else if (source_key.startsWith("Intervals: ")) {
×
UNCOV
1413
                    source_key = source_key.mid(11);
×
UNCOV
1414
                } else if (source_key.startsWith("analog:")) {
×
UNCOV
1415
                    source_key = source_key.mid(7);
×
UNCOV
1416
                } else if (source_key.startsWith("TimeFrame: ")) {
×
UNCOV
1417
                    source_key = source_key.mid(11);
×
1418
                }
1419

1420
                ColumnInfo info(column_name.toStdString(),
84✔
1421
                                QString("Column from %1 using %2").arg(data_source, computer_name).toStdString(),
56✔
1422
                                source_key.toStdString(),
56✔
1423
                                computer_name.toStdString());
140✔
1424

1425
                // Set output type based on computer info
1426
                if (auto * registry = _data_manager->getTableRegistry()) {
28✔
1427
                    auto & computer_registry = registry->getComputerRegistry();
28✔
1428
                    auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
28✔
1429
                    if (computer_info) {
28✔
1430
                        info.outputType = computer_info->outputType;
28✔
1431
                        info.outputTypeName = computer_info->outputTypeName;
28✔
1432
                        info.isVectorType = computer_info->isVectorType;
28✔
1433
                        if (info.isVectorType) {
28✔
1434
                            info.elementType = computer_info->elementType;
5✔
1435
                            info.elementTypeName = computer_info->elementTypeName;
5✔
1436
                        }
1437
                    }
1438
                }
1439

1440
                column_infos.push_back(std::move(info));
28✔
1441
            }
28✔
1442
        }
1443
    }
1444

1445
    return column_infos;
26✔
UNCOV
1446
}
×
1447

UNCOV
1448
bool TableDesignerWidget::isComputerCompatibleWithDataSource(std::string const & computer_name, QString const & data_source) const {
×
UNCOV
1449
    if (!_data_manager) return false;
×
1450

UNCOV
1451
    auto * registry = _data_manager->getTableRegistry();
×
UNCOV
1452
    if (!registry) return false;
×
1453

UNCOV
1454
    auto & computer_registry = registry->getComputerRegistry();
×
1455
    auto computer_info = computer_registry.findComputerInfo(computer_name);
×
1456
    if (!computer_info) return false;
×
1457

1458
    // Basic compatibility check based on data source type and common computer patterns
1459
    if (data_source.startsWith("Events: ")) {
×
1460
        // Event-based computers typically have "Event" in their name
1461
        return computer_name.find("Event") != std::string::npos;
×
1462
    } else if (data_source.startsWith("Intervals: ")) {
×
1463
        // Interval-based computers typically work with intervals or events
UNCOV
1464
        return computer_name.find("Event") != std::string::npos ||
×
UNCOV
1465
               computer_name.find("Interval") != std::string::npos;
×
1466
    } else if (data_source.startsWith("analog:")) {
×
1467
        // Analog-based computers typically have "Analog" in their name
1468
        return computer_name.find("Analog") != std::string::npos;
×
1469
    } else if (data_source.startsWith("TimeFrame: ")) {
×
1470
        // TimeFrame-based computers - generally most computers can work with timestamps
1471
        return computer_name.find("Timestamp") != std::string::npos ||
×
1472
               computer_name.find("Time") != std::string::npos;
×
1473
    }
1474

1475
    // Default: assume compatibility for unrecognized patterns
1476
    return true;
×
1477
}
1478

1479
QString TableDesignerWidget::generateDefaultColumnName(QString const & data_source, QString const & computer_name) const {
934✔
1480
    QString source_name = data_source;
934✔
1481

1482
    // Extract the actual name from prefixed data sources
1483
    if (source_name.startsWith("Events: ")) {
934✔
1484
        source_name = source_name.mid(8);
198✔
1485
    } else if (source_name.startsWith("Intervals: ")) {
736✔
1486
        source_name = source_name.mid(11);
224✔
1487
    } else if (source_name.startsWith("analog:")) {
512✔
1488
        source_name = source_name.mid(7);
512✔
UNCOV
1489
    } else if (source_name.startsWith("TimeFrame: ")) {
×
UNCOV
1490
        source_name = source_name.mid(11);
×
1491
    }
1492

1493
    // Create a concise name
1494
    return QString("%1_%2").arg(source_name, computer_name);
2,802✔
1495
}
934✔
1496

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