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