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