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

paulmthompson / WhiskerToolbox / 17556919945

08 Sep 2025 04:04PM UTC coverage: 71.521% (-0.001%) from 71.522%
17556919945

push

github

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

36445 of 50957 relevant lines covered (71.52%)

1282.87 hits per line

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

59.56
/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
    // Replace existing Table Information contents in UI with the Section
79
    if (ui->table_info_layout) {
9✔
80
        QLayoutItem * child;
81
        while ((child = ui->table_info_layout->takeAt(0)) != nullptr) {
18✔
82
            if (child->widget()) child->widget()->setParent(nullptr);
×
83
            delete child;
×
84
        }
85
        ui->table_info_layout->addWidget(_table_info_section);
9✔
86
    }
87
    // Hook save from table info widget
88
    connect(_table_info_widget, &TableInfoWidget::saveClicked, this, &TableDesignerWidget::onSaveTableInfo);
9✔
89

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

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

104
    // Add observer to automatically refresh dropdowns when DataManager changes
105
    if (_data_manager) {
9✔
106
        _data_manager->addObserver([this]() {
9✔
107
            refreshAllDataSources();
2✔
108
        });
2✔
109
    }
110

111
    qDebug() << "TableDesignerWidget initialized with TableViewerWidget for efficient pagination";
9✔
112
}
9✔
113

114
TableDesignerWidget::~TableDesignerWidget() {
9✔
115
    delete ui;
9✔
116
}
9✔
117

118
void TableDesignerWidget::refreshAllDataSources() {
2✔
119
    qDebug() << "Manually refreshing all data sources...";
2✔
120
    refreshRowDataSourceCombo();
2✔
121
    refreshComputersTree();
2✔
122

123
    // If we have a selected table, refresh its info
124
    if (!_current_table_id.isEmpty()) {
2✔
125
        loadTableInfo(_current_table_id);
×
126
    }
127
}
2✔
128

129

