v2.0.0
Loading...
Searching...
No Matches
channeldataview.cpp
Go to the documentation of this file.
1//=============================================================================================================
13
14//=============================================================================================================
15// INCLUDES
16//=============================================================================================================
17
18#include "channeldataview.h"
23
24#include <fiff/fiff_info.h>
25
26//=============================================================================================================
27// QT INCLUDES
28//=============================================================================================================
29
30#include <QVBoxLayout>
31#include <QHBoxLayout>
32#include <QScrollBar>
33#include <QToolButton>
34#include <QKeyEvent>
35#include <algorithm>
36#include <numeric>
37#include <QResizeEvent>
38#include <QSettings>
39#include <QLabel>
40#include <QPainter>
41#include <QtMath>
42
43//=============================================================================================================
44// USED NAMESPACES
45//=============================================================================================================
46
47using namespace DISPLIB;
48using namespace FIFFLIB;
49using namespace Eigen;
50
51//=============================================================================================================
52// PRIVATE HELPER WIDGET — RulerHeaderWidget
53// Left-column header that spans the full height of the TimeRulerWidget (stim
54// lane + time zone). The top portion is labelled "Stim" and the bottom portion
55// shows "Time" with a "mm:ss·ms" sub-label — mirroring the two zones of the ruler.
56//=============================================================================================================
57
58class RulerHeaderWidget : public QWidget
59{
60public:
61 explicit RulerHeaderWidget(QWidget *parent = nullptr) : QWidget(parent)
62 {
63 setFixedHeight(TimeRulerWidget::kTotalH);
64 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
65 }
66
67 void setClockTimeFormat(bool useClock) {
68 if (m_useClock == useClock) return;
69 m_useClock = useClock;
70 update();
71 }
72
73protected:
74 void paintEvent(QPaintEvent *) override
75 {
76 QPainter p(this);
77 p.setRenderHint(QPainter::TextAntialiasing, true);
78
79 const int kStimH = TimeRulerWidget::kStimZoneH;
80 const int kTimeH = TimeRulerWidget::kTimeZoneH;
81 const int W = width();
82
83 // Time zone background (top) — matches ruler time lane
84 p.fillRect(QRect(0, 0, W, kTimeH), QColor(245, 245, 247));
85
86 // Stim zone background (bottom) — matches ruler stim lane
87 p.fillRect(QRect(0, kTimeH, W, kStimH), QColor(238, 238, 246));
88
89 // Separator between zones
90 p.setPen(QPen(QColor(190, 190, 205), 1));
91 p.drawLine(0, kTimeH, W - 1, kTimeH);
92
93 // Right-side separator (between header and ruler)
94 p.setPen(QPen(QColor(185, 185, 195), 1));
95 p.drawLine(W - 1, 0, W - 1, height());
96
97 // "Time" bold in upper half of the time zone (top)
98 QFont tf = font();
99 tf.setPointSizeF(8.0);
100 tf.setBold(true);
101 p.setFont(tf);
102 p.setPen(QColor(60, 60, 70));
103 p.drawText(QRect(0, 0, W, kTimeH / 2 + 2), Qt::AlignCenter, QStringLiteral("Time"));
104
105 // Format sub-label in lower half of the time zone
106 QFont subf = font();
107 subf.setPointSizeF(6.5);
108 p.setFont(subf);
109 p.setPen(QColor(130, 130, 145));
110 QString fmtLabel = m_useClock ? QStringLiteral("mm:ss\u00B7ms")
111 : QStringLiteral("seconds");
112 p.drawText(QRect(0, kTimeH / 2, W, kTimeH / 2),
113 Qt::AlignCenter, fmtLabel);
114
115 // "Stim" label in the stim zone (bottom)
116 QFont sf = font();
117 sf.setPointSizeF(7.5);
118 sf.setBold(true);
119 p.setFont(sf);
120 p.setPen(QColor(80, 80, 100));
121 p.drawText(QRect(0, kTimeH, W, kStimH), Qt::AlignCenter, QStringLiteral("Stim"));
122 }
123
124private:
125 bool m_useClock = false;
126};
127
128//=============================================================================================================
129// DEFINE MEMBER METHODS
130//=============================================================================================================
131
132ChannelDataView::ChannelDataView(const QString &sSettingsPath,
133 QWidget *parent,
134 Qt::WindowFlags f)
135 : AbstractView(parent, f)
136 , m_sSettingsPath(sSettingsPath)
137 , m_pModel(new ChannelDataModel(this))
138{
139 setupLayout();
140 loadSettings();
141}
142
143//=============================================================================================================
144
149
150//=============================================================================================================
151
152void ChannelDataView::setupLayout()
153{
154 auto *outerLayout = new QVBoxLayout(this);
155 outerLayout->setContentsMargins(0, 0, 0, 0);
156 outerLayout->setSpacing(0);
157
158 // ── Render area ──────────────────────────────────────────────────────
159 // Layout:
160 // renderRow (HBox)
161 // leftCol (VBox): [stimHdr(22px) | timeHdr(28px) | labelPanel ]
162 // traceCol (VBox): [stimStrip(22px) | ruler(28px) | rhiView(flex)]
163 // rightCol (VBox): [spacer(22px) | scrollModeBtn(28px) | chanSB ]
164
165 auto *renderRow = new QHBoxLayout();
166 renderRow->setContentsMargins(0, 0, 0, 0);
167 renderRow->setSpacing(0);
168
169 // ── Left column ───────────────────────────────────────────────────────
170 auto *leftCol = new QVBoxLayout();
171 leftCol->setContentsMargins(0, 0, 0, 0);
172 leftCol->setSpacing(0);
173
174 // Combined header spanning the full ruler height (stim + time zones)
175 m_pRulerHeader = new RulerHeaderWidget(this);
176 leftCol->addWidget(m_pRulerHeader, 0);
177
178 m_pLabelPanel = new ChannelLabelPanel(this);
179 leftCol->addWidget(m_pLabelPanel, 1);
180
181 renderRow->addLayout(leftCol, 0);
182
183 // ── Centre column: unified ruler (stim+time) + RHI view ──────────────
184 auto *traceColumn = new QVBoxLayout();
185 traceColumn->setContentsMargins(0, 0, 0, 0);
186 traceColumn->setSpacing(0);
187
188 m_pTimeRuler = new TimeRulerWidget(this);
189 traceColumn->addWidget(m_pTimeRuler, 0);
190
191 m_pRhiView = new ChannelRhiView(this);
192 m_pRhiView->setModel(m_pModel.data());
193 m_pRhiView->setBackgroundColor(m_bgColor);
194 m_pRhiView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
195 m_pRhiView->setFocusPolicy(Qt::ClickFocus);
196 traceColumn->addWidget(m_pRhiView, 1);
197
198 renderRow->addLayout(traceColumn, 1);
199
200 // ── Right column: spacer (ruler height) + channel scrollbar ─────────
201 auto *rightCol = new QVBoxLayout();
202 rightCol->setContentsMargins(0, 0, 0, 0);
203 rightCol->setSpacing(0);
204
205 // Spacer that aligns with the ruler height so the channel scrollbar
206 // starts exactly where the channel area begins.
207 auto *rulerSpacer = new QWidget(this);
208 rulerSpacer->setFixedHeight(TimeRulerWidget::kTotalH);
209 rulerSpacer->setVisible(true);
210 rightCol->addWidget(rulerSpacer, 0);
211
212 m_pChannelScrollBar = new QScrollBar(Qt::Vertical, this);
213 m_pChannelScrollBar->setMinimum(0);
214 m_pChannelScrollBar->setMaximum(0);
215 m_pChannelScrollBar->setValue(0);
216 m_pChannelScrollBar->setSingleStep(1);
217 m_pChannelScrollBar->setPageStep(12);
218 m_pChannelScrollBar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
219 rightCol->addWidget(m_pChannelScrollBar, 1);
220
221 renderRow->addLayout(rightCol, 0);
222
223 outerLayout->addLayout(renderRow, 1);
224
225 // ── Overview bar (minimap) ───────────────────────────────────────────
226 m_pOverviewBar = new OverviewBarWidget(this);
227 m_pOverviewBar->setModel(m_pModel.data());
228 outerLayout->addWidget(m_pOverviewBar, 0);
229
230 // ── Bottom row: horizontal scroll bar + scroll-mode toggle ───────────
231 // The scroll-mode button lives at the bottom-right corner, between the
232 // horizontal and vertical scrollbars, so it doesn't crowd the ruler area.
233 auto *bottomRow = new QHBoxLayout();
234 bottomRow->setContentsMargins(0, 0, 0, 0);
235 bottomRow->setSpacing(0);
236
237 m_pScrollBar = new QScrollBar(Qt::Horizontal, this);
238 m_pScrollBar->setMinimum(0);
239 m_pScrollBar->setMaximum(0);
240 m_pScrollBar->setValue(0);
241 m_pScrollBar->setSingleStep(1);
242 m_pScrollBar->setPageStep(100);
243 m_pScrollBar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
244 bottomRow->addWidget(m_pScrollBar, 1);
245
246 m_pScrollModeButton = new QToolButton(this);
247 m_pScrollModeButton->setCheckable(true);
248 m_pScrollModeButton->setChecked(true); // default: ↕ = channels
249 m_pScrollModeButton->setText(QStringLiteral("\u2195 Ch"));
250 m_pScrollModeButton->setToolTip(QStringLiteral(
251 "Vertical mouse wheel:\n"
252 "\u2195 Ch \u2013 scroll through channels\n"
253 "\u21c4 Time \u2013 scroll through time"));
254 bottomRow->addWidget(m_pScrollModeButton, 0);
255
256 outerLayout->addLayout(bottomRow, 0);
257
258 // ── Connections ──────────────────────────────────────────────────────
259 connect(m_pScrollBar, &QScrollBar::valueChanged,
260 this, &ChannelDataView::onScrollBarMoved);
261
262 connect(m_pChannelScrollBar, &QScrollBar::valueChanged,
263 this, &ChannelDataView::onChannelScrollBarMoved);
264
265 connect(m_pRhiView, &ChannelRhiView::scrollSampleChanged,
266 this, &ChannelDataView::onRhiScrollChanged);
267
268 connect(m_pRhiView, &ChannelRhiView::channelOffsetChanged,
269 this, &ChannelDataView::onChannelOffsetChanged);
270
271 connect(m_pLabelPanel, &ChannelLabelPanel::channelScrollRequested,
273
274 connect(m_pRhiView, &ChannelRhiView::channelOffsetChanged,
276
277 connect(m_pScrollModeButton, &QToolButton::toggled, this, [this](bool checked) {
278 // checked = ↕ channels mode; unchecked = ⟷ time mode
279 m_pRhiView->setWheelScrollsChannels(checked);
280 m_pScrollModeButton->setText(checked ? QStringLiteral("\u2195 Ch")
281 : QStringLiteral("\u21c4 Time"));
282 });
283
284 connect(m_pRhiView, &ChannelRhiView::sampleClicked,
286
287 connect(m_pRhiView, &ChannelRhiView::sampleRangeSelected,
289
290 connect(m_pRhiView, &ChannelRhiView::annotationBoundaryMoved,
292
293 // Forward crosshair cursor data to ChannelDataView consumers
294 connect(m_pRhiView, &ChannelRhiView::cursorDataChanged,
296
297 connect(m_pRhiView, &ChannelRhiView::scrollSampleChanged,
298 m_pTimeRuler, &TimeRulerWidget::setScrollSample);
299
300 connect(m_pRhiView, &ChannelRhiView::samplesPerPixelChanged,
302
309
310 // Recompute samples-per-pixel when the RHI view itself is resized so the
311 // time window stays constant regardless of the widget's pixel width.
312 connect(m_pRhiView, &ChannelRhiView::viewResized,
313 this, [this](int, int) { updateSamplesPerPixel(); });
314
315 connect(m_pModel.data(), &ChannelDataModel::dataChanged,
316 this, &ChannelDataView::updateScrollBarRange);
317
318 connect(m_pModel.data(), &ChannelDataModel::metaChanged,
319 this, &ChannelDataView::updateChannelScrollBarRange);
320
321 // Keep label panel in sync with model changes so status pills update immediately.
322 connect(m_pModel.data(), &ChannelDataModel::metaChanged,
323 m_pLabelPanel, QOverload<>::of(&QWidget::update));
324 connect(m_pModel.data(), &ChannelDataModel::dataChanged,
325 m_pLabelPanel, QOverload<>::of(&QWidget::update));
326
327 // Feed current visible sample window to the label panel for the RMS level bar.
328 connect(m_pRhiView, &ChannelRhiView::scrollSampleChanged,
329 this, [this](float sample) {
330 if (m_pLabelPanel && m_pRhiView) {
331 m_pLabelPanel->setVisibleSampleRange(
332 static_cast<int>(sample),
333 static_cast<int>(sample) + m_pRhiView->visibleSampleCount());
334 }
335 });
336
337 // ── Overview bar connections ─────────────────────────────────────────
338 // Keep viewport indicator in sync with scroll/zoom changes.
339 connect(m_pRhiView, &ChannelRhiView::scrollSampleChanged,
340 this, [this](float sample) {
341 if (m_pOverviewBar && m_pRhiView) {
342 m_pOverviewBar->setViewport(
343 sample,
344 static_cast<float>(m_pRhiView->visibleSampleCount()));
345 }
346 });
347 connect(m_pRhiView, &ChannelRhiView::samplesPerPixelChanged,
348 this, [this](float) {
349 if (m_pOverviewBar && m_pRhiView) {
350 m_pOverviewBar->setViewport(
351 m_pRhiView->scrollSample(),
352 static_cast<float>(m_pRhiView->visibleSampleCount()));
353 }
354 });
355 // Rebuild envelope when data changes.
356 connect(m_pModel.data(), &ChannelDataModel::dataChanged,
357 this, [this]() {
358 if (m_pOverviewBar) {
359 m_pOverviewBar->setModel(m_pModel.data());
360 }
361 });
362 // Click/drag on overview bar → navigate.
363 connect(m_pOverviewBar, &OverviewBarWidget::scrollRequested,
364 this, [this](float targetSample) {
365 if (m_pRhiView)
366 m_pRhiView->scrollTo(targetSample, 150);
367 });
368
369 setFocusProxy(m_pRhiView);
370 setFocusPolicy(Qt::StrongFocus);
371}
372
373//=============================================================================================================
374
375void ChannelDataView::init(QSharedPointer<FiffInfo> pInfo)
376{
377 m_pFiffInfo = pInfo;
378 m_pModel->init(pInfo);
379
380 // Re-apply scale map and colour after re-init
381 if (!m_scaleMap.isEmpty())
382 m_pModel->setScaleMap(m_scaleMap);
383 m_pModel->setSignalColor(m_signalColor);
384
385 if (pInfo) {
386 if (m_pTimeRuler)
387 m_pTimeRuler->setSfreq(pInfo->sfreq);
388 if (m_pRhiView)
389 m_pRhiView->setSfreq(static_cast<float>(pInfo->sfreq));
390 if (m_pOverviewBar)
391 m_pOverviewBar->setSfreq(static_cast<float>(pInfo->sfreq));
392 }
393
394 if (m_pLabelPanel) {
395 m_pLabelPanel->setModel(m_pModel.data());
396 m_pLabelPanel->setVisibleChannelCount(m_pRhiView ? m_pRhiView->visibleChannelCount() : 12);
397 }
398
399 updateSamplesPerPixel();
400 updateScrollBarRange();
401 updateChannelScrollBarRange();
402}
403
404//=============================================================================================================
405
406void ChannelDataView::setFileBounds(int first, int last)
407{
408 m_firstFileSample = first;
409 m_lastFileSample = last;
410 if (m_pRhiView) {
411 m_pRhiView->setFirstFileSample(first);
412 m_pRhiView->setLastFileSample(last);
413 }
414 if (m_pTimeRuler)
415 m_pTimeRuler->setFirstFileSample(first);
416 if (m_pOverviewBar) {
417 m_pOverviewBar->setFirstFileSample(first);
418 m_pOverviewBar->setLastFileSample(last);
419 }
420 updateScrollBarRange();
421}
422
423//=============================================================================================================
424
425void ChannelDataView::setData(const MatrixXd &data, int firstSample)
426{
427 m_pModel->setData(data, firstSample);
428 updateScrollBarRange();
429}
430
431//=============================================================================================================
432
433void ChannelDataView::addData(const MatrixXd &data)
434{
435 m_pModel->appendData(data);
436 updateScrollBarRange();
437}
438
439//=============================================================================================================
440
441void ChannelDataView::scrollToSample(int sample, bool animate)
442{
443 m_pRhiView->scrollTo(static_cast<float>(sample), animate ? 200 : 0);
444}
445
446//=============================================================================================================
447
449{
450 m_windowSizeSeconds = qMax(seconds, 0.01f);
451 updateSamplesPerPixel();
452 saveSettings();
453}
454
455//=============================================================================================================
456
458{
459 return m_windowSizeSeconds;
460}
461
462//=============================================================================================================
463
464void ChannelDataView::setZoom(double factor)
465{
466 m_zoomFactor = qMax(factor, 0.001);
467 updateSamplesPerPixel();
468 saveSettings();
469}
470
471//=============================================================================================================
472
474{
475 return m_zoomFactor;
476}
477
478//=============================================================================================================
479
480void ChannelDataView::setBackgroundColor(const QColor &color)
481{
482 m_bgColor = color;
483 if (m_pRhiView)
484 m_pRhiView->setBackgroundColor(color);
485 saveSettings();
486}
487
488//=============================================================================================================
489
491{
492 return m_bgColor;
493}
494
495//=============================================================================================================
496
497void ChannelDataView::setSignalColor(const QColor &color)
498{
499 m_signalColor = color;
500 m_pModel->setSignalColor(color);
501 saveSettings();
502}
503
504//=============================================================================================================
505
507{
508 return m_signalColor;
509}
510
511//=============================================================================================================
512
513void ChannelDataView::setScalingMap(const QMap<qint32, float> &scaleMap)
514{
515 m_scaleMap = scaleMap;
516 m_pModel->setScaleMap(scaleMap);
517}
518
519//=============================================================================================================
520
521QMap<qint32, float> ChannelDataView::scalingMap() const
522{
523 return m_scaleMap;
524}
525
526//=============================================================================================================
527
529{
530 m_hideBadChannels = hide;
531 if (m_pRhiView)
532 m_pRhiView->setHideBadChannels(hide);
533 if (m_pLabelPanel)
534 m_pLabelPanel->setHideBadChannels(hide);
535 updateChannelScrollBarRange();
536}
537
538//=============================================================================================================
539
541{
542 return m_hideBadChannels;
543}
544
545//=============================================================================================================
546
547void ChannelDataView::setChannelFilter(const QStringList &names)
548{
549 QVector<int> indices;
550 if (!names.isEmpty() && m_pModel) {
551 int total = m_pModel->channelCount();
552 for (int i = 0; i < total; ++i) {
553 const ChannelDisplayInfo info = m_pModel->channelInfo(i);
554 if (info.isVirtualChannel || names.contains(info.name))
555 indices.append(i);
556 }
557 }
558
559 if (m_pRhiView)
560 m_pRhiView->setChannelIndices(indices);
561 if (m_pLabelPanel)
562 m_pLabelPanel->setChannelIndices(indices);
563
564 // Reset scroll to start of filtered set and update scrollbar range
565 updateChannelScrollBarRange();
566}
567
568//=============================================================================================================
569
571{
572 m_pModel->setRemoveDC(dc);
573}
574
575//=============================================================================================================
576
578{
579 m_pModel->setDetrendMode(mode);
580}
581
582//=============================================================================================================
583
585{
586 return m_pModel->detrendMode();
587}
588
589//=============================================================================================================
590
591void ChannelDataView::setEvents(const QVector<ChannelRhiView::EventMarker> &events)
592{
593 if (m_pRhiView)
594 m_pRhiView->setEvents(events);
595
596 if (m_pOverviewBar)
597 m_pOverviewBar->setEvents(events);
598
599 if (m_pTimeRuler) {
600 QVector<TimeRulerEventMark> rulerMarks;
601 rulerMarks.reserve(events.size());
602 for (const auto &ev : events)
603 rulerMarks.append({ev.sample, ev.color, ev.label});
604 m_pTimeRuler->setEvents(rulerMarks);
605 }
606}
607
608//=============================================================================================================
609
610void ChannelDataView::setEpochMarkers(const QVector<int> &triggerSamples)
611{
612 if (m_pRhiView)
613 m_pRhiView->setEpochMarkers(triggerSamples);
614}
615
617{
618 if (m_pRhiView)
619 m_pRhiView->setEpochMarkersVisible(visible);
620}
621
623{
624 return m_pRhiView ? m_pRhiView->epochMarkersVisible() : false;
625}
626
628{
629 if (m_pRhiView)
630 m_pRhiView->setClippingVisible(visible);
631}
632
634{
635 return m_pRhiView ? m_pRhiView->clippingVisible() : false;
636}
637
639{
640 if (m_pRhiView)
641 m_pRhiView->setZScoreMode(enabled);
642}
643
645{
646 return m_pRhiView ? m_pRhiView->zScoreMode() : false;
647}
648
649//=============================================================================================================
650
651void ChannelDataView::setReferenceMarkers(const QVector<TimeRulerReferenceMark> &markers)
652{
653 if (m_pTimeRuler)
654 m_pTimeRuler->setReferenceMarkers(markers);
655}
656
657//=============================================================================================================
658
659void ChannelDataView::setAnnotations(const QVector<ChannelRhiView::AnnotationSpan> &annotations)
660{
661 if (m_pRhiView)
662 m_pRhiView->setAnnotations(annotations);
663 if (m_pOverviewBar)
664 m_pOverviewBar->setAnnotations(annotations);
665}
666
667//=============================================================================================================
668
670{
671 if (m_pRhiView)
672 m_pRhiView->setAnnotationSelectionEnabled(enabled);
673}
674
675//=============================================================================================================
676
678{
679 if (m_pRhiView)
680 m_pRhiView->setEventsVisible(visible);
681}
682
684{
685 return m_pRhiView ? m_pRhiView->eventsVisible() : true;
686}
687
688//=============================================================================================================
689
691{
692 if (m_pRhiView)
693 m_pRhiView->setAnnotationsVisible(visible);
694}
695
697{
698 return m_pRhiView ? m_pRhiView->annotationsVisible() : true;
699}
700
701//=============================================================================================================
702
704{
705 if (m_pOverviewBar)
706 m_pOverviewBar->setVisible(visible);
707}
708
710{
711 return m_pOverviewBar ? m_pOverviewBar->isVisible() : true;
712}
713
714//=============================================================================================================
715
717{
718 if (m_pRhiView)
719 m_pRhiView->setScrollSpeedFactor(factor);
720}
721
723{
724 return m_pRhiView ? m_pRhiView->scrollSpeedFactor() : 1.0f;
725}
726
727//=============================================================================================================
728
730{
731 if (!m_pModel)
732 return;
733
734 // Define a canonical type ordering
735 static const QStringList typeOrder = {
736 "MEG grad", "MEG mag", "EEG", "EOG", "ECG", "EMG", "STIM", "MISC"
737 };
738
739 const int n = m_pModel->channelCount();
740 QVector<int> indices(n);
741 std::iota(indices.begin(), indices.end(), 0);
742
743 std::stable_sort(indices.begin(), indices.end(), [&](int a, int b) {
744 int ia = typeOrder.indexOf(m_pModel->channelInfo(a).typeLabel);
745 int ib = typeOrder.indexOf(m_pModel->channelInfo(b).typeLabel);
746 if (ia < 0) ia = typeOrder.size();
747 if (ib < 0) ib = typeOrder.size();
748 return ia < ib;
749 });
750
751 if (m_pRhiView)
752 m_pRhiView->setChannelIndices(indices);
753 if (m_pLabelPanel)
754 m_pLabelPanel->setChannelIndices(indices);
755 updateChannelScrollBarRange();
756}
757
759{
760 if (m_pRhiView)
761 m_pRhiView->setChannelIndices({});
762 if (m_pLabelPanel)
763 m_pLabelPanel->setChannelIndices({});
764 updateChannelScrollBarRange();
765}
766
767//=============================================================================================================
768
770{
771 if (m_pRhiView)
772 m_pRhiView->setCrosshairEnabled(enabled);
773}
774
776{
777 return m_pRhiView ? m_pRhiView->crosshairEnabled() : false;
778}
779
780//=============================================================================================================
781
783{
784 if (m_pRhiView)
785 m_pRhiView->setScalebarsVisible(visible);
786}
787
789{
790 return m_pRhiView ? m_pRhiView->scalebarsVisible() : false;
791}
792
793//=============================================================================================================
794
796{
797 if (m_pRhiView)
798 m_pRhiView->setButterflyMode(enabled);
799 if (m_pLabelPanel)
800 m_pLabelPanel->setButterflyMode(enabled);
801}
802
804{
805 return m_pRhiView ? m_pRhiView->butterflyMode() : false;
806}
807
808//=============================================================================================================
809
811{
812 if (m_pTimeRuler)
813 m_pTimeRuler->toggleTimeFormat();
814 bool useClock = m_pTimeRuler ? m_pTimeRuler->clockTimeFormat() : false;
815 if (m_pRhiView)
816 m_pRhiView->setClockTimeFormat(useClock);
817 if (m_pRulerHeader)
818 static_cast<RulerHeaderWidget*>(m_pRulerHeader)->setClockTimeFormat(useClock);
819}
820
822{
823 if (m_pTimeRuler)
824 m_pTimeRuler->setClockTimeFormat(useClock);
825 if (m_pRhiView)
826 m_pRhiView->setClockTimeFormat(useClock);
827 if (m_pRulerHeader)
828 static_cast<RulerHeaderWidget*>(m_pRulerHeader)->setClockTimeFormat(useClock);
829}
830
832{
833 return m_pTimeRuler ? m_pTimeRuler->clockTimeFormat() : false;
834}
835
836//=============================================================================================================
837
839{
840 return m_pRhiView ? m_pRhiView->visibleFirstSample() : 0;
841}
842
843//=============================================================================================================
844
846{
847 return m_pRhiView ? m_pRhiView->visibleSampleCount() : 0;
848}
849
850//=============================================================================================================
851
853{
854 return m_pRhiView ? m_pRhiView->geometry() : QRect();
855}
856
857//=============================================================================================================
858
860{
861 const QRect viewportRect = signalViewportRect();
862 const int visibleSamples = qMax(1, visibleSampleCount());
863
864 if(viewportRect.width() <= 0) {
865 return viewportRect.left();
866 }
867
868 const double samplesPerPixel = static_cast<double>(visibleSamples)
869 / static_cast<double>(viewportRect.width());
870 const double xOffset = static_cast<double>(sample - firstVisibleSample()) / samplesPerPixel;
871
872 return viewportRect.left()
873 + qBound(0, qRound(xOffset), viewportRect.width() - 1);
874}
875
876//=============================================================================================================
877
879{
880 const QRect viewportRect = signalViewportRect();
881 const int visibleSamples = qMax(1, visibleSampleCount());
882
883 if(viewportRect.width() <= 0) {
884 return firstVisibleSample();
885 }
886
887 const int clampedX = qBound(viewportRect.left(), x, viewportRect.right());
888 const double samplesPerPixel = static_cast<double>(visibleSamples)
889 / static_cast<double>(viewportRect.width());
890
891 return firstVisibleSample()
892 + qRound(static_cast<double>(clampedX - viewportRect.left()) * samplesPerPixel);
893}
894
895//=============================================================================================================
896
898{
899 if (m_sSettingsPath.isEmpty())
900 return;
901 QSettings s;
902 s.setValue(m_sSettingsPath + "/windowSizeSeconds", m_windowSizeSeconds);
903 s.setValue(m_sSettingsPath + "/zoomFactor", m_zoomFactor);
904 s.setValue(m_sSettingsPath + "/backgroundColor", m_bgColor);
905 s.setValue(m_sSettingsPath + "/signalColor", m_signalColor);
906}
907
908//=============================================================================================================
909
911{
912 if (m_sSettingsPath.isEmpty())
913 return;
914 QSettings s;
915 float rawWindowSec = s.value(m_sSettingsPath + "/windowSizeSeconds", 10.f).toFloat();
916 m_windowSizeSeconds = qBound(0.5f, rawWindowSec, 120.f);
917 // Remove stale out-of-range value so next session starts clean.
918 if (rawWindowSec != m_windowSizeSeconds)
919 s.remove(m_sSettingsPath + "/windowSizeSeconds");
920
921 m_zoomFactor = s.value(m_sSettingsPath + "/zoomFactor", 1.0).toDouble();
922 m_bgColor = s.value(m_sSettingsPath + "/backgroundColor", QColor(250, 250, 250)).value<QColor>();
923 m_signalColor = s.value(m_sSettingsPath + "/signalColor", QColor(Qt::darkGreen)).value<QColor>();
924
925 // Propagate loaded colors to the sub-views (they may have been created
926 // with defaults in setupLayout() before loadSettings() ran).
927 if (m_pRhiView)
928 m_pRhiView->setBackgroundColor(m_bgColor);
929 if (m_pModel)
930 m_pModel->setSignalColor(m_signalColor);
931}
932
933//=============================================================================================================
934
936{
937 m_pModel->clearData();
938 if (m_pScrollBar) {
939 m_pScrollBar->setMaximum(0);
940 m_pScrollBar->setValue(0);
941 }
942 if (m_pRhiView)
943 m_pRhiView->setScrollSample(0.f);
944 if (m_pTimeRuler) {
945 m_pTimeRuler->setFirstFileSample(0);
946 m_pTimeRuler->setScrollSample(0.f);
947 m_pTimeRuler->setReferenceMarkers({});
948 }
949}
950
951//=============================================================================================================
952
954
956
957//=============================================================================================================
958
959void ChannelDataView::keyPressEvent(QKeyEvent *event)
960{
961 if (!m_pRhiView) {
962 QWidget::keyPressEvent(event);
963 return;
964 }
965
966 float step = m_pRhiView->visibleSampleCount() * 0.1f;
967 float page = m_pRhiView->visibleSampleCount() * 0.9f;
968
969 switch (event->key()) {
970 case Qt::Key_Left:
971 m_pRhiView->scrollTo(m_pRhiView->scrollSample() - step, 150);
972 break;
973 case Qt::Key_Right:
974 m_pRhiView->scrollTo(m_pRhiView->scrollSample() + step, 150);
975 break;
976 case Qt::Key_PageUp:
977 m_pRhiView->scrollTo(m_pRhiView->scrollSample() - page, 200);
978 break;
979 case Qt::Key_PageDown:
980 m_pRhiView->scrollTo(m_pRhiView->scrollSample() + page, 200);
981 break;
982 case Qt::Key_Home:
983 m_pRhiView->scrollTo(static_cast<float>(m_pModel->firstSample()), 300);
984 break;
985 case Qt::Key_End:
986 {
987 float lastStart = static_cast<float>(
988 m_pModel->firstSample() + m_pModel->totalSamples()
989 - m_pRhiView->visibleSampleCount());
990 m_pRhiView->scrollTo(qMax(lastStart, 0.f), 300);
991 }
992 break;
993 case Qt::Key_Plus:
994 case Qt::Key_Equal:
995 m_pRhiView->zoomTo(m_pRhiView->samplesPerPixel() * 0.75f, 200);
996 break;
997 case Qt::Key_Minus:
998 m_pRhiView->zoomTo(m_pRhiView->samplesPerPixel() * 1.33f, 200);
999 break;
1000 case Qt::Key_B:
1003 break;
1004 case Qt::Key_D:
1005 if (m_pModel) {
1006 // Cycle: None → Mean → Linear → None
1007 switch (m_pModel->detrendMode()) {
1008 case DetrendMode::None: m_pModel->setDetrendMode(DetrendMode::Mean); break;
1009 case DetrendMode::Mean: m_pModel->setDetrendMode(DetrendMode::Linear); break;
1010 case DetrendMode::Linear: m_pModel->setDetrendMode(DetrendMode::None); break;
1011 }
1012 }
1013 break;
1014 case Qt::Key_S:
1017 break;
1018 case Qt::Key_X:
1021 break;
1022 case Qt::Key_E:
1025 break;
1026 case Qt::Key_G:
1029 break;
1030 case Qt::Key_C:
1033 break;
1034 case Qt::Key_Z:
1037 break;
1038 case Qt::Key_A:
1039 if (event->modifiers() & Qt::ShiftModifier) {
1042 } else {
1043 QWidget::keyPressEvent(event);
1044 return;
1045 }
1046 break;
1047 case Qt::Key_O:
1050 break;
1051 case Qt::Key_BracketRight:
1054 break;
1055 case Qt::Key_BracketLeft:
1058 break;
1059 case Qt::Key_T:
1061 break;
1062 default:
1063 QWidget::keyPressEvent(event);
1064 return;
1065 }
1066 event->accept();
1067}
1068
1069//=============================================================================================================
1070
1071void ChannelDataView::resizeEvent(QResizeEvent *event)
1072{
1073 AbstractView::resizeEvent(event);
1074 updateSamplesPerPixel();
1075 updateScrollBarRange();
1076}
1077
1078//=============================================================================================================
1079
1080void ChannelDataView::onScrollBarMoved(int value)
1081{
1082 if (m_scrollBarUpdating || !m_pRhiView)
1083 return;
1084 m_pRhiView->setScrollSample(static_cast<float>(value));
1085}
1086
1087//=============================================================================================================
1088
1089void ChannelDataView::onRhiScrollChanged(float sample)
1090{
1091 emit scrollPositionChanged(static_cast<int>(sample));
1092
1093 if (!m_pScrollBar)
1094 return;
1095
1096 m_scrollBarUpdating = true;
1097 m_pScrollBar->setValue(static_cast<int>(sample));
1098 m_scrollBarUpdating = false;
1099}
1100
1101//=============================================================================================================
1102
1103void ChannelDataView::updateScrollBarRange()
1104{
1105 if (!m_pScrollBar || !m_pRhiView)
1106 return;
1107
1108 int visible = m_pRhiView->visibleSampleCount();
1109 int minVal, maxVal;
1110
1111 if (m_firstFileSample >= 0 && m_lastFileSample >= 0) {
1112 // File bounds known: allow scrolling across the whole file
1113 minVal = m_firstFileSample;
1114 maxVal = qMax(m_firstFileSample, m_lastFileSample - visible + 1);
1115 } else {
1116 // Fall back to ring-buffer bounds
1117 int firstSamp = m_pModel->firstSample();
1118 int total = m_pModel->totalSamples();
1119 minVal = firstSamp;
1120 maxVal = qMax(firstSamp, firstSamp + total - visible);
1121 }
1122
1123 m_scrollBarUpdating = true;
1124 m_pScrollBar->setMinimum(minVal);
1125 m_pScrollBar->setMaximum(maxVal);
1126 m_pScrollBar->setPageStep(visible);
1127 m_scrollBarUpdating = false;
1128}
1129
1130//=============================================================================================================
1131
1132void ChannelDataView::updateSamplesPerPixel()
1133{
1134 if (!m_pRhiView || !m_pFiffInfo)
1135 return;
1136
1137 float sfreq = static_cast<float>(m_pFiffInfo->sfreq);
1138 int viewPx = m_pRhiView->width();
1139 if (viewPx <= 0)
1140 return;
1141
1142 // samples per pixel = (window_duration × sample_rate) / viewport_width / zoom
1143 float spp = (m_windowSizeSeconds * sfreq) / viewPx / static_cast<float>(m_zoomFactor);
1144 m_pRhiView->setSamplesPerPixel(qMax(spp, 1e-4f));
1145 updateScrollBarRange();
1146}
1147
1148//=============================================================================================================
1149
1150void ChannelDataView::onChannelScrollBarMoved(int value)
1151{
1152 if (m_channelScrollBarUpdating || !m_pRhiView)
1153 return;
1154 m_pRhiView->setFirstVisibleChannel(value);
1155}
1156
1157//=============================================================================================================
1158
1159void ChannelDataView::onChannelOffsetChanged(int firstChannel)
1160{
1161 if (!m_pChannelScrollBar)
1162 return;
1163 m_channelScrollBarUpdating = true;
1164 m_pChannelScrollBar->setValue(firstChannel);
1165 m_channelScrollBarUpdating = false;
1166}
1167
1168//=============================================================================================================
1169
1170void ChannelDataView::updateChannelScrollBarRange()
1171{
1172 if (!m_pChannelScrollBar || !m_pRhiView)
1173 return;
1174
1175 // Use the view's logical channel count (respects active filter)
1176 int totalCh = m_pRhiView ? m_pRhiView->totalLogicalChannels()
1177 : m_pModel->channelCount();
1178 int visibleCnt = m_pRhiView->visibleChannelCount();
1179 int maxVal = qMax(0, totalCh - visibleCnt);
1180
1181 m_channelScrollBarUpdating = true;
1182 m_pChannelScrollBar->setMaximum(maxVal);
1183 m_pChannelScrollBar->setPageStep(visibleCnt);
1184 m_channelScrollBarUpdating = false;
1185}
QRhi-based GPU-accelerated channel time-series renderer used by the modern raw browser.
Vertical column of channel-name labels synced with ChannelDataView's row geometry.
Circular-buffer Qt model exposing a rolling window of the live FIFF stream as a table.
Horizontal time-axis ruler displayed beneath ChannelDataView with sample / second ticks.
Composite real-time multi-channel time-series scroller (label panel, table view, time ruler,...
Full FIFF measurement metadata: everything from FIFFB_MEAS / FIFFB_MEAS_INFO needed to interpret a re...
FIFF file I/O, in-memory data structures and high-level readers/writers.
2-D display widgets and visualisation helpers (charts, topography, colour maps).
DetrendMode
Channel display metadata (read-only from the renderer's perspective).
AbstractView(QWidget *parent=0, Qt::WindowFlags f=Qt::Widget)
RulerHeaderWidget(QWidget *parent=nullptr)
void setClockTimeFormat(bool useClock)
void paintEvent(QPaintEvent *) override
ChannelDataView(const QString &sSettingsPath=QString(), QWidget *parent=nullptr, Qt::WindowFlags f=Qt::Widget)
void setScalingMap(const QMap< qint32, float > &scaleMap)
void referenceMarkerRemoveRequested(int sample)
void eventsVisibleToggled(bool on)
void clippingToggled(bool on)
int viewportXToSample(int x) const
void setButterflyMode(bool enabled)
void crosshairToggled(bool on)
void setBackgroundColor(const QColor &color)
void setClockTimeFormat(bool useClock)
void setWindowSize(float seconds)
void sampleClicked(int sample)
DetrendMode detrendMode() const
void zScoreModeToggled(bool on)
void annotationsVisibleToggled(bool on)
void setOverviewBarVisible(bool visible)
void updateGuiMode(GuiMode mode) override
void setSignalColor(const QColor &color)
void scrollPositionChanged(int sample)
void setData(const Eigen::MatrixXd &data, int firstSample=0)
void referenceMarkerAddRequested(int sample)
void setEpochMarkersVisible(bool visible)
void setEvents(const QVector< ChannelRhiView::EventMarker > &events)
void setZScoreMode(bool enabled)
void setDetrendMode(DetrendMode mode)
void scrollToSample(int sample, bool animate=true)
void scalebarsToggled(bool on)
void setFileBounds(int first, int last)
void resizeEvent(QResizeEvent *event) override
void init(QSharedPointer< FIFFLIB::FiffInfo > pInfo)
void butterflyToggled(bool on)
void setEpochMarkers(const QVector< int > &triggerSamples)
void setClippingVisible(bool visible)
int sampleToViewportX(int sample) const
void epochMarkersToggled(bool on)
void overviewBarToggled(bool on)
void setAnnotationsVisible(bool visible)
void setScalebarsVisible(bool visible)
void setAnnotationSelectionEnabled(bool enabled)
void updateProcessingMode(ProcessingMode mode) override
void cursorDataChanged(float timeSec, float amplitude, const QString &channelName, const QString &unitLabel)
void addData(const Eigen::MatrixXd &data)
QMap< qint32, float > scalingMap() const
void sampleRangeSelected(int startSample, int endSample)
void setEventsVisible(bool visible)
void scrollSpeedChanged(float factor)
void setCrosshairEnabled(bool enabled)
void annotationBoundaryMoved(int annotationIndex, bool isStartBoundary, int newSample)
void keyPressEvent(QKeyEvent *event) override
void setReferenceMarkers(const QVector< TimeRulerReferenceMark > &markers)
void setAnnotations(const QVector< ChannelRhiView::AnnotationSpan > &annotations)
void setScrollSpeedFactor(float factor)
void setZoom(double factor)
void setChannelFilter(const QStringList &names)
Channel display metadata (read-only from the renderer's perspective).
Circular-buffer Qt model exposing a rolling window of the live FIFF stream as a table.
Vertical column of channel-name labels synced with ChannelDataView row geometry.
void channelScrollRequested(int targetFirst)
QRhi-based GPU-accelerated channel time-series renderer used by the modern raw browser.
void viewResized(int newWidth, int newHeight)
void channelOffsetChanged(int firstChannel)
void samplesPerPixelChanged(float spp)
void sampleRangeSelected(int startSample, int endSample)
void annotationBoundaryMoved(int annotationIndex, bool isStartBoundary, int newSample)
void setModel(ChannelDataModel *model)
void setFirstVisibleChannel(int ch)
void sampleClicked(int sample)
void setBackgroundColor(const QColor &color)
void setScrollSample(float sample)
void cursorDataChanged(float timeSec, float amplitude, const QString &channelName, const QString &unitLabel)
void scrollSampleChanged(float sample)
void setWheelScrollsChannels(bool channelsMode)
Minimap thumbnail of the entire recording shown above the raw browser.
void scrollRequested(float targetSample)
void setModel(ChannelDataModel *model)
Horizontal time-axis ruler displayed beneath ChannelDataView.
static constexpr int kTotalH
Total widget height (px).
void setScrollSample(float sample)
static constexpr int kTimeZoneH
Height of the time-tick zone (px).
static constexpr int kStimZoneH
Height of the stimulus lane (px).
void removeReferenceMarkerRequested(int sample)
void addReferenceMarkerRequested(int sample)
void setSamplesPerPixel(float spp)