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