v2.0.0
Loading...
Searching...
No Matches
channellabelpanel.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "channellabelpanel.h"
18#include "channeldatamodel.h"
19
20//=============================================================================================================
21// QT INCLUDES
22//=============================================================================================================
23
24#include <QPainter>
25#include <QMouseEvent>
26#include <QHelpEvent>
27#include <QToolTip>
28#include <QtMath>
29
30#include <utility>
31
32//=============================================================================================================
33// USED NAMESPACES
34//=============================================================================================================
35
36using namespace DISPLIB;
37
38//=============================================================================================================
39// CONSTANTS
40//=============================================================================================================
41
42namespace
43{
44constexpr int kPanelWidth = 120;
45constexpr int kStripWidth = 5; // type-colour strip on left edge
46constexpr int kBadBadgeW = 28;
47constexpr int kBadBadgeH = 11;
48}
49
50//=============================================================================================================
51// DEFINE MEMBER METHODS
52//=============================================================================================================
53
55: QWidget(parent)
56{
57 setFixedWidth(kPanelWidth);
58 setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
59 setCursor(Qt::PointingHandCursor);
60 setMouseTracking(true);
61}
62
63//=============================================================================================================
64
66{
67 return QSize(kPanelWidth, 400);
68}
69
70//=============================================================================================================
71
73{
74 return QSize(kPanelWidth, 50);
75}
76
77//=============================================================================================================
78
80{
81 m_model = model;
82 update();
83}
84
85//=============================================================================================================
86
87void ChannelLabelPanel::setChannelIndices(const QVector<int>& indices)
88{
89 m_channelIndices = indices;
90 update();
91}
92
93//=============================================================================================================
94
96{
97 if (ch == m_firstVisibleChannel)
98 return;
99 m_firstVisibleChannel = ch;
100 update();
101}
102
103//=============================================================================================================
104
106{
107 if (count == m_visibleChannelCount)
108 return;
109 m_visibleChannelCount = count;
110 update();
111}
112
113//=============================================================================================================
114
116{
117 if (m_hideBadChannels == hide)
118 return;
119 m_hideBadChannels = hide;
120 update();
121}
122
123//=============================================================================================================
124
126{
127 if (m_butterflyMode == enabled)
128 return;
129 m_butterflyMode = enabled;
130 update();
131}
132
133//=============================================================================================================
134
135void ChannelLabelPanel::setVisibleSampleRange(int firstSample, int lastSample)
136{
137 m_visSampleFirst = firstSample;
138 m_visSampleLast = lastSample;
139 update();
140}
141
142//=============================================================================================================
143
145{
146 QPainter p(this);
147 p.setRenderHint(QPainter::TextAntialiasing, true);
148
149 // Background — slightly more grey than the render surface
150 p.fillRect(rect(), QColor(242, 242, 242));
151
152 if (!m_model)
153 return;
154
155 // ── Butterfly mode: show type group labels ──────────────────────
156 if (m_butterflyMode) {
157 // Collect unique types in order
158 struct TypeGroup
159 {
160 QString typeLabel;
161 QColor color;
162 int count = 0;
163 };
164 QVector<TypeGroup> groups;
165 QMap<QString, int> typeToGroup;
166
167 const QVector<int> allCh = effectiveChannelIndices();
168 for (int ch : allCh) {
169 auto info = m_model->channelInfo(ch);
170 if (typeToGroup.contains(info.typeLabel)) {
171 groups[typeToGroup[info.typeLabel]].count++;
172 } else {
173 int idx = groups.size();
174 typeToGroup[info.typeLabel] = idx;
175 TypeGroup g;
176 g.typeLabel = info.typeLabel;
177 g.color = info.color;
178 g.count = 1;
179 groups.append(g);
180 }
181 }
182
183 int nLanes = groups.size();
184 if (nLanes <= 0)
185 return;
186
187 const int pw = width();
188 const float laneH = static_cast<float>(height()) / nLanes;
189
190 QFont nameFont = font();
191 nameFont.setPointSizeF(qBound(8.0, static_cast<double>(laneH) * 0.25, 13.0));
192 nameFont.setBold(true);
193
194 QFont countFont = nameFont;
195 countFont.setPointSizeF(qBound(6.5, static_cast<double>(laneH) * 0.16, 9.0));
196 countFont.setBold(false);
197
198 for (int i = 0; i < nLanes; ++i) {
199 float yTop = i * laneH;
200
201 // Lane separator
202 if (i > 0) {
203 p.setPen(QPen(QColor(200, 200, 200), 1));
204 p.drawLine(QPointF(0, yTop), QPointF(pw, yTop));
205 }
206
207 // Type-colour strip
208 p.fillRect(QRectF(0, yTop, kStripWidth, laneH), groups[i].color);
209
210 // Type name
211 p.setFont(nameFont);
212 p.setPen(QColor(25, 25, 25));
213 QRectF nameRect(kStripWidth + 4, yTop + 1,
214 pw - kStripWidth - 8, laneH * 0.50f);
215 p.drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, groups[i].typeLabel);
216
217 // Channel count
218 p.setFont(countFont);
219 p.setPen(QColor(110, 110, 120));
220 QString countStr = QString::number(groups[i].count) + QStringLiteral(" ch");
221 QRectF countRect(kStripWidth + 4, yTop + laneH * 0.52f,
222 pw - kStripWidth - 8, laneH * 0.30f);
223 p.drawText(countRect, Qt::AlignLeft | Qt::AlignVCenter, countStr);
224 }
225 return;
226 }
227
228 // ── Normal mode: per-channel labels ─────────────────────────────
229
230 const QVector<int> displayChannels = effectiveChannelIndices();
231 const int totalCh = displayChannels.size();
232 int visibleCount = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
233 if (visibleCount <= 0)
234 return;
235
236 const int pw = width();
237 const float laneH = static_cast<float>(height()) / visibleCount;
238
239 // Whether DC removal is active (shown as a pill in the status line)
240 const bool dcActive = m_model->removeDC();
241
242 // Font sizes proportional to lane height, clamped for readability
243 QFont nameFont = font();
244 nameFont.setPointSizeF(qBound(7.0, static_cast<double>(laneH) * 0.22, 11.0));
245 nameFont.setBold(true);
246
247 QFont typeFont = nameFont;
248 typeFont.setPointSizeF(qBound(6.0, static_cast<double>(laneH) * 0.15, 8.5));
249 typeFont.setBold(false);
250
251 float yTop = 0.f;
252 for (int i = 0; i < visibleCount; ++i) {
253 int logIdx = m_firstVisibleChannel + i;
254 int ch = (logIdx >= 0 && logIdx < displayChannels.size()) ? displayChannels.at(logIdx) : -1;
255 if (ch < 0) {
256 yTop += laneH;
257 continue;
258 }
259 auto info = m_model->channelInfo(ch);
260 float yBot = yTop + laneH;
261
262 // Lane separator
263 p.setPen(QPen(QColor(200, 200, 200), 1));
264 if (i > 0)
265 p.drawLine(QPointF(0, yTop), QPointF(pw, yTop));
266
267 // Bad channel background
268 if (info.bad)
269 p.fillRect(QRectF(0, yTop, pw, laneH), QColor(255, 225, 225));
270
271 // Type-colour strip
272 p.fillRect(QRectF(0, yTop, kStripWidth, laneH), info.color);
273
274 // ── BAD badge (top-right corner) ─────────────────────────────────
275 if (info.bad) {
276 QRectF badRect(pw - kBadBadgeW - 2, yTop + 2, kBadBadgeW, kBadBadgeH);
277 p.fillRect(badRect, QColor(210, 30, 30));
278 QFont badFont = typeFont;
279 badFont.setPointSizeF(6.0);
280 badFont.setBold(true);
281 p.setFont(badFont);
282 p.setPen(Qt::white);
283 p.drawText(badRect, Qt::AlignCenter, QStringLiteral("BAD"));
284 }
285
286 // ── Channel name (bold, elided) ───────────────────────────────────
287 p.setFont(nameFont);
288 p.setPen(info.bad ? QColor(180, 20, 20) : QColor(25, 25, 25));
289 int nameBadgeGap = info.bad ? (kBadBadgeW + 4) : 4;
290 QRectF nameRect(kStripWidth + 4, yTop + 1,
291 pw - kStripWidth - nameBadgeGap - 4,
292 laneH * 0.48f);
293 QString elidedName = p.fontMetrics().elidedText(
294 info.name, Qt::ElideRight, static_cast<int>(nameRect.width()));
295 p.drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, elidedName);
296
297 // ── Status line: type · bad · dc ─────────────────────────────────
298 QString statusLine = info.typeLabel;
299 if (info.bad)
300 statusLine += QStringLiteral(" \u00B7 bad");
301 if (info.isVirtualChannel)
302 statusLine += QStringLiteral(" \u00B7 deriv");
303 if (dcActive)
304 statusLine += QStringLiteral(" \u00B7 dc");
305
306 p.setFont(typeFont);
307 p.setPen(info.bad ? QColor(190, 60, 60) : QColor(110, 110, 120));
308 QRectF statusRect(kStripWidth + 4, yTop + laneH * 0.50f,
309 pw - kStripWidth - 8, laneH * 0.28f);
310 p.drawText(statusRect, Qt::AlignLeft | Qt::AlignVCenter, statusLine);
311
312 // ── RMS level bar ─────────────────────────────────────────────────
313 float rms = 0.f;
314 if (m_visSampleFirst < m_visSampleLast)
315 rms = m_model->channelRms(ch, m_visSampleFirst, m_visSampleLast);
316 float level = (info.amplitudeMax > 0.f)
317 ? qBound(0.f, rms / info.amplitudeMax, 1.f)
318 : 0.f;
319
320 const float barY = yTop + laneH * 0.80f;
321 const float barH = qMax(2.f, laneH * 0.14f);
322 const float barX0 = kStripWidth + 4.f;
323 const float barW = pw - kStripWidth - 8.f;
324
325 p.fillRect(QRectF(barX0, barY, barW, barH), QColor(215, 215, 215));
326
327 if (level > 0.f) {
328 QColor barColor;
329 if (level < 0.5f)
330 barColor = QColor(50, 180, 80);
331 else if (level < 0.85f)
332 barColor = QColor(220, 160, 30);
333 else
334 barColor = QColor(210, 50, 40);
335 if (info.bad)
336 barColor = barColor.darker(115);
337 p.fillRect(QRectF(barX0, barY, static_cast<qreal>(barW) * level, barH), barColor);
338 }
339
340 yTop = yBot;
341 }
342}
343
344//=============================================================================================================
345
347{
348 if (event->button() == Qt::LeftButton) {
349 m_dragging = true;
350 m_dragActivated = false;
351 m_dragStartY = event->position().toPoint().y();
352 m_dragStartFirst = m_firstVisibleChannel;
353 event->accept();
354 }
355}
356
357//=============================================================================================================
358
360{
361 if (!m_dragging) {
362 event->ignore();
363 return;
364 }
365
366 int dy = event->position().toPoint().y() - m_dragStartY;
367 if (!m_dragActivated && qAbs(dy) > 4)
368 m_dragActivated = true;
369
370 if (!m_dragActivated) {
371 event->accept();
372 return;
373 }
374
375 const int totalCh = effectiveChannelIndices().size();
376 const int maxFirst = qMax(0, totalCh - m_visibleChannelCount);
377 const float laneH = (m_visibleChannelCount > 0 && height() > 0)
378 ? static_cast<float>(height()) / m_visibleChannelCount
379 : 30.f;
380
381 // Dragging DOWN means earlier channels (positive dy → lower first index)
382 int targetFirst = qBound(0,
383 m_dragStartFirst - static_cast<int>(dy / laneH),
384 maxFirst);
385
386 emit channelScrollRequested(targetFirst);
387 event->accept();
388}
389
390//=============================================================================================================
391
393{
394 if (event->button() == Qt::LeftButton) {
395 const bool wasClick = m_dragging && !m_dragActivated;
396 m_dragging = false;
397 m_dragActivated = false;
398
399 if (wasClick && m_model && m_visibleChannelCount > 0) {
400 const float laneH = static_cast<float>(height()) / m_visibleChannelCount;
401 const int row = static_cast<int>(event->position().y() / laneH);
402 const QVector<int> vis = effectiveChannelIndices();
403 const int idx = m_firstVisibleChannel + row;
404 if (idx >= 0 && idx < vis.size()) {
405 int ch = vis[idx];
406 auto info = m_model->channelInfo(ch);
407 bool newBad = !info.bad;
408 m_model->setChannelBad(ch, newBad);
409 emit channelBadToggled(ch, newBad);
410 update();
411 }
412 }
413
414 event->accept();
415 }
416}
417
418//=============================================================================================================
419
420QVector<int> ChannelLabelPanel::effectiveChannelIndices() const
421{
422 QVector<int> indices;
423
424 if (!m_model) {
425 return indices;
426 }
427
428 if (m_channelIndices.isEmpty()) {
429 indices.reserve(m_model->channelCount());
430 for (int channelIndex = 0; channelIndex < m_model->channelCount(); ++channelIndex) {
431 indices.append(channelIndex);
432 }
433 } else {
434 indices = m_channelIndices;
435 }
436
437 if (!m_hideBadChannels) {
438 return indices;
439 }
440
441 QVector<int> visibleIndices;
442 visibleIndices.reserve(indices.size());
443 for (int channelIndex : std::as_const(indices)) {
444 if (channelIndex < 0) {
445 continue;
446 }
447
448 const ChannelDisplayInfo info = m_model->channelInfo(channelIndex);
449 if (!info.bad) {
450 visibleIndices.append(channelIndex);
451 }
452 }
453
454 return visibleIndices;
455}
456
458{
459 if (e->type() == QEvent::ToolTip && m_model) {
460 auto* he = static_cast<QHelpEvent*>(e);
461 const QVector<int> displayChannels = effectiveChannelIndices();
462 int totalCh = displayChannels.size();
463 int visibleCount = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
464 if (visibleCount > 0) {
465 const float laneH = static_cast<float>(height()) / visibleCount;
466 const int i = qBound(0, static_cast<int>(he->pos().y() / laneH), visibleCount - 1);
467 if (i >= 0 && i < visibleCount) {
468 const int logIdx = m_firstVisibleChannel + i;
469 const int ch = (logIdx >= 0 && logIdx < displayChannels.size())
470 ? displayChannels.at(logIdx)
471 : -1;
472 if (ch < 0)
473 return QWidget::event(e);
474 auto info = m_model->channelInfo(ch);
475 float rms = (m_visSampleFirst < m_visSampleLast)
476 ? m_model->channelRms(ch, m_visSampleFirst, m_visSampleLast)
477 : 0.f;
478 float level = (info.amplitudeMax > 0.f)
479 ? qBound(0.f, rms / info.amplitudeMax * 100.f, 100.f)
480 : 0.f;
481 QString tip = QStringLiteral("<b>%1</b><br>"
482 "Type: %2<br>"
483 "Scale: %3<br>"
484 "Level: %4 %<br>"
485 "Bad: %5<br>"
486 "Virtual: %6")
487 .arg(info.name)
488 .arg(info.typeLabel)
489 .arg(info.amplitudeMax, 0, 'e', 2)
490 .arg(static_cast<int>(level))
491 .arg(info.bad ? QStringLiteral("yes") : QStringLiteral("no"))
492 .arg(info.isVirtualChannel ? QStringLiteral("yes") : QStringLiteral("no"));
493 QToolTip::showText(he->globalPos(), tip, this);
494 return true;
495 }
496 }
497 }
498 return QWidget::event(e);
499}
Circular-buffer Qt model exposing a rolling window of the live FIFF stream as a table.
Vertical column of channel-name labels synced with DISPLIB::ChannelDataView's row geometry.
2-D display widgets and visualisation helpers (charts, topography, colour maps).
Circular-buffer Qt model exposing a rolling window of the live FIFF stream as a table.
void channelScrollRequested(int targetFirst)
void setVisibleSampleRange(int firstSample, int lastSample)
void setChannelIndices(const QVector< int > &indices)
void mouseReleaseEvent(QMouseEvent *event) override
QSize sizeHint() const override
void channelBadToggled(int channelIndex, bool bad)
void mouseMoveEvent(QMouseEvent *event) override
bool event(QEvent *e) override
void paintEvent(QPaintEvent *event) override
QSize minimumSizeHint() const override
void setButterflyMode(bool enabled)
void mousePressEvent(QMouseEvent *event) override
ChannelLabelPanel(QWidget *parent=nullptr)
void setModel(ChannelDataModel *model)