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