1 // A simple(?) set of sliders related to controlling multimedia players.
2 #include <QPainter>
3 #include <QOpenGLContext>
4 #include <QTimer>
5 #include <cmath>
6 #include "drawnslider.h"
7
8
9
10 // dr: drawrect electric floating boogaloo
11 // As the doc says, QPainter draws the right and bottom edges one pixel below
12 // and to the right of the expected location. So provide a helper function
13 // that draws an adjusted rect to keep the main math clean. As it turns out,
14 // we can avoid a whole host of bugs by using doubles rather than ints.
dr(QPainter * p,QRectF r)15 static void dr(QPainter *p, QRectF r) {
16 QRectF r2(r.left() + 0.5, r.top() + 0.5, r.width() - 1, r.height() - 1);
17 p->drawRect(r2);
18 }
19
20
21
DrawnSlider(QWidget * parent,QSize handle,QSize margin)22 DrawnSlider::DrawnSlider(QWidget *parent, QSize handle, QSize margin) :
23 QWidget(parent)
24 {
25 setFocusPolicy(Qt::NoFocus);
26 setMouseTracking(true);
27 setSliderGeometry(handle.width(), handle.height(),
28 margin.width(), margin.height());
29 }
30
setValue(double v)31 void DrawnSlider::setValue(double v)
32 {
33 vValue = qBound(vMinimum, v, vMaximum);
34 xPosition = valueToX(vValue);
35 update();
36 }
37
setMaximum(double v)38 void DrawnSlider::setMaximum(double v)
39 {
40 vMaximum = v;
41 if (vValue > v)
42 setValue(v);
43 else
44 update();
45 }
46
setMinimum(double v)47 void DrawnSlider::setMinimum(double v)
48 {
49 vMinimum = v;
50 if (vValue < v)
51 setValue(v);
52 else
53 update();
54 }
55
value()56 double DrawnSlider::value()
57 {
58 return vValue;
59 }
60
maximum()61 double DrawnSlider::maximum()
62 {
63 return vMaximum;
64 }
65
minimum()66 double DrawnSlider::minimum()
67 {
68 return vMinimum;
69 }
70
setSliderGeometry(int handleWidth,int handleHeight,int marginX,int marginY)71 void DrawnSlider::setSliderGeometry(int handleWidth, int handleHeight,
72 int marginX, int marginY)
73 {
74 this->handleWidth = handleWidth;
75 this->handleHeight = handleHeight;
76 this->marginX = marginX;
77 this->marginY = marginY;
78 setMinimumHeight(handleHeight);
79 setMaximumHeight(handleHeight);
80 }
81
handleHover(double x)82 void DrawnSlider::handleHover(double x)
83 {
84 // do nothing by default
85 (void)x;
86 }
87
valueToX(double value)88 double DrawnSlider::valueToX(double value)
89 {
90 double stride = sliderArea.right() - sliderArea.left();
91 double x = sliderArea.left() + (((value-minimum()) * stride)
92 / std::max(1.0, maximum() - minimum()));
93 return qBound(x, sliderArea.left(), sliderArea.right());
94 }
95
xToValue(double x)96 double DrawnSlider::xToValue(double x)
97 {
98 double stride = std::max(1.0, sliderArea.right() - sliderArea.left());
99 double val = ((x-sliderArea.left()) * (maximum() - minimum()))
100 / stride;
101 return qBound(val, minimum(), maximum());
102 }
103
paintEvent(QPaintEvent * event)104 void DrawnSlider::paintEvent(QPaintEvent *event)
105 {
106 Q_UNUSED(event)
107 QPalette pal;
108 pal = reinterpret_cast<QWidget*>(parentWidget())->palette();
109 grooveBorder = pal.color(QPalette::Normal, QPalette::Shadow);
110 grooveFill = pal.color(QPalette::Normal, QPalette::Base);
111 handleBorder = pal.color(QPalette::Normal, QPalette::Dark);
112 handleFill = pal.color(QPalette::Normal, QPalette::Button);
113 bgColor = pal.color(QPalette::Normal, QPalette::Window);
114 loopColor = pal.color(QPalette::Normal, QPalette::Highlight);
115 markColor = pal.color(QPalette::Normal, QPalette::Shadow);
116
117 if (redrawPics) {
118 makeBackground();
119 makeHandle();
120 redrawPics = false;
121 }
122
123 QPainter p(this);
124 int pr = devicePixelRatio();
125 p.scale(1.0/pr, 1.0/pr);
126 p.setRenderHint(QPainter::Antialiasing);
127 p.setRenderHint(QPainter::SmoothPixmapTransform);
128 p.drawImage(0, 0, backgroundPic);
129 p.setOpacity(isEnabled() ? 1.0 : 0.333);
130
131 if (minimum() != maximum()) {
132 double px;
133 double x = isDragging ? xPosition : valueToX(value());
134 x -= handleWidth/2.0;
135 x *= pr;
136 int index = int(modf(x, &px) * 16.0)&15;
137 p.drawImage(QPointF(px - 0.5, (height() - handleHeight)/2), handlePics[index]);
138 }
139 }
140
resizeEvent(QResizeEvent * event)141 void DrawnSlider::resizeEvent(QResizeEvent *event)
142 {
143 Q_UNUSED(event)
144 /*
145 MEDIA SLIDER CASE
146
147 <---- SLIDER AREA
148 ^ +------------------------
149 |
150 |< marginX>
151 |
152 +------------------+----- ^ ^
153 |
154 | | marginY
155 |
156 | +--------------- v
157 | |
158 height | | GROOVE hH
159 | |
160 | +---------------
161 | DRAWN
162 | |
163 | AREA
164 +------------------+----- v
165 |
166 |
167 |
168 v +------------------------
169
170
171 VOLUME SLIDER CASE
172
173 <---- SLIDER AREA
174 ^ +------------------------
175 |
176 |< hW/2 >
177 |
178 +------------------+----- ^
179 |
180 | |
181 | DRAWN AREA
182 | |
183 |
184 height | + ---- hH
185 | ----
186 | ---- |
187 | ----
188 | ---- |
189 |----
190 +------------------+---- ^ v
191 |
192 | padding
193 |
194 v +----------------------- v
195
196 Therefore, "drawn area" is actually the control area, minus not needed
197 padding.
198
199 Therefore, "groove area" is actually the drawn area, minus any margin.
200
201 Therefore, "slider area" is actually the drawn area, minus the handle
202 area.
203
204 Therefore, a volume slider ignores the groove area as it has no groove.
205 However, it should provide 'margins' equal to half its handle size, as
206 these relate to where the middle of the handle is.
207 */
208
209 grooveArea = QRectF(0, 0, width(), height());
210 drawnArea = grooveArea;
211 grooveArea.adjust(marginX, marginY, -marginX, -marginY);
212 sliderArea = grooveArea;
213 sliderArea.adjust(0, 0, -(handleWidth&1), 0);
214 redrawPics = true;
215 }
216
mousePressEvent(QMouseEvent * ev)217 void DrawnSlider::mousePressEvent(QMouseEvent *ev)
218 {
219 if (ev->button() == Qt::LeftButton) {
220 isDragging = true;
221 xPosition = ev->localPos().x();
222 setValue(xToValue(ev->localPos().x()));
223 emit sliderMoved(value());
224 }
225 }
226
mouseReleaseEvent(QMouseEvent * ev)227 void DrawnSlider::mouseReleaseEvent(QMouseEvent *ev)
228 {
229 if (isDragging && ev->button() == Qt::LeftButton) {
230 isDragging = false;
231 update();
232 }
233 }
234
mouseMoveEvent(QMouseEvent * ev)235 void DrawnSlider::mouseMoveEvent(QMouseEvent *ev)
236 {
237 if (isDragging && ev->buttons() & Qt::LeftButton) {
238 double mouseValue = xToValue(ev->localPos().x());
239 if (value() != mouseValue) {
240 setValue(mouseValue);
241 emit sliderMoved(value());
242 }
243 }
244 handleHover(ev->localPos().x());
245 // Forward mouse move events also to the parent widget
246 ev->ignore();
247 }
248
MediaSlider(QWidget * parent)249 MediaSlider::MediaSlider(QWidget *parent) :
250 DrawnSlider(parent, QSize(11, 12), QSize(5, 3))
251 {
252 }
253
clearTicks()254 void MediaSlider::clearTicks()
255 {
256 ticks.clear();
257 vLoopA = vLoopB = -1;
258 loopArea = { -1, -1, 0, 0 };
259 redrawPics = true;
260 }
261
setTick(double value,QString text)262 void MediaSlider::setTick(double value, QString text)
263 {
264 ticks.insert(value, text);
265 }
266
setLoopA(double a)267 void MediaSlider::setLoopA(double a)
268 {
269 vLoopA = a; updateLoopArea();
270 }
271
setLoopB(double b)272 void MediaSlider::setLoopB(double b)
273 {
274 vLoopB = b; updateLoopArea();
275 }
276
loopA()277 double MediaSlider::loopA()
278 {
279 return vLoopA;
280 }
281
loopB()282 double MediaSlider::loopB() {
283 return vLoopB;
284 }
285
isLoopEmpty()286 bool MediaSlider::isLoopEmpty()
287 {
288 return vLoopA < 0 || vLoopB < 0;
289 }
290
resizeEvent(QResizeEvent * event)291 void MediaSlider::resizeEvent(QResizeEvent *event)
292 {
293 DrawnSlider::resizeEvent(event);
294 updateLoopArea();
295 }
296
makeBackground()297 void MediaSlider::makeBackground()
298 {
299 int pr = devicePixelRatio();
300 int pw = width() * pr;
301 int ph = width() * pr;
302 backgroundPic = QImage(pw, ph, QImage::Format_RGB32);
303 backgroundPic.fill(bgColor);
304 QPainter p(&backgroundPic);
305 p.scale(pr, pr);
306
307 // Draw inside area
308 p.setPen(Qt::NoPen);
309 p.setBrush(grooveFill);
310 dr(&p, grooveArea);
311
312 // Draw any highlighted area
313 p.setPen(loopColor);
314 p.setBrush(loopColor);
315 if (vLoopA >= 0 && vLoopB >= 0) {
316 p.setBrush(loopColor);
317 dr(&p, loopArea);
318 } else if (vLoopA >= 0) {
319 double pos = valueToX(vLoopA);
320 p.drawLine(QPointF(pos + 0.5, grooveArea.top() + 1.5),
321 QPointF(pos + 0.5, grooveArea.bottom() - 1.5));
322 } else if (vLoopB >= 0) {
323 double pos = valueToX(vLoopB);
324 p.drawLine(QPointF(pos + 0.5, grooveArea.top() + 1.5),
325 QPointF(pos + 0.5, grooveArea.bottom() - 1.5));
326
327 }
328
329 // Draw outside groove
330 p.setPen(grooveBorder);
331 p.setBrush(Qt::NoBrush);
332 dr(&p, grooveArea);
333
334 // Draw chapter marks
335 for (auto i = ticks.constBegin(); i != ticks.constEnd(); i++) {
336 double pos = valueToX(i.key());
337 // Don't draw over the edge of the groove twice when disabled, so the
338 // affected groove sides don't appear dark.
339 if (isEnabled() || (pos > grooveArea.left() + 1.0 &&
340 pos < grooveArea.right() - 1.0)) {
341 p.drawLine(QPointF(pos + 0.5, grooveArea.top() + 0.5),
342 QPointF(pos + 0.5, grooveArea.bottom() - 0.5));
343 }
344 }
345 }
346
makeHandle()347 void MediaSlider::makeHandle()
348 {
349 int pr = devicePixelRatio();
350 int pw = handleWidth * pr;
351 int ph = handleHeight * pr;
352
353 for (int i = 0; i < 16; i++) {
354 QImage handlePic(pw+1, ph, QImage::Format_ARGB32_Premultiplied);
355 handlePic.fill(0);
356 QPainter p(&handlePic);
357 p.setRenderHint(QPainter::Antialiasing);
358 p.setTransform(QTransform().translate(i/16.0,0).scale(pr,pr));
359 QRectF slider(0, 0, handleWidth, handleHeight);
360 slider.adjust(1.5, 1.5, -1.5, -1.5);
361 p.setBrush(Qt::NoBrush);
362 p.setPen(QPen(handleBorder, 4, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin));
363 dr(&p, slider);
364 p.setPen(QPen(handleFill, 2, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin));
365 dr(&p, slider);
366 handlePics[i] = handlePic;
367 }
368 }
369
enterEvent(QEvent * event)370 void MediaSlider::enterEvent(QEvent *event)
371 {
372 Q_UNUSED(event)
373 emit hoverBegin();
374 }
375
leaveEvent(QEvent * event)376 void MediaSlider::leaveEvent(QEvent *event)
377 {
378 Q_UNUSED(event)
379 emit hoverEnd();
380 }
381
handleHover(double x)382 void MediaSlider::handleHover(double x)
383 {
384 double valueOfX = xToValue(x);
385
386 emit hoverValue(valueOfX, valueToTickText(valueOfX), x);
387 }
388
updateLoopArea()389 void MediaSlider::updateLoopArea()
390 {
391 double left = valueToX(vLoopA);
392 double right = valueToX(vLoopB);
393 loopArea = {left, grooveArea.top() + 1, right - left, grooveArea.height() - 2};
394 update();
395 }
396
valueToTickText(double value)397 QString MediaSlider::valueToTickText(double value)
398 {
399 QString last_text;
400
401 double last_tick = -1;
402 for (auto i = ticks.constBegin(); i != ticks.constEnd(); i++) {
403 last_tick = i.key();
404 if (last_tick > value)
405 break;
406 last_text = i.value();
407 }
408 return last_text;
409 }
410
411
412
VolumeSlider(QWidget * parent)413 VolumeSlider::VolumeSlider(QWidget *parent) :
414 DrawnSlider(parent, QSize(10, 20), QSize(5, 10))
415 {
416 }
417
makeBackground()418 void VolumeSlider::makeBackground()
419 {
420 int pr = devicePixelRatio();
421 int pw = int(drawnArea.width() * pr);
422 int ph = int(drawnArea.height() * pr);
423 backgroundPic = QImage(pw, ph, QImage::Format_RGB32);
424 backgroundPic.fill(bgColor);
425 QPainter p(&backgroundPic);
426 p.scale(pr, pr);
427
428 double x1 = drawnArea.left() + 0.5;
429 double y1 = drawnArea.top() + 0.5;
430 double x2 = drawnArea.width() - 0.5;
431 double y2 = drawnArea.height() - 0.5;
432 QPointF groove[] = { QPointF(x1, y2), QPointF(x2, y2), QPointF(x2, y1) };
433
434 p.setRenderHint(QPainter::Antialiasing);
435 p.setPen(grooveBorder);
436 p.setBrush(grooveFill);
437 p.drawConvexPolygon(groove, 3);
438 }
439
makeHandle()440 void VolumeSlider::makeHandle()
441 {
442 int pr = devicePixelRatio();
443 int pw = handleWidth * pr;
444 int ph = handleHeight * pr;
445 for (int i = 0; i < 16; i++) {
446 QImage handlePic(pw+1, ph, QImage::Format_ARGB32);
447 handlePic.fill(0);
448 QPainter p(&handlePic);
449 p.setRenderHint(QPainter::Antialiasing);
450 p.scale(pr, pr);
451 p.translate(i/16.0,0);
452 p.setBrush(handleFill);
453 p.setPen(handleBorder);
454 dr(&p, QRectF(0,0,handleWidth,handleHeight));
455 handlePics[i] = handlePic;
456 }
457 }
458