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