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