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