v2.0.0
Loading...
Searching...
No Matches
timerulerwidget.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "timerulerwidget.h"
18
19//=============================================================================================================
20// QT INCLUDES
21//=============================================================================================================
22
23#include <QContextMenuEvent>
24#include <QPainter>
25#include <QPaintEvent>
26#include <QFontDatabase>
27#include <QMenu>
28#include <QtMath>
29#include <cmath>
30#include <limits>
31
32//=============================================================================================================
33// USED NAMESPACES
34//=============================================================================================================
35
36using namespace DISPLIB;
37
38//=============================================================================================================
39// CONSTANTS
40//=============================================================================================================
41
42namespace
43{
44// Same nice-interval table used by the render grid — guarantees perfect alignment.
45static const double kNiceIntervals[] = {0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0};
46constexpr double kMinMajorPx = 80.0; // minimum px spacing between major ticks
47constexpr int kMajorH = 10; // tick mark height in px (downward from bottom border)
48constexpr int kMinorH = 5;
49constexpr int kLabelGap = 3; // gap between label bottom and tick top
50} // namespace
51
52//=============================================================================================================
53// DEFINE MEMBER METHODS
54//=============================================================================================================
55
57: QWidget(parent)
58{
59 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
60 setFixedHeight(kTotalH);
61}
62
63//=============================================================================================================
64
65void TimeRulerWidget::setSfreq(double sfreq)
66{
67 m_sfreq = (sfreq > 0.0) ? sfreq : 1.0;
68 update();
69}
70
71//=============================================================================================================
72
73void TimeRulerWidget::setFirstFileSample(int firstFileSample)
74{
75 m_firstFileSample = firstFileSample;
76 update();
77}
78
79//=============================================================================================================
80
81void TimeRulerWidget::setEvents(const QVector<TimeRulerEventMark>& events)
82{
83 m_events = events;
84 update();
85}
86
87//=============================================================================================================
88
89void TimeRulerWidget::setReferenceMarkers(const QVector<TimeRulerReferenceMark>& markers)
90{
91 m_referenceMarkers = markers;
92 update();
93}
94
95//=============================================================================================================
96
98{
99 // Exact: a relative fuzzy compare ignores steps below 1e-5 of the position (10 samples at 10^6)
100 if (m_scrollSample == sample)
101 return;
102 m_scrollSample = sample;
103 update();
104}
105
106//=============================================================================================================
107
109{
110 if (qFuzzyCompare(m_spp, spp))
111 return;
112 m_spp = spp;
113 update();
114}
115
116//=============================================================================================================
117
118QString TimeRulerWidget::formatTime(double seconds)
119{
120 if (seconds < 0.0)
121 seconds = 0.0;
122
123 if (seconds >= 3600.0) {
124 int h = static_cast<int>(seconds) / 3600;
125 int m = (static_cast<int>(seconds) % 3600) / 60;
126 int s = static_cast<int>(seconds) % 60;
127 return QString("%1:%2:%3")
128 .arg(h)
129 .arg(m, 2, 10, QChar('0'))
130 .arg(s, 2, 10, QChar('0'));
131 } else if (seconds >= 60.0) {
132 int m = static_cast<int>(seconds) / 60;
133 double s = seconds - m * 60.0;
134 return QString("%1:%2").arg(m).arg(s, 4, 'f', 1, QChar('0'));
135 } else if (seconds >= 10.0) {
136 return QString("%1 s").arg(static_cast<int>(std::round(seconds)));
137 } else if (seconds >= 1.0) {
138 return QString("%1 s").arg(seconds, 0, 'f', 1);
139 } else if (seconds >= 0.1) {
140 return QString("%1 s").arg(seconds, 0, 'f', 2);
141 } else if (seconds >= 0.001) {
142 return QString("%1 ms").arg(seconds * 1e3, 0, 'f', 1);
143 } else {
144 return QString("%1 ms").arg(seconds * 1e3, 0, 'f', 2);
145 }
146}
147
148//=============================================================================================================
149
151{
152 m_useClockTime = !m_useClockTime;
153 update();
154}
155
156//=============================================================================================================
157
159{
160 if (m_useClockTime == useClock)
161 return;
162 m_useClockTime = useClock;
163 update();
164}
165
166//=============================================================================================================
167
168int TimeRulerWidget::sampleAtX(int x) const
169{
170 const int clampedX = qBound(0, x, qMax(0, width() - 1));
171 const double sample = static_cast<double>(m_scrollSample) + static_cast<double>(clampedX) * static_cast<double>(m_spp);
172 return qRound(sample);
173}
174
175//=============================================================================================================
176
177int TimeRulerWidget::nearestReferenceMarkerIndex(int sample, int tolerancePixels) const
178{
179 if (m_referenceMarkers.isEmpty() || m_spp <= 0.f) {
180 return -1;
181 }
182
183 int nearestIndex = -1;
184 double nearestDistance = std::numeric_limits<double>::max();
185 const double toleranceSamples = static_cast<double>(tolerancePixels) * static_cast<double>(m_spp);
186
187 for (int i = 0; i < m_referenceMarkers.size(); ++i) {
188 const double distance = qAbs(static_cast<double>(m_referenceMarkers.at(i).sample - sample));
189 if (distance <= toleranceSamples && distance < nearestDistance) {
190 nearestDistance = distance;
191 nearestIndex = i;
192 }
193 }
194
195 return nearestIndex;
196}
197
198//=============================================================================================================
199
200void TimeRulerWidget::paintEvent(QPaintEvent* /*event*/)
201{
202 const int W = width();
203 const int H = height(); // == kTotalH == kStimZoneH + kTimeZoneH
204 const double spp = static_cast<double>(m_spp);
205
206 if (W <= 0 || spp <= 0.0 || m_sfreq <= 0.0)
207 return;
208
209 QPainter p(this);
210 p.setRenderHint(QPainter::Antialiasing, false);
211 p.setRenderHint(QPainter::TextAntialiasing, true);
212
213 // Layout (top → bottom):
214 // [0 .. kTimeZoneH) — time zone: tick marks + labels
215 // [kTimeZoneH .. kTotalH) — stim zone: event chips
216
217 // ── Time zone background (top kTimeZoneH px) ──────────────────────
218 p.fillRect(QRect(0, 0, W, kTimeZoneH), QColor(245, 245, 247));
219
220 // ── Stim lane background (bottom kStimZoneH px) ───────────────────
221 p.fillRect(QRect(0, kTimeZoneH, W, kStimZoneH), QColor(238, 238, 246));
222
223 // Separator line between the two zones
224 p.setPen(QPen(QColor(190, 190, 205), 1));
225 p.drawLine(0, kTimeZoneH, W, kTimeZoneH);
226
227 // Bottom border
228 p.setPen(QPen(QColor(185, 185, 195), 1));
229 p.drawLine(0, H - 1, W, H - 1);
230
231 // ── Persistent sample markers ────────────────────────────────────
232 if (!m_referenceMarkers.isEmpty()) {
233 QFont markerFont = p.font();
234 markerFont.setPixelSize(9);
235 markerFont.setBold(true);
236 p.setFont(markerFont);
237
238 constexpr int kMarkerChipH = 12;
239 constexpr int kMarkerPadX = 5;
240 const int markerChipY = 2;
241
242 for (const TimeRulerReferenceMark& marker : m_referenceMarkers) {
243 const float xF = (static_cast<float>(marker.sample) - m_scrollSample) / static_cast<float>(spp);
244 if (xF < -2.f || xF > W + 2.f) {
245 continue;
246 }
247
248 const int xi = static_cast<int>(std::round(xF));
249 QColor markerColor = marker.color;
250 markerColor.setAlpha(210);
251 p.setPen(QPen(markerColor, 1));
252 p.drawLine(xi, 0, xi, H - 1);
253
254 const QString label = marker.label.isEmpty()
255 ? QString::number(marker.sample)
256 : marker.label;
257 const int chipW = qMax(20, p.fontMetrics().horizontalAdvance(label) + 2 * kMarkerPadX);
258 QRect chipRect(xi - chipW / 2, markerChipY, chipW, kMarkerChipH);
259 chipRect.moveLeft(qBound(2, chipRect.left(), qMax(2, W - chipRect.width() - 2)));
260
261 QColor fillColor = marker.color;
262 fillColor.setAlpha(220);
263 p.fillRect(chipRect, fillColor);
264 p.setPen(Qt::white);
265 p.drawText(chipRect, Qt::AlignCenter, label);
266 }
267 }
268
269 // ── Stim event chips ─────────────────────────────────────────────
270 if (!m_events.isEmpty()) {
271 QFont evFont;
272 evFont.setPixelSize(9);
273 evFont.setBold(true);
274 p.setFont(evFont);
275
276 constexpr int kChipW = 26;
277 constexpr int kChipH = 11;
278 // Chips sit centred vertically in the stim zone (bottom strip)
279 constexpr int kChipY = kTimeZoneH + (kStimZoneH - kChipH) / 2;
280
281 // Track the rightmost x edge drawn so far. When a chip would overlap,
282 // we skip it entirely (only the tick mark is kept). Events must be
283 // sorted by ascending sample for this to work correctly.
284 int lastChipRight = -kChipW;
285
286 for (const TimeRulerEventMark& ev : m_events) {
287 float xF = (static_cast<float>(ev.sample) - m_scrollSample) / static_cast<float>(spp);
288 if (xF < -2.f || xF > W + 2.f)
289 continue;
290 int ix = static_cast<int>(xF);
291
292 // Tick mark at the top of the stim zone (bridging separator into stim area)
293 QColor col = ev.color;
294 col.setAlpha(200);
295 p.setPen(QPen(col, 1));
296 p.drawLine(ix, kTimeZoneH, ix, kTimeZoneH + 3);
297
298 // Chip: only draw if it fits without overlapping the previous chip.
299 // When events are too close, we drop the chip (keeping only the tick mark)
300 // so labels never pile up at the right edge.
301 int chipX = ix - kChipW / 2;
302 chipX = qMax(0, chipX); // don't go off left edge
303 if (chipX + kChipW > W)
304 continue; // would spill off right edge — skip entirely
305 if (chipX < lastChipRight + 2)
306 continue; // would overlap previous chip — skip
307
308 QRectF chip(chipX, kChipY, kChipW, kChipH);
309 QColor fill = ev.color;
310 fill.setAlpha(150);
311 p.fillRect(chip, fill);
312 p.setPen(Qt::white);
313 QString lbl = ev.label.isEmpty() ? QStringLiteral("?") : ev.label;
314 p.drawText(chip, Qt::AlignCenter, lbl);
315
316 lastChipRight = chipX + kChipW;
317 }
318 }
319
320 // ── Choose tick interval ──────────────────────────────────────────
321 const double pxPerSec = m_sfreq / spp;
322 double tickIntervalS = kNiceIntervals[0];
323 for (double iv : kNiceIntervals) {
324 tickIntervalS = iv;
325 if (iv * pxPerSec >= kMinMajorPx)
326 break;
327 }
328
329 const double tickSamples = tickIntervalS * m_sfreq;
330 const double minorSamples = tickSamples / 5.0;
331 const double origin = static_cast<double>(m_firstFileSample);
332
333 // ── Font ─────────────────────────────────────────────────────────
334 QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
335 font.setPointSizeF(8.0);
336 p.setFont(font);
337 const QFontMetrics fm(font);
338
339 // ── Minor ticks (bottom of time zone, pointing down) ─────────────
340 {
341 double firstMinorS = std::ceil((m_scrollSample - origin - minorSamples) / minorSamples) * minorSamples + origin;
342 p.setPen(QPen(QColor(165, 165, 175), 1));
343 for (double s = firstMinorS;; s += minorSamples) {
344 double xPx = (s - m_scrollSample) / spp;
345 if (xPx > W + 2)
346 break;
347 if (xPx < -2)
348 continue;
349 int xi = static_cast<int>(std::round(xPx));
350 p.drawLine(xi, kTimeZoneH - 1 - kMinorH, xi, kTimeZoneH - 2);
351 }
352 }
353
354 // ── Major ticks + labels ─────────────────────────────────────────
355 {
356 double firstMajorS = std::ceil((m_scrollSample - origin - tickSamples) / tickSamples) * tickSamples + origin;
357 for (double s = firstMajorS;; s += tickSamples) {
358 double xPx = (s - m_scrollSample) / spp;
359 if (xPx > W + 2)
360 break;
361 if (xPx < -2)
362 continue;
363
364 int xi = static_cast<int>(std::round(xPx));
365
366 p.setPen(QPen(QColor(100, 100, 115), 1));
367 p.drawLine(xi, kTimeZoneH - 1 - kMajorH, xi, kTimeZoneH - 2);
368
369 double elapsedSec = (s - origin) / m_sfreq;
370 if (elapsedSec >= -tickIntervalS * 0.5) {
371 QString label;
372 if (m_useClockTime && elapsedSec >= 0.0) {
373 int totalMs = static_cast<int>(elapsedSec * 1000.0 + 0.5);
374 int m = totalMs / 60000;
375 int sec = (totalMs % 60000) / 1000;
376 int ms = totalMs % 1000;
377 label = QString("%1:%2.%3")
378 .arg(m, 2, 10, QChar('0'))
379 .arg(sec, 2, 10, QChar('0'))
380 .arg(ms, 3, 10, QChar('0'));
381 } else {
382 label = formatTime(elapsedSec);
383 }
384 const int lw = fm.horizontalAdvance(label);
385
386 int lx = xi - lw / 2;
387 lx = qBound(2, lx, W - lw - 2);
388 int ly = kTimeZoneH - 1 - kMajorH - kLabelGap;
389
390 p.setPen(QColor(65, 65, 80));
391 p.drawText(lx, ly, label);
392 }
393 }
394 }
395}
396
397//=============================================================================================================
398
399void TimeRulerWidget::contextMenuEvent(QContextMenuEvent* event)
400{
401 if (m_sfreq <= 0.0 || m_spp <= 0.0f) {
402 QWidget::contextMenuEvent(event);
403 return;
404 }
405
406 const int sample = sampleAtX(event->pos().x());
407 const int nearbyMarkerIndex = nearestReferenceMarkerIndex(sample);
408
409 QMenu menu(this);
410 QAction* addMarkerAction = menu.addAction(tr("Add Marker Here"));
411 QAction* removeMarkerAction = nullptr;
412 QAction* clearMarkersAction = nullptr;
413
414 if (nearbyMarkerIndex >= 0) {
415 removeMarkerAction = menu.addAction(tr("Remove Nearest Marker"));
416 }
417
418 if (!m_referenceMarkers.isEmpty()) {
419 menu.addSeparator();
420 clearMarkersAction = menu.addAction(tr("Clear All Markers"));
421 }
422
423 QAction* selectedAction = menu.exec(event->globalPos());
424 if (!selectedAction) {
425 return;
426 }
427
428 if (selectedAction == addMarkerAction) {
429 emit addReferenceMarkerRequested(sample);
430 } else if (selectedAction == removeMarkerAction) {
432 } else if (selectedAction == clearMarkersAction) {
434 }
435}
Horizontal time-axis ruler displayed beneath DISPLIB::ChannelDataView with sample / second ticks.
2-D display widgets and visualisation helpers (charts, topography, colour maps).
Lightweight event mark passed to TimeRulerWidget for the stim lane.
Lightweight reference/sample marker passed to TimeRulerWidget.
static constexpr int kTotalH
Total widget height (px).
void setClockTimeFormat(bool useClock)
void setSfreq(double sfreq)
TimeRulerWidget(QWidget *parent=nullptr)
void contextMenuEvent(QContextMenuEvent *event) override
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 setFirstFileSample(int firstFileSample)
void removeReferenceMarkerRequested(int sample)
void addReferenceMarkerRequested(int sample)
void setReferenceMarkers(const QVector< TimeRulerReferenceMark > &markers)
void setSamplesPerPixel(float spp)
void setEvents(const QVector< TimeRulerEventMark > &events)
void paintEvent(QPaintEvent *event) override