130
void TableDesignerWidget::connectSignals() {
9✔
131
    // Table selection signals
132
    connect(ui->table_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
27✔
133
            this, &TableDesignerWidget::onTableSelectionChanged);
18✔
134
    connect(ui->new_table_btn, &QPushButton::clicked,
27✔
135
            this, &TableDesignerWidget::onCreateNewTable);
18✔
136
    connect(ui->delete_table_btn, &QPushButton::clicked,
27✔
137
            this, &TableDesignerWidget::onDeleteTable);
18✔
138

139
    // Table info signals are connected via TableInfoWidget
140

141
    // Row source signals
142
    connect(ui->row_data_source_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
27✔
143
            this, &TableDesignerWidget::onRowDataSourceChanged);
18✔
144
    connect(ui->capture_range_spinbox, QOverload<int>::of(&QSpinBox::valueChanged),
27✔
145
            this, &TableDesignerWidget::onCaptureRangeChanged);
18✔
146
    connect(ui->interval_beginning_radio, &QRadioButton::toggled,
27✔
147
            this, &TableDesignerWidget::onIntervalSettingChanged);
18✔
148
    connect(ui->interval_end_radio, &QRadioButton::toggled,
27✔
149
            this, &TableDesignerWidget::onIntervalSettingChanged);
18✔
150
    connect(ui->interval_itself_radio, &QRadioButton::toggled,
27✔
151
            this, &TableDesignerWidget::onIntervalSettingChanged);
18✔
152

153
    // Column design signals (tree-based)
154
    connect(ui->computers_tree, &QTreeWidget::itemChanged,
27✔
155
            this, &TableDesignerWidget::onComputersTreeItemChanged);
18✔
156
    connect(ui->computers_tree, &QTreeWidget::itemChanged,
27✔
157
            this, &TableDesignerWidget::onComputersTreeItemEdited);
18✔
158

159
    // Build signals
160
    connect(ui->build_table_btn, &QPushButton::clicked,
27✔
161
            this, &TableDesignerWidget::onBuildTable);
18✔
162
    if (ui->apply_transform_btn) {
9✔
163
        connect(ui->apply_transform_btn, &QPushButton::clicked,
27✔
164
                this, &TableDesignerWidget::onApplyTransform);
18✔
165
    }
166
    if (ui->export_csv_btn) {
9✔
167
        connect(ui->export_csv_btn, &QPushButton::clicked,
27✔
168
                this, &TableDesignerWidget::onExportCsv);
18✔
169
    }
170

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

193
void TableDesignerWidget::onTableSelectionChanged() {
15✔
194
    int current_index = ui->table_combo->currentIndex();
15✔
195
    if (current_index < 0) {
15✔
196
        clearUI();
3✔
197
        return;
3✔
198
    }
199

200
    QString table_id = ui->table_combo->itemData(current_index).toString();
12✔
201
    if (table_id.isEmpty()) {
12✔
202
        clearUI();
9✔
203
        return;
9✔
204
    }
205

206
    _current_table_id = table_id;
3✔
207
    loadTableInfo(table_id);
3✔
208

209
    // Enable/disable controls
210
    ui->delete_table_btn->setEnabled(true);
3✔
211
    // Table info section is controlled separately
212
    ui->build_table_btn->setEnabled(true);
3✔
213
    if (auto gb = this->findChild<QGroupBox*>("row_source_group")) gb->setEnabled(true);
6✔
214
    if (auto gb = this->findChild<QGroupBox*>("column_design_group")) gb->setEnabled(true);
6✔
215
    // Enable save info within TableInfoWidget
216
    if (_table_info_section) _table_info_section->setEnabled(true);
3✔
217

218
    updateBuildStatus("Table selected: " + table_id);
3✔
219

220
    qDebug() << "Selected table:" << table_id;
3✔
221
}
12✔
222

223
void TableDesignerWidget::onCreateNewTable() {
×
224
    bool ok;
×
225
    QString name = QInputDialog::getText(this, "New Table", "Enter table name:", QLineEdit::Normal, "New Table", &ok);
×
226

227
    if (!ok || name.isEmpty()) {
×
228
        return;
×
229
    }
230

231
    auto * registry = _data_manager->getTableRegistry();
×
232
    if (!registry) { return; }
×
233
    auto table_id = registry->generateUniqueTableId("Table");
×
234

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

249
void TableDesignerWidget::onDeleteTable() {
×
250
    if (_current_table_id.isEmpty()) {
×
251
        return;
×
252
    }
253

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

258
    if (reply == QMessageBox::Yes) {
×
259
        auto * registry = _data_manager->getTableRegistry();
×
260
        if (registry && registry->removeTable(_current_table_id.toStdString())) {
×
261
            // The combo will be refreshed by the signal handler
262
            clearUI();
×
263
        } else {
264
            QMessageBox::warning(this, "Error", "Failed to delete table: " + _current_table_id);
×
265
        }
266
    }
267
}
268

269
void TableDesignerWidget::onRowDataSourceChanged() {
19✔
270
    QString selected = ui->row_data_source_combo->currentText();
19✔
271
    if (selected.isEmpty()) {
19✔
272
        ui->row_info_label->setText("No row source selected");
5✔
273
        return;
5✔
274
    }
275

276
    // Save the row source selection to the current table
277
    // Only save if we have a current table and we're not loading table info
278
    if (!_current_table_id.isEmpty() && _data_manager) {
14✔
279
        if (auto * reg = _data_manager->getTableRegistry()) {
3✔
280
            reg->updateTableRowSource(_current_table_id.toStdString(), selected.toStdString());
3✔
281
        }
282
    }
283

284
    // Update the info label
285
    updateRowInfoLabel(selected);
14✔
286

287
    // Update interval settings visibility
288
    updateIntervalSettingsVisibility();
14✔
289

290
    // Refresh computers tree since available computers depend on row selector type
291
    refreshComputersTree();
14✔
292

293
    qDebug() << "Row data source changed to:" << selected;
14✔
294
    triggerPreviewDebounced();
14✔
295
}
19✔
296

297
void TableDesignerWidget::onCaptureRangeChanged() {
×
298
    // Update the info label to reflect the new capture range
299
    QString selected = ui->row_data_source_combo->currentText();
×
300
    if (!selected.isEmpty()) {
×
301
        updateRowInfoLabel(selected);
×
302
    }
303
    triggerPreviewDebounced();
×
304
}
×
305

306
void TableDesignerWidget::onIntervalSettingChanged() {
×
307
    // Update the info label to reflect the new interval setting
308
    QString selected = ui->row_data_source_combo->currentText();
×
309
    if (!selected.isEmpty()) {
×
310
        updateRowInfoLabel(selected);
×
311
    }
312

313
    // Update capture range visibility based on interval setting
314
    updateIntervalSettingsVisibility();
×
315
    triggerPreviewDebounced();
×
316
}
×
317

318

319
void TableDesignerWidget::onBuildTable() {
×
320
    if (_current_table_id.isEmpty()) {
×
321
        updateBuildStatus("No table selected", true);
×
322
        return;
×
323
    }
324

325
    QString row_source = ui->row_data_source_combo->currentText();
×
326
    if (row_source.isEmpty()) {
×
327
        updateBuildStatus("No row data source selected", true);
×
328
        return;
×
329
    }
330

331
    // Get enabled column infos from the tree
332
    auto column_infos = getEnabledColumnInfos();
×
333
    if (column_infos.empty()) {
×
334
        updateBuildStatus("No computers enabled. Check boxes in the tree to enable computers.", true);
×
335
        return;
×
336
    }
337

338
    try {
339
        // Create the row selector
340
        auto row_selector = createRowSelector(row_source);
×
341
        if (!row_selector) {
×
342
            updateBuildStatus("Failed to create row selector", true);
×
343
            return;
×
344
        }
345

346
        // Get the data manager extension
347
        auto * reg = _data_manager->getTableRegistry();
×
348
        auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
×
349
        if (!data_manager_extension) {
×
350
            updateBuildStatus("DataManager extension not available", true);
×
351
            return;
×
352
        }
353

354
        // Create the TableViewBuilder
355
        TableViewBuilder builder(data_manager_extension);
×
356
        builder.setRowSelector(std::move(row_selector));
×
357

358
        // Add all enabled columns from the tree
359
        bool all_columns_valid = true;
×
360
        for (auto const & column_info: column_infos) {
×
361
            if (!reg->addColumnToBuilder(builder, column_info)) {
×
362
                updateBuildStatus(QString("Failed to create column: %1").arg(QString::fromStdString(column_info.name)), true);
×
363
                all_columns_valid = false;
×
364
                break;
×
365
            }
366
        }
367

368
        if (!all_columns_valid) {
×
369
            return;
×
370
        }
371

372
        // Build the table
373
        auto table_view = builder.build();
×
374

375
        // Store the built table in the TableManager and update table info with current columns
376
        if (reg) {
×
377
            // Update table info with current column configuration
378
            auto table_info = reg->getTableInfo(_current_table_id.toStdString());
×
379
            table_info.columns = column_infos;// Store the current enabled columns
×
380
            reg->updateTableInfo(_current_table_id.toStdString(),
×
381
                                 table_info.name, table_info.description);
382

383
            // Store the built table
384
            if (reg->storeBuiltTable(_current_table_id.toStdString(), std::move(table_view))) {
×
385
                updateBuildStatus(QString("Table built successfully with %1 columns!").arg(column_infos.size()));
×
386
                qDebug() << "Successfully built table:" << _current_table_id << "with" << column_infos.size() << "columns";
×
387
            } else {
388
                updateBuildStatus("Failed to store built table", true);
×
389
            }
390
        } else {
×
391
            updateBuildStatus("Registry unavailable", true);
×
392
        }
393

394
    } catch (std::exception const & e) {
×
395
        updateBuildStatus(QString("Error building table: %1").arg(e.what()), true);
×
396
        qDebug() << "Exception during table building:" << e.what();
×
397
    }
×
398
}
×
399

400
bool TableDesignerWidget::buildTableFromTree() {
1✔
401
    // This is essentially the same as onBuildTable but returns success status
402
    if (_current_table_id.isEmpty()) {
1✔
403
        updateBuildStatus("No table selected", true);
×
404
        return false;
×
405
    }
406

407
    QString row_source = ui->row_data_source_combo->currentText();
1✔
408
    if (row_source.isEmpty()) {
1✔
409
        updateBuildStatus("No row data source selected", true);
×
410
        return false;
×
411
    }
412

413
    // Get enabled column infos from the tree
414
    auto column_infos = getEnabledColumnInfos();
1✔
415
    if (column_infos.empty()) {
1✔
416
        updateBuildStatus("No computers enabled. Check boxes in the tree to enable computers.", true);
×
417
        return false;
×
418
    }
419

420
    try {
421
        // Create the row selector
422
        auto row_selector = createRowSelector(row_source);
1✔
423
        if (!row_selector) {
1✔
424
            updateBuildStatus("Failed to create row selector", true);
×
425
            return false;
×
426
        }
427

428
        // Get the data manager extension
429
        auto * reg = _data_manager->getTableRegistry();
1✔
430
        auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
1✔
431
        if (!data_manager_extension) {
1✔
432
            updateBuildStatus("DataManager extension not available", true);
×
433
            return false;
×
434
        }
435

436
        // Create the TableViewBuilder
437
        TableViewBuilder builder(data_manager_extension);
1✔
438
        builder.setRowSelector(std::move(row_selector));
1✔
439

440
        // Add all enabled columns from the tree
441
        bool all_columns_valid = true;
1✔
442
        for (auto const & column_info: column_infos) {
4✔
443
            if (!reg->addColumnToBuilder(builder, column_info)) {
3✔
444
                updateBuildStatus(QString("Failed to create column: %1").arg(QString::fromStdString(column_info.name)), true);
×
445
                all_columns_valid = false;
×
446
                break;
×
447
            }
448
        }
449

450
        if (!all_columns_valid) {
1✔
451
            return false;
×
452
        }
453

454
        // Build the table
455
        auto table_view = builder.build();
1✔
456

457
        // Store the built table in the TableManager and update table info with current columns
458
        if (reg) {
1✔
459
            // Update table info with current column configuration
460
            auto table_info = reg->getTableInfo(_current_table_id.toStdString());
1✔
461
            table_info.columns = column_infos;// Store the current enabled columns
1✔
462
            reg->updateTableInfo(_current_table_id.toStdString(),
1✔
463
                                 table_info.name,
464
                                 table_info.description);
465

466
            // Store the built table
467
            if (reg->storeBuiltTable(_current_table_id.toStdString(), std::move(table_view))) {
1✔
468
                updateBuildStatus(QString("Table built successfully with %1 columns!").arg(column_infos.size()));
1✔
469
                qDebug() << "Successfully built table:" << _current_table_id << "with" << column_infos.size() << "columns";
1✔
470
                return true;
1✔
471
            } else {
472
                updateBuildStatus("Failed to store built table", true);
×
473
                return false;
×
474
            }
475
        } else {
1✔
476
            updateBuildStatus("Registry unavailable", true);
×
477
            return false;
×
478
        }
479

480
    } catch (std::exception const & e) {
1✔
481
        updateBuildStatus(QString("Error building table: %1").arg(e.what()), true);
×
482
        qDebug() << "Exception during table building:" << e.what();
×
483
        return false;
×
484
    }
×
485
}
1✔
486

487
void TableDesignerWidget::onApplyTransform() {
×
488
    if (_current_table_id.isEmpty() || !_data_manager) {
×
489
        updateBuildStatus("No base table selected", true);
×
490
        return;
×
491
    }
492

493
    // Fetch the built base table
494
    auto * reg = _data_manager->getTableRegistry();
×
495
    if (!reg) {
×
496
        updateBuildStatus("Registry unavailable", true);
×
497
        return;
×
498
    }
499
    auto base_view = reg->getBuiltTable(_current_table_id.toStdString());
×
500
    if (!base_view) {
×
501
        updateBuildStatus("Build the base table first", true);
×
502
        return;
×
503
    }
504

505
    // Currently only PCA option is exposed
506
    QString transform = ui->transform_type_combo ? ui->transform_type_combo->currentText() : QString();
×
507
    if (transform != "PCA") {
×
508
        updateBuildStatus("Unsupported transform", true);
×
509
        return;
×
510
    }
511

512
    // Configure PCA
513
    PCAConfig cfg;
×
514
    cfg.center = ui->transform_center_checkbox && ui->transform_center_checkbox->isChecked();
×
515
    cfg.standardize = ui->transform_standardize_checkbox && ui->transform_standardize_checkbox->isChecked();
×
516
    if (ui->transform_include_edit) {
×
517
        for (auto const & s: parseCommaSeparatedList(ui->transform_include_edit->text())) cfg.include.push_back(s);
×
518
    }
519
    if (ui->transform_exclude_edit) {
×
520
        for (auto const & s: parseCommaSeparatedList(ui->transform_exclude_edit->text())) cfg.exclude.push_back(s);
×
521
    }
522

523
    try {
524
        PCATransform pca(cfg);
×
525
        TableView derived = pca.apply(*base_view);
×
526

527
        // Determine output id/name
528
        QString out_name = ui->transform_output_name_edit ? ui->transform_output_name_edit->text().trimmed()
×
529
                                                          : QString();
×
530
        if (out_name.isEmpty()) {
×
531
            QString base = _table_info_widget ? _table_info_widget->getName() : QString();
×
532
            out_name = base.isEmpty() ? QString("(PCA)") : QString("%1 (PCA)").arg(base);
×
533
        }
×
534

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

550
std::vector<std::string> TableDesignerWidget::parseCommaSeparatedList(QString const & text) const {
×
551
    std::vector<std::string> out;
×
552
    for (QString s: text.split(",", Qt::SkipEmptyParts)) {
×
553
        s = s.trimmed();
×
554
        if (!s.isEmpty()) out.push_back(s.toStdString());
×
555
    }
×
556
    return out;
×
557
}
×
558

559
void TableDesignerWidget::onExportCsv() {
×
560
    if (_current_table_id.isEmpty() || !_data_manager) {
×
561
        updateBuildStatus("No table selected", true);
×
562
        return;
×
563
    }
564

565
    auto * reg = _data_manager->getTableRegistry();
×
566
    if (!reg) {
×
567
        updateBuildStatus("Registry unavailable", true);
×
568
        return;
×
569
    }
570
    auto view = reg->getBuiltTable(_current_table_id.toStdString());
×
571
    if (!view) {
×
572
        updateBuildStatus("Build the table first", true);
×
573
        return;
×
574
    }
575

576
    QString filename = promptSaveCsvFilename();
×
577
    if (filename.isEmpty()) return;
×
578
    if (!filename.endsWith(".csv", Qt::CaseInsensitive)) filename += ".csv";
×
579

580
    // CSV options
581
    QString delimiter = ui->export_delimiter_combo ? ui->export_delimiter_combo->currentText() : "Comma";
×
582
    QString lineEnding = ui->export_line_ending_combo ? ui->export_line_ending_combo->currentText() : "LF (\\n)";
×
583
    int precision = ui->export_precision_spinbox ? ui->export_precision_spinbox->value() : 3;
×
584
    bool includeHeader = ui->export_header_checkbox && ui->export_header_checkbox->isChecked();
×
585

586
    std::string delim = ",";
×
587
    if (delimiter == "Space") delim = " ";
×
588
    else if (delimiter == "Tab")
×
589
        delim = "\t";
×
590
    std::string eol = "\n";
×
591
    if (lineEnding.startsWith("CRLF")) eol = "\r\n";
×
592

593
    try {
594
        std::ofstream file(filename.toStdString());
×
595
        if (!file.is_open()) {
×
596
            updateBuildStatus("Could not open file for writing", true);
×
597
            return;
×
598
        }
599
        file << std::fixed << std::setprecision(precision);
×
600

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

631
QString TableDesignerWidget::promptSaveCsvFilename() const {
×
632
    return QFileDialog::getSaveFileName(const_cast<TableDesignerWidget *>(this), "Export Table to CSV", QString(), "CSV Files (*.csv)");
×
633
}
634

635
void TableDesignerWidget::onSaveTableInfo() {
×
636
    if (_current_table_id.isEmpty()) {
×
637
        return;
×
638
    }
639

640
    QString name = _table_info_widget ? _table_info_widget->getName() : QString();
×
641
    QString description = _table_info_widget ? _table_info_widget->getDescription() : QString();
×
642

643
    if (name.isEmpty()) {
×
644
        QMessageBox::warning(this, "Error", "Table name cannot be empty");
×
645
        return;
×
646
    }
647

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

664
void TableDesignerWidget::onTableManagerTableCreated(QString const & table_id) {
3✔
665
    refreshTableCombo();
3✔
666
    qDebug() << "Table created signal received:" << table_id;
3✔
667
}
3✔
668

669
void TableDesignerWidget::onTableManagerTableRemoved(QString const & table_id) {
×
670
    refreshTableCombo();
×
671
    if (_current_table_id == table_id) {
×
672
        _current_table_id.clear();
×
673
        clearUI();
×
674
    }
675
    qDebug() << "Table removed signal received:" << table_id;
×
676
}
×
677

678
void TableDesignerWidget::onTableManagerTableInfoUpdated(QString const & table_id) {
4✔
679
    if (_current_table_id == table_id && !_loading_column_configuration) {
4✔
680
        loadTableInfo(table_id);
4✔
681
    }
682
    qDebug() << "Table info updated signal received:" << table_id;
4✔
683
}
4✔
684

685
void TableDesignerWidget::refreshTableCombo() {
12✔
686
    ui->table_combo->clear();
12✔
687

688
    auto * reg = _data_manager->getTableRegistry();
12✔
689
    auto table_infos = reg ? reg->getAllTableInfo() : std::vector<TableInfo>{};
12✔
690
    for (auto const & info: table_infos) {
15✔
691
        ui->table_combo->addItem(QString::fromStdString(info.name), QString::fromStdString(info.id));
3✔
692
    }
693

694
    if (ui->table_combo->count() == 0) {
12✔
695
        ui->table_combo->addItem("(No tables available)", "");
9✔
696
    }
697
}
24✔
698

699
void TableDesignerWidget::refreshRowDataSourceCombo() {
11✔
700
    ui->row_data_source_combo->clear();
11✔
701

702
    if (!_data_manager) {
11✔
703
        qDebug() << "refreshRowDataSourceCombo: No table manager";
×
704
        return;
×
705
    }
706

707
    auto data_sources = getAvailableDataSources();
11✔
708
    qDebug() << "refreshRowDataSourceCombo: Found" << data_sources.size() << "data sources:" << data_sources;
11✔
709

710
    for (QString const & source: data_sources) {
111✔
711
        // Only include valid row sources in this combo
712
        if (source.startsWith("TimeFrame: ") || source.startsWith("Events: ") || source.startsWith("Intervals: ")) {
100✔
713
            ui->row_data_source_combo->addItem(source);
78✔
714
        }
715
    }
716

717
    if (ui->row_data_source_combo->count() == 0) {
11✔
718
        ui->row_data_source_combo->addItem("(No data sources available)");
×
719
        qDebug() << "refreshRowDataSourceCombo: No data sources available";
×
720
    }
721
}
11✔
722

723

724
void TableDesignerWidget::loadTableInfo(QString const & table_id) {
7✔
725
    if (table_id.isEmpty() || !_data_manager) {
7✔
726
        clearUI();
×
727
        return;
×
728
    }
729

730
    auto * reg = _data_manager->getTableRegistry();
7✔
731
    auto info = reg ? reg->getTableInfo(table_id.toStdString()) : TableInfo{};
7✔
732
    if (info.id.empty()) {
7✔
733
        clearUI();
×
734
        return;
×
735
    }
736

737
    // Load table information
738
    if (_table_info_widget) {
7✔
739
        _table_info_widget->setName(QString::fromStdString(info.name));
7✔
740
        _table_info_widget->setDescription(QString::fromStdString(info.description));
7✔
741
    }
742

743
    // Load row source if available
744
    if (!info.rowSourceName.empty()) {
7✔
745
        int row_index = ui->row_data_source_combo->findText(QString::fromStdString(info.rowSourceName));
4✔
746
        if (row_index >= 0) {
4✔
747
            // Block signals to prevent circular dependency when loading table info
748
            ui->row_data_source_combo->blockSignals(true);
4✔
749
            ui->row_data_source_combo->setCurrentIndex(row_index);
4✔
750
            ui->row_data_source_combo->blockSignals(false);
4✔
751

752
            // Manually update the info label without triggering the signal handler
753
            updateRowInfoLabel(QString::fromStdString(info.rowSourceName));
4✔
754

755
            // Update interval settings visibility
756
            updateIntervalSettingsVisibility();
4✔
757

758
            // Since signals were blocked, this will ensure the tree is refreshed
759
            // when the computers tree is populated later in this function
760
        }
761
    }
762

763
    // Clear old column list (deprecated)
764
    // The computers tree will be populated based on available data sources
765
    refreshComputersTree();
7✔
766

767
    updateBuildStatus(QString("Loaded table: %1").arg(QString::fromStdString(info.name)));
7✔
768
    triggerPreviewDebounced();
7✔
769
}
7✔
770

771
void TableDesignerWidget::clearUI() {
21✔
772
    _current_table_id.clear();
21✔
773

774
    // Clear table info
775
    if (_table_info_widget) {
21✔
776
        _table_info_widget->setName("");
21✔
777
        _table_info_widget->setDescription("");
21✔
778
    }
779

780
    // Clear row source
781
    ui->row_data_source_combo->setCurrentIndex(-1);
21✔
782
    ui->row_info_label->setText("No row source selected");
21✔
783

784
    // Reset capture range and interval settings
785
    setCaptureRange(30000);// Default value
21✔
786
    if (ui->interval_beginning_radio) {
21✔
787
        ui->interval_beginning_radio->setChecked(true);
21✔
788
    }
789
    if (ui->interval_itself_radio) {
21✔
790
        ui->interval_itself_radio->setChecked(false);
21✔
791
    }
792
    if (ui->interval_settings_group) {
21✔
793
        ui->interval_settings_group->setVisible(false);
21✔
794
    }
795

796
    // Clear computers tree
797
    if (ui->computers_tree) {
21✔
798
        ui->computers_tree->clear();
21✔
799
    }
800

801
    // Disable controls
802
    ui->delete_table_btn->setEnabled(false);
21✔
803
    // Table info section is controlled separately
804
    ui->build_table_btn->setEnabled(false);
21✔
805
    if (auto gb = this->findChild<QGroupBox*>("row_source_group")) gb->setEnabled(false);
42✔
806
    if (auto gb = this->findChild<QGroupBox*>("column_design_group")) gb->setEnabled(false);
42✔
807
    if (_table_info_section) _table_info_section->setEnabled(false);
21✔
808

809
    updateBuildStatus("No table selected");
21✔
810
    if (_table_viewer) _table_viewer->clearTable();
21✔
811
}
21✔
812

813
void TableDesignerWidget::updateBuildStatus(QString const & message, bool is_error) {
32✔
814
    ui->build_status_label->setText(message);
32✔
815

816
    if (is_error) {
32✔
817
        ui->build_status_label->setStyleSheet("QLabel { color: red; font-weight: bold; }");
×
818
    } else {
819
        ui->build_status_label->setStyleSheet("QLabel { color: green; }");
32✔
820
    }
821
}
32✔
822

823
QStringList TableDesignerWidget::getAvailableDataSources() const {
43✔
824
    QStringList sources;
43✔
825

826
    if (!_data_manager) {
43✔
827
        qDebug() << "getAvailableDataSources: No table manager";
×
828
        return sources;
×
829
    }
830

831
    auto * reg = _data_manager->getTableRegistry();
43✔
832
    auto data_manager_extension = reg ? reg->getDataManagerExtension() : nullptr;
43✔
833
    if (!data_manager_extension) {
43✔
834
        qDebug() << "getAvailableDataSources: No data manager extension";
×
835
        return sources;
×
836
    }
837

838
    if (!_data_manager) {
43✔
839
        qDebug() << "getAvailableDataSources: No data manager";
×
840
        return sources;
×
841
    }
842

843
    // Add TimeFrame keys as potential row sources
844
    // TimeFrames can define intervals for analysis
845
    auto timeframe_keys = _data_manager->getTimeFrameKeys();
43✔
846
    qDebug() << "getAvailableDataSources: TimeFrame keys:" << timeframe_keys.size();
43✔
847
    for (auto const & key: timeframe_keys) {
215✔
848
        QString source = QString("TimeFrame: %1").arg(QString::fromStdString(key.str()));
172✔
849
        sources << source;
172✔
850
        qDebug() << "  Added TimeFrame:" << source;
172✔
851
    }
172✔
852

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

863
    // Add DigitalIntervalSeries keys as potential row sources
864
    // Intervals directly define analysis windows
865
    auto interval_keys = _data_manager->getKeys<DigitalIntervalSeries>();
43✔
866
    qDebug() << "getAvailableDataSources: Interval keys:" << interval_keys.size();
43✔
867
    for (auto const & key: interval_keys) {
86✔
868
        QString source = QString("Intervals: %1").arg(QString::fromStdString(key));
43✔
869
        sources << source;
43✔
870
        qDebug() << "  Added Intervals:" << source;
43✔
871
    }
43✔
872

873
    // Add AnalogTimeSeries keys as data sources (for computers; not row selectors)
874
    auto analog_keys = _data_manager->getKeys<AnalogTimeSeries>();
43✔
875
    qDebug() << "getAvailableDataSources: Analog keys:" << analog_keys.size();
43✔
876
    for (auto const & key: analog_keys) {
129✔
877
        QString source = QString("analog:%1").arg(QString::fromStdString(key));
86✔
878
        sources << source;
86✔
879
        qDebug() << "  Added Analog:" << source;
86✔
880
    }
86✔
881

882
    qDebug() << "getAvailableDataSources: Total sources found:" << sources.size();
43✔
883

884
    return sources;
43✔
885
}
43✔
886

887
std::pair<std::optional<DataSourceVariant>, RowSelectorType>
888
TableDesignerWidget::createDataSourceVariant(QString const & data_source_string,
290✔
889
                                             std::shared_ptr<DataManagerExtension> data_manager_extension) const {
890
    std::optional<DataSourceVariant> result;
290✔
891
    RowSelectorType row_selector_type = RowSelectorType::IntervalBased;
290✔
892

893
    if (data_source_string.startsWith("TimeFrame: ")) {
290✔
894
        // TimeFrames are used with TimestampSelector for rows; no concrete data source needed
895
        row_selector_type = RowSelectorType::Timestamp;
128✔
896

897
    } else if (data_source_string.startsWith("Events: ")) {
162✔
898
        QString source_name = data_source_string.mid(8);// Remove "Events: " prefix
66✔
899
        // Event-based computers in the registry operate with interval rows
900
        row_selector_type = RowSelectorType::IntervalBased;
66✔
901

902
        if (auto event_source = data_manager_extension->getEventSource(source_name.toStdString())) {
66✔
903
            result = event_source;
66✔
904
        }
66✔
905

906
    } else if (data_source_string.startsWith("Intervals: ")) {
162✔
907
        QString source_name = data_source_string.mid(11);// Remove "Intervals: " prefix
32✔
908
        row_selector_type = RowSelectorType::IntervalBased;
32✔
909

910
        if (auto interval_source = data_manager_extension->getIntervalSource(source_name.toStdString())) {
32✔
911
            result = interval_source;
32✔
912
        }
32✔
913
    } else if (data_source_string.startsWith("analog:")) {
96✔
914
        QString source_name = data_source_string.mid(7);// Remove "analog:" prefix
64✔
915
        row_selector_type = RowSelectorType::IntervalBased;
64✔
916

917
        if (auto analog_source = data_manager_extension->getAnalogSource(source_name.toStdString())) {
64✔
918
            result = analog_source;
64✔
919
        }
64✔
920
    }
64✔
921

922
    return {result, row_selector_type};
580✔
923
}
290✔
924

925
void TableDesignerWidget::updateRowInfoLabel(QString const & selected_source) {
18✔
926
    if (selected_source.isEmpty()) {
18✔
927
        ui->row_info_label->setText("No row source selected");
×
928
        return;
×
929
    }
930

931
    // Parse the selected source to get type and name
932
    QString source_type;
18✔
933
    QString source_name;
18✔
934

935
    if (selected_source.startsWith("TimeFrame: ")) {
18✔
936
        source_type = "TimeFrame";
11✔
937
        source_name = selected_source.mid(11);// Remove "TimeFrame: " prefix
11✔
938
    } else if (selected_source.startsWith("Events: ")) {
7✔
939
        source_type = "Events";
×
940
        source_name = selected_source.mid(8);// Remove "Events: " prefix
×
941
    } else if (selected_source.startsWith("Intervals: ")) {
7✔
942
        source_type = "Intervals";
7✔
943
        source_name = selected_source.mid(11);// Remove "Intervals: " prefix
7✔
944
    }
945

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

949
    if (!_data_manager) {
18✔
950
        ui->row_info_label->setText(info_text);
×
951
        return;
×
952
    }
953

954
    auto * reg3 = _data_manager->getTableRegistry();
18✔
955
    auto data_manager_extension = reg3 ? reg3->getDataManagerExtension() : nullptr;
18✔
956
    if (!data_manager_extension) {
18✔
957
        ui->row_info_label->setText(info_text);
×
958
        return;
×
959
    }
960

961
    auto const source_name_str = source_name.toStdString();
18✔
962

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

981
            // Add capture range and interval setting information
982
            if (isIntervalItselfSelected()) {
7✔
983
                info_text += QString("\nUsing intervals as-is (no capture range)");
×
984
            } else {
985
                int capture_range = getCaptureRange();
7✔
986
                QString interval_point = isIntervalBeginningSelected() ? "beginning" : "end";
7✔
987
                info_text += QString("\nCapture range: ±%1 samples around %2 of intervals").arg(capture_range).arg(interval_point);
7✔
988
            }
7✔
989
        }
7✔
990
    }
7✔
991

992
    ui->row_info_label->setText(info_text);
18✔
993
}
18✔
994

995
std::unique_ptr<IRowSelector> TableDesignerWidget::createRowSelector(QString const & row_source) {
10✔
996
    // Parse the row source to get type and name
997
    QString source_type;
10✔
998
    QString source_name;
10✔
999

1000
    if (row_source.startsWith("TimeFrame: ")) {
10✔
1001
        source_type = "TimeFrame";
×
1002
        source_name = row_source.mid(11);// Remove "TimeFrame: " prefix
×
1003
    } else if (row_source.startsWith("Events: ")) {
10✔
1004
        source_type = "Events";
×
1005
        source_name = row_source.mid(8);// Remove "Events: " prefix
×
1006
    } else if (row_source.startsWith("Intervals: ")) {
10✔
1007
        source_type = "Intervals";
10✔
1008
        source_name = row_source.mid(11);// Remove "Intervals: " prefix
10✔
1009
    } else {
1010
        qDebug() << "Unknown row source format:" << row_source;
×
1011
        return nullptr;
×
1012
    }
1013

1014
    auto const source_name_str = source_name.toStdString();
10✔
1015

1016
    try {
1017
        if (source_type == "TimeFrame") {
10✔
1018
            // Create IntervalSelector using TimeFrame
1019
            auto timeframe = _data_manager->getTime(TimeKey(source_name_str));
×
1020
            if (!timeframe) {
×
1021
                qDebug() << "TimeFrame not found:" << source_name;
×
1022
                return nullptr;
×
1023
            }
1024

1025
            // Use timestamps to select all rows
1026
            std::vector<TimeFrameIndex> timestamps;
×
1027
            for (int64_t i = 0; i < timeframe->getTotalFrameCount(); ++i) {
×
1028
                timestamps.push_back(TimeFrameIndex(i));
×
1029
            }
1030
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe);
×
1031

1032
        } else if (source_type == "Events") {
10✔
1033
            // Create TimestampSelector using DigitalEventSeries
1034
            auto event_series = _data_manager->getData<DigitalEventSeries>(source_name_str);
×
1035
            if (!event_series) {
×
1036
                qDebug() << "DigitalEventSeries not found:" << source_name;
×
1037
                return nullptr;
×
1038
            }
1039

1040
            auto events = event_series->getEventSeries();
×
1041
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
×
1042
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
×
1043
            if (!timeframe_obj) {
×
1044
                qDebug() << "TimeFrame not found for events:" << timeframe_key.str();
×
1045
                return nullptr;
×
1046
            }
1047

1048
            // Convert events to TimeFrameIndex
1049
            std::vector<TimeFrameIndex> timestamps;
×
1050
            for (auto const & event: events) {
×
1051
                timestamps.push_back(TimeFrameIndex(static_cast<int64_t>(event)));
×
1052
            }
1053

1054
            return std::make_unique<TimestampSelector>(std::move(timestamps), timeframe_obj);
×
1055

1056
        } else if (source_type == "Intervals") {
10✔
1057
            // Create IntervalSelector using DigitalIntervalSeries with capture range
1058
            auto interval_series = _data_manager->getData<DigitalIntervalSeries>(source_name_str);
10✔
1059
            if (!interval_series) {
10✔
1060
                qDebug() << "DigitalIntervalSeries not found:" << source_name;
×
1061
                return nullptr;
×
1062
            }
1063

1064
            auto intervals = interval_series->getDigitalIntervalSeries();
10✔
1065
            auto timeframe_key = _data_manager->getTimeKey(source_name_str);
10✔
1066
            auto timeframe_obj = _data_manager->getTime(timeframe_key);
10✔
1067
            if (!timeframe_obj) {
10✔
1068
                qDebug() << "TimeFrame not found for intervals:" << timeframe_key.str();
×
1069
                return nullptr;
×
1070
            }
1071

1072
            // Get capture range and interval setting
1073
            int capture_range = getCaptureRange();
10✔
1074
            bool use_beginning = isIntervalBeginningSelected();
10✔
1075
            bool use_interval_itself = isIntervalItselfSelected();
10✔
1076

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

1092
                    // Create a new interval around the reference point
1093
                    int64_t start_point = reference_point - capture_range;
40✔
1094
                    int64_t end_point = reference_point + capture_range;
40✔
1095

1096
                    // Ensure bounds are within the timeframe
1097
                    start_point = std::max(start_point, int64_t(0));
40✔
1098
                    end_point = std::min(end_point, static_cast<int64_t>(timeframe_obj->getTotalFrameCount() - 1));
40✔
1099

1100
                    tf_intervals.emplace_back(TimeFrameIndex(start_point), TimeFrameIndex(end_point));
40✔
1101
                }
1102
            }
1103

1104
            return std::make_unique<IntervalSelector>(std::move(tf_intervals), timeframe_obj);
10✔
1105
        }
10✔
1106

1107
    } catch (std::exception const & e) {
×
1108
        qDebug() << "Exception creating row selector:" << e.what();
×
1109
        return nullptr;
×
1110
    }
×
1111

1112
    qDebug() << "Unsupported row source type:" << source_type;
×
1113
    return nullptr;
×
1114
}
10✔
1115

1116
bool TableDesignerWidget::addColumnToBuilder(TableViewBuilder & builder, ColumnInfo const & column_info) {
×
1117
    // Use the simplified TableRegistry method that handles all the type checking internally
1118
    auto * reg = _data_manager->getTableRegistry();
×
1119
    if (!reg) {
×
1120
        qDebug() << "TableRegistry not available";
×
1121
        return false;
×
1122
    }
1123

1124
    bool success = reg->addColumnToBuilder(builder, column_info);
×
1125
    if (!success) {
×
1126
        qDebug() << "Failed to add column to builder:" << QString::fromStdString(column_info.name);
×
1127
    }
1128

1129
    return success;
×
1130
}
1131

1132
void TableDesignerWidget::updateIntervalSettingsVisibility() {
18✔
1133
    if (!ui->interval_settings_group) {
18✔
1134
        return;
×
1135
    }
1136

1137
    QString selected_key = ui->row_data_source_combo->currentText();
18✔
1138
    if (selected_key.isEmpty()) {
18✔
1139
        ui->interval_settings_group->setVisible(false);
×
1140
        if (ui->capture_range_spinbox) {
×
1141
            ui->capture_range_spinbox->setEnabled(false);
×
1142
        }
1143
        return;
×
1144
    }
1145

1146
    if (!_data_manager) {
18✔
1147
        ui->interval_settings_group->setVisible(false);
×
1148
        if (ui->capture_range_spinbox) {
×
1149
            ui->capture_range_spinbox->setEnabled(false);
×
1150
        }
1151
        return;
×
1152
    }
1153

1154
    // Check if the selected source is an interval series
1155
    if (selected_key.startsWith("Intervals: ")) {
18✔
1156
        ui->interval_settings_group->setVisible(true);
7✔
1157

1158
        // Enable/disable capture range based on interval setting
1159
        if (ui->capture_range_spinbox) {
7✔
1160
            bool use_interval_itself = isIntervalItselfSelected();
7✔
1161
            ui->capture_range_spinbox->setEnabled(!use_interval_itself);
7✔
1162
        }
1163
    } else {
1164
        ui->interval_settings_group->setVisible(false);
11✔
1165
        if (ui->capture_range_spinbox) {
11✔
1166
            ui->capture_range_spinbox->setEnabled(false);
11✔
1167
        }
1168
    }
1169
}
18✔
1170

1171
int TableDesignerWidget::getCaptureRange() const {
17✔
1172
    if (ui->capture_range_spinbox) {
17✔
1173
        return ui->capture_range_spinbox->value();
17✔
1174
    }
1175
    return 30000;// Default value
×
1176
}
1177

1178
void TableDesignerWidget::setCaptureRange(int value) {
21✔
1179
    if (ui->capture_range_spinbox) {
21✔
1180
        ui->capture_range_spinbox->blockSignals(true);
21✔
1181
        ui->capture_range_spinbox->setValue(value);
21✔
1182
        ui->capture_range_spinbox->blockSignals(false);
21✔
1183
    }
1184
}
21✔
1185

1186
bool TableDesignerWidget::isIntervalBeginningSelected() const {
17✔
1187
    if (ui->interval_beginning_radio) {
17✔
1188
        return ui->interval_beginning_radio->isChecked();
17✔
1189
    }
1190
    return true;// Default to beginning
×
1191
}
1192

1193
bool TableDesignerWidget::isIntervalItselfSelected() const {
24✔
1194
    if (ui->interval_itself_radio) {
24✔
1195
        return ui->interval_itself_radio->isChecked();
24✔
1196
    }
1197
    return false;// Default to not selected
×
1198
}
1199

1200
void TableDesignerWidget::triggerPreviewDebounced() {
67✔
1201
    if (_preview_debounce_timer) _preview_debounce_timer->start();
67✔
1202
    // Also trigger an immediate rebuild to support non-interactive/test contexts
1203
    rebuildPreviewNow();
67✔
1204
}
67✔
1205

1206
void TableDesignerWidget::rebuildPreviewNow() {
67✔
1207
    if (!_data_manager || !_table_viewer) return;
67✔
1208
    if (_current_table_id.isEmpty()) {
67✔
1209
        _table_viewer->clearTable();
40✔
1210
        return;
40✔
1211
    }
1212

1213
    QString row_source = ui->row_data_source_combo ? ui->row_data_source_combo->currentText() : QString();
27✔
1214
    if (row_source.isEmpty()) {
27✔
1215
        _table_viewer->clearTable();
6✔
1216
        return;
6✔
1217
    }
1218

1219
    // Get enabled column infos from the computers tree
1220
    auto column_infos = getEnabledColumnInfos();
21✔
1221
    if (column_infos.empty()) {
21✔
1222
        _table_viewer->clearTable();
12✔
1223
        return;
12✔
1224
    }
1225

1226
    // Create row selector for the entire dataset
1227
    auto selector = createRowSelector(row_source);
9✔
1228
    if (!selector) {
9✔
1229
        _table_viewer->clearTable();
×
1230
        return;
×
1231
    }
1232

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

1252
    // Set up the table viewer with pagination
1253
    _table_viewer->setTableConfiguration(
45✔
1254
            std::move(selector),
9✔
1255
            std::move(column_infos),
9✔
1256
            _data_manager,
9✔
1257
            QString("Preview: %1").arg(_current_table_id));
18✔
1258

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

1278
void TableDesignerWidget::refreshComputersTree() {
32✔
1279
    if (!_data_manager) return;
32✔
1280

1281
    _updating_computers_tree = true;
32✔
1282

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

1298
    ui->computers_tree->clear();
32✔
1299
    ui->computers_tree->setHeaderLabels({"Data Source / Computer", "Enabled", "Column Name"});
160✔
1300

1301
    auto * registry = _data_manager->getTableRegistry();
32✔
1302
    if (!registry) {
32✔
1303
        _updating_computers_tree = false;
×
1304
        return;
×
1305
    }
1306

1307
    auto data_manager_extension = registry->getDataManagerExtension();
32✔
1308
    if (!data_manager_extension) {
32✔
1309
        _updating_computers_tree = false;
×
1310
        return;
×
1311
    }
1312

1313
    auto & computer_registry = registry->getComputerRegistry();
32✔
1314

1315
    // Get available data sources
1316
    auto data_sources = getAvailableDataSources();
32✔
1317

1318
    // Create tree structure: Data Source -> Computers
1319
    for (QString const & data_source: data_sources) {
322✔
1320
        auto * data_source_item = new QTreeWidgetItem(ui->computers_tree);
290✔
1321
        data_source_item->setText(0, data_source);
290✔
1322
        data_source_item->setFlags(Qt::ItemIsEnabled);
290✔
1323
        data_source_item->setExpanded(false);// Start collapsed
290✔
1324

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

1328
        if (!data_source_variant.has_value()) {
290✔
1329
            qDebug() << "Failed to create data source variant for:" << data_source;
128✔
1330
            continue;
128✔
1331
        }
1332

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

1336
        // Add compatible computers as children
1337
        for (auto const & computer_info: available_computers) {
1,096✔
1338
            auto * computer_item = new QTreeWidgetItem(data_source_item);
934✔
1339
            computer_item->setText(0, QString::fromStdString(computer_info.name));
934✔
1340
            computer_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsEditable);
934✔
1341
            computer_item->setCheckState(1, Qt::Unchecked);
934✔
1342

1343
            // Generate default column name
1344
            QString default_name = generateDefaultColumnName(data_source, QString::fromStdString(computer_info.name));
934✔
1345
            computer_item->setText(2, default_name);
934✔
1346
            computer_item->setFlags(computer_item->flags() | Qt::ItemIsEditable);
934✔
1347

1348
            // Store data source and computer name for later use
1349
            computer_item->setData(0, Qt::UserRole, data_source);
934✔
1350
            computer_item->setData(1, Qt::UserRole, QString::fromStdString(computer_info.name));
934✔
1351

1352
            // Restore previous state if present
1353
            std::string prev_key = (data_source + "||" + QString::fromStdString(computer_info.name)).toStdString();
934✔
1354
            auto it_prev = previous_states.find(prev_key);
934✔
1355
            if (it_prev != previous_states.end()) {
934✔
1356
                computer_item->setCheckState(1, it_prev->second.first);
583✔
1357
                if (!it_prev->second.second.isEmpty()) {
583✔
1358
                    computer_item->setText(2, it_prev->second.second);
583✔
1359
                }
1360
            }
1361
        }
934✔
1362
    }
290✔
1363

1364
    // Resize columns to content
1365
    ui->computers_tree->resizeColumnToContents(0);
32✔
1366
    ui->computers_tree->resizeColumnToContents(1);
32✔
1367
    ui->computers_tree->resizeColumnToContents(2);
32✔
1368

1369
    _updating_computers_tree = false;
32✔
1370

1371
    // Update preview after refresh
1372
    triggerPreviewDebounced();
32✔
1373
}
64✔
1374

1375
void TableDesignerWidget::onComputersTreeItemChanged() {
7,138✔
1376
    if (_updating_computers_tree) return;
7,138✔
1377

1378
    // Trigger preview update when checkbox states change
1379
    triggerPreviewDebounced();
13✔
1380
}
1381

1382
void TableDesignerWidget::onComputersTreeItemEdited(QTreeWidgetItem * item, int column) {
7,138✔
1383
    if (_updating_computers_tree) return;
7,138✔
1384

1385
    // Only respond to column name edits (column 2)
1386
    if (column == 2) {
13✔
1387
        // Column name was edited, trigger preview update
1388
        triggerPreviewDebounced();
1✔
1389
    }
1390
}
1391

1392
std::vector<ColumnInfo> TableDesignerWidget::getEnabledColumnInfos() const {
26✔
1393
    std::vector<ColumnInfo> column_infos;
26✔
1394

1395
    if (!ui->computers_tree) return column_infos;
26✔
1396

1397
    // Iterate through all data source items
1398
    for (int i = 0; i < ui->computers_tree->topLevelItemCount(); ++i) {
260✔
1399
        auto * data_source_item = ui->computers_tree->topLevelItem(i);
234✔
1400

1401
        // Iterate through computer items under each data source
1402
        for (int j = 0; j < data_source_item->childCount(); ++j) {
988✔
1403
            auto * computer_item = data_source_item->child(j);
754✔
1404

1405
            // Check if this computer is enabled
1406
            if (computer_item->checkState(1) == Qt::Checked) {
754✔
1407
                QString data_source = computer_item->data(0, Qt::UserRole).toString();
28✔
1408
                QString computer_name = computer_item->data(1, Qt::UserRole).toString();
28✔
1409
                QString column_name = computer_item->text(2);
28✔
1410

1411
                if (column_name.isEmpty()) {
28✔
1412
                    column_name = generateDefaultColumnName(data_source, computer_name);
×
1413
                }
1414

1415
                // Create ColumnInfo (use raw key without UI prefixes)
1416
                QString source_key = data_source;
28✔
1417
                if (source_key.startsWith("Events: ")) {
28✔
1418
                    source_key = source_key.mid(8);
28✔
1419
                } else if (source_key.startsWith("Intervals: ")) {
×
1420
                    source_key = source_key.mid(11);
×
1421
                } else if (source_key.startsWith("analog:")) {
×
1422
                    source_key = source_key.mid(7);
×
1423
                } else if (source_key.startsWith("TimeFrame: ")) {
×
1424
                    source_key = source_key.mid(11);
×
1425
                }
1426

1427
                ColumnInfo info(column_name.toStdString(),
84✔
1428
                                QString("Column from %1 using %2").arg(data_source, computer_name).toStdString(),
56✔
1429
                                source_key.toStdString(),
56✔
1430
                                computer_name.toStdString());
140✔
1431

1432
                // Set output type based on computer info
1433
                if (auto * registry = _data_manager->getTableRegistry()) {
28✔
1434
                    auto & computer_registry = registry->getComputerRegistry();
28✔
1435
                    auto computer_info = computer_registry.findComputerInfo(computer_name.toStdString());
28✔
1436
                    if (computer_info) {
28✔
1437
                        info.outputType = computer_info->outputType;
28✔
1438
                        info.outputTypeName = computer_info->outputTypeName;
28✔
1439
                        info.isVectorType = computer_info->isVectorType;
28✔
1440
                        if (info.isVectorType) {
28✔
1441
                            info.elementType = computer_info->elementType;
5✔
1442
                            info.elementTypeName = computer_info->elementTypeName;
5✔
1443
                        }
1444
                    }
1445
                }
1446

1447
                column_infos.push_back(std::move(info));
28✔
1448
            }
28✔
1449
        }
1450
    }
1451

1452
    return column_infos;
26✔
1453
}
×
1454

1455
bool TableDesignerWidget::isComputerCompatibleWithDataSource(std::string const & computer_name, QString const & data_source) const {
×
1456
    if (!_data_manager) return false;
×
1457

1458
    auto * registry = _data_manager->getTableRegistry();
×
1459
    if (!registry) return false;
×
1460

1461
    auto & computer_registry = registry->getComputerRegistry();
×
1462
    auto computer_info = computer_registry.findComputerInfo(computer_name);
×
1463
    if (!computer_info) return false;
×
1464

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

1482
    // Default: assume compatibility for unrecognized patterns
1483
    return true;
×
1484
}
1485

1486
QString TableDesignerWidget::generateDefaultColumnName(QString const & data_source, QString const & computer_name) const {
934✔
1487
    QString source_name = data_source;
934✔
1488

1489
    // Extract the actual name from prefixed data sources
1490
    if (source_name.startsWith("Events: ")) {
934✔
1491
        source_name = source_name.mid(8);
198✔
1492
    } else if (source_name.startsWith("Intervals: ")) {
736✔
1493
        source_name = source_name.mid(11);
224✔
1494
    } else if (source_name.startsWith("analog:")) {
512✔
1495
        source_name = source_name.mid(7);
512✔
1496
    } else if (source_name.startsWith("TimeFrame: ")) {
×
1497
        source_name = source_name.mid(11);
×
1498
    }
1499

1500
    // Create a concise name
1501
    return QString("%1_%2").arg(source_name, computer_name);
2,802✔
1502
}
934✔
1503

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