|
1 /* |
|
2 * RangedSlider.cpp is part bmsapp. |
|
3 * |
|
4 * Original written for Brewtarget, and is Copyright the following |
|
5 * authors 2009-2020 |
|
6 * - Matt Young <mfsy@yahoo.com> |
|
7 * - Mik Firestone <mikfire@gmail.com> |
|
8 * - Philip G. Lee <rocketman768@gmail.com> |
|
9 * |
|
10 * bmsapp is free software: you can redistribute it and/or modify |
|
11 * it under the terms of the GNU General Public License as published by |
|
12 * the Free Software Foundation, either version 3 of the License, or |
|
13 * (at your option) any later version. |
|
14 * |
|
15 * bmsapp is distributed in the hope that it will be useful, |
|
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
18 * GNU General Public License for more details. |
|
19 * |
|
20 * You should have received a copy of the GNU General Public License |
|
21 * along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
22 */ |
|
23 |
|
24 #include "RangedSlider.h" |
|
25 //#include "brewtarget.h" |
|
26 #include <QPaintEvent> |
|
27 #include <QPainter> |
|
28 #include <QColor> |
|
29 #include <QPalette> |
|
30 #include <QApplication> |
|
31 #include <QRectF> |
|
32 #include <QFont> |
|
33 #include <QFontMetrics> |
|
34 #include <QMouseEvent> |
|
35 #include <QLabel> |
|
36 #include <QToolTip> |
|
37 #include <QLinearGradient> |
|
38 #include <QPainterPath> |
|
39 |
|
40 #include <QDebug> |
|
41 |
|
42 RangedSlider::RangedSlider(QWidget* parent) |
|
43 : QWidget(parent), |
|
44 _min(0.0), |
|
45 _max(1.0), |
|
46 _prefMin(0.25), |
|
47 _prefMax(0.75), |
|
48 _val(0.5), |
|
49 _valText("0.500"), |
|
50 _prec(3), |
|
51 _tickInterval(0), |
|
52 _secondaryTicks(1), |
|
53 _tooltipText(""), |
|
54 _bgBrush(QColor(255,255,255)), |
|
55 _prefRangeBrush(QColor(0,0,0)), |
|
56 _prefRangePen(Qt::NoPen), |
|
57 _markerBrush(QColor(255,255,255)), |
|
58 _markerTextIsValue(false), |
|
59 valueTextFont("Arial", |
|
60 14, // QFonts are specified in point size, so the hard-coded number is fine here. |
|
61 QFont::Black), // Note that QFont::Black is a weight (more bold than ExtraBold), not a colour. |
|
62 indicatorTextFont("Arial", |
|
63 10, |
|
64 QFont::Normal) // Previously we just did the indicator text in 'default' font |
|
65 { |
|
66 // Ensure this->heightInPixels is properly initialised |
|
67 this->recalculateHeightInPixels(); |
|
68 |
|
69 // In principle we want to set our min/max sizes etc here. However, if, say, a maximumSize property has been set |
|
70 // for this object in a Designer UI File (eg ui/mainWindow.ui) then that setting will override this one, because it |
|
71 // will be applied later (in fact pretty much straight after this constructor returns). So we also make this call |
|
72 // inside setValue(), which will be invoked _after_ the setter calls that were auto-generated from the Designer UI |
|
73 // File. |
|
74 this->setSizes(); |
|
75 |
|
76 // Generate mouse move events whenever mouse movers over widget. |
|
77 this->setMouseTracking(true); |
|
78 |
|
79 this->repaint(); |
|
80 } |
|
81 |
|
82 void RangedSlider::setPreferredRange( double min, double max ) |
|
83 { |
|
84 _prefMin = min; |
|
85 _prefMax = max; |
|
86 |
|
87 // Only show tooltips if the range has nonzero size. |
|
88 setMouseTracking(min < max); |
|
89 |
|
90 _tooltipText = QString("%1 - %2").arg(min, 0, 'f', _prec).arg(max, 0, 'f', _prec); |
|
91 |
|
92 update(); |
|
93 } |
|
94 |
|
95 void RangedSlider::setPreferredRange(QPair<double,double> minmax) |
|
96 { |
|
97 setPreferredRange( minmax.first, minmax.second ); |
|
98 } |
|
99 |
|
100 void RangedSlider::setRange( double min, double max ) |
|
101 { |
|
102 _min = min; |
|
103 _max = max; |
|
104 update(); |
|
105 } |
|
106 |
|
107 void RangedSlider::setRange(QPair<double,double> minmax) |
|
108 { |
|
109 setRange( minmax.first, minmax.second ); |
|
110 } |
|
111 |
|
112 void RangedSlider::setValue(double value) |
|
113 { |
|
114 _val = value; |
|
115 _valText = QString("%1").arg(_val, 0, 'f', _prec); |
|
116 update(); |
|
117 |
|
118 // See comment in constructor for why we call this here |
|
119 this->setSizes(); |
|
120 return; |
|
121 } |
|
122 |
|
123 void RangedSlider::setPrecision(int precision) |
|
124 { |
|
125 _prec = precision; |
|
126 update(); |
|
127 } |
|
128 |
|
129 void RangedSlider::setBackgroundBrush( QBrush const& brush ) |
|
130 { |
|
131 _bgBrush = brush; |
|
132 update(); |
|
133 } |
|
134 |
|
135 void RangedSlider::setPreferredRangeBrush( QBrush const& brush ) |
|
136 { |
|
137 _prefRangeBrush = brush; |
|
138 update(); |
|
139 } |
|
140 |
|
141 void RangedSlider::setPreferredRangePen( QPen const& pen ) |
|
142 { |
|
143 _prefRangePen = pen; |
|
144 update(); |
|
145 } |
|
146 |
|
147 void RangedSlider::setMarkerBrush( QBrush const& brush ) |
|
148 { |
|
149 _markerBrush = brush; |
|
150 update(); |
|
151 } |
|
152 |
|
153 void RangedSlider::setMarkerText( QString const& text ) |
|
154 { |
|
155 _markerText = text; |
|
156 update(); |
|
157 } |
|
158 |
|
159 void RangedSlider::setMarkerTextIsValue(bool val) |
|
160 { |
|
161 _markerTextIsValue = val; |
|
162 update(); |
|
163 } |
|
164 |
|
165 void RangedSlider::setTickMarks( double primaryInterval, int secondaryTicks ) |
|
166 { |
|
167 _secondaryTicks = (secondaryTicks<1)? 1 : secondaryTicks; |
|
168 _tickInterval = primaryInterval/_secondaryTicks; |
|
169 |
|
170 update(); |
|
171 } |
|
172 |
|
173 void RangedSlider::recalculateHeightInPixels() const { |
|
174 // |
|
175 // We need to be able to tell Qt about our minimum and preferred size in pixels. This is something that's going |
|
176 // to depend on the dots-per-inch (DPI) resolution of the monitor we're being displayed on. Advice at |
|
177 // https://doc.qt.io/qt-5/highdpi.html is that, for best High DPI display support, we should replace hard-coded |
|
178 // sizes in layouts and drawing code with values calculated from font metrics or screen size. For this widget, we |
|
179 // use font sizes (as described in more detail later in this comment) as it seems simpler and the height of the |
|
180 // widget really is determined by the size of the text it contains. |
|
181 // |
|
182 // In theory, someone might be running the app on a system with multiple screens with different DPI resolutions, |
|
183 // so, in principle we ought to do redo size calculations every time the widget is moved, in case it moves from one |
|
184 // screen to another and the resolution changes. In practice, I'm not sure how big a requirement this is. So, for |
|
185 // now, have just tried to organise things so that it would, in principle, be possible to implement such behaviour |
|
186 // in future. |
|
187 // |
|
188 // We want height to be fixed, as the slider does not get more readable if you make it taller. So minimum and |
|
189 // preferred height are the same. |
|
190 // |
|
191 // For width, We are OK for them to expand and contract horizontally, within reason, as the size of the main window |
|
192 // changes. Minimum and preferred widths are a bit of a rule-of-thumb, but 2× and 4× height are a sensible stab. |
|
193 // |
|
194 // Firstly we have to determine what the fixed height is. We could query the DPI resolution and size of the current |
|
195 // screen, but the simplest thing is to use font sizes. The slider is basically two lines of characters high. Top |
|
196 // line is the "indicator" text that sits above the slider visual in the middle of the "preferred range". Bottom |
|
197 // line is the slider visual and the "value" text that sits to the right of the slider visual. The fonts for both |
|
198 // bits of text are set in device-independent points in the constructor, and we can just query their height etc in |
|
199 // pixels. |
|
200 // |
|
201 // Secondly, the way we tell Qt about minimum and preferred sizes is slightly different: |
|
202 // • setMinimumSize() tells Qt not to make us smaller than the specified size when resizing windows etc, HOWEVER |
|
203 // it does not determine what size we are initially drawn |
|
204 // • instead, Qt calls sizeHint() when doing initial layout, and we must override this to supply our desired |
|
205 // initial dimensions. NB: Although it makes no sense, there is nothing to stop this method returning dimensions |
|
206 // below the minimums already set via setMinimumSize(). (AIUI, sizeHint() is also called on resize events to |
|
207 // find our preferred dimensions.) |
|
208 // |
|
209 // The final wrinkle is that the height of the font sort of depends what you mean. Strictly, using the inter-line |
|
210 // spacing (= height plus leading, though the latter is often 0) gives you enough space to show any character of the |
|
211 // font. It is helpful for the indicator text to have a bit of space below it before we draw the graphical bit, and |
|
212 // it's not a large font in any case. However, for large value text font we don't necessarily need all this space |
|
213 // because we currently only show digits and decimal points, which don't require space below the baseline. However, |
|
214 // assumptions about space below the baseline are locale-specific, so, say, using ascent() instead of lineSpacing() |
|
215 // could end up painting us into a corner. |
|
216 // |
|
217 QFontMetrics indicatorTextFontMetrics(this->indicatorTextFont); |
|
218 QFontMetrics valueTextFontMetrics(this->valueTextFont); |
|
219 this->heightInPixels = indicatorTextFontMetrics.lineSpacing() + valueTextFontMetrics.lineSpacing(); |
|
220 return; |
|
221 } |
|
222 |
|
223 void RangedSlider::setSizes() { |
|
224 // Caller's responsibility to have recently called this->recalculateHeightInPixels(). (See comment in that function |
|
225 // for how we choose minimum width.) |
|
226 this->setMinimumSize(2 * this->heightInPixels, this->heightInPixels); |
|
227 |
|
228 this->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed ); |
|
229 |
|
230 // There no particular reason to limit our horizontal size, so, in principle, this call asks that there be no such |
|
231 // (practical) limit. |
|
232 this->setMaximumWidth(QWIDGETSIZE_MAX); |
|
233 |
|
234 return; |
|
235 } |
|
236 |
|
237 QSize RangedSlider::sizeHint() const |
|
238 { |
|
239 this->recalculateHeightInPixels(); |
|
240 return QSize(4 * this->heightInPixels, this->heightInPixels); |
|
241 } |
|
242 |
|
243 void RangedSlider::mouseMoveEvent(QMouseEvent* event) |
|
244 { |
|
245 event->accept(); |
|
246 |
|
247 QPoint tipPoint( mapToGlobal(QPoint(0,0)) ); |
|
248 QToolTip::showText( tipPoint, _tooltipText, this ); |
|
249 } |
|
250 |
|
251 void RangedSlider::paintEvent(QPaintEvent* event) |
|
252 { |
|
253 // |
|
254 // Simplistically, the high-level layout of the slider is: |
|
255 // |
|
256 // |-------------------------------------------------------------| |
|
257 // | Indicator text | B L A N K | |
|
258 // |------------------------------------------------+------------| |
|
259 // | <--------------- Graphical Area -------------> | Value text | |
|
260 // |-------------------------------------------------------------| |
|
261 // |
|
262 // The graphical area has: |
|
263 // - a background rectangle of the full width of the area, representing the range from this->_min to this->_max |
|
264 // - a foreground rectangle showing the sub-range of this background from this->_prefMin to this->_prefMax |
|
265 // - a line ("the indicator") showing where this->_val lies in the (this->_min to this->_max) range |
|
266 // |
|
267 // The indicator text sits above the indicator line and shows either its value (this->_valText) or some textual |
|
268 // description (eg "Slightly Malty" on the IBU/GU scale) which comes from this->_markerText. |
|
269 // |
|
270 // In principle, we could have the value text a slightly different height than the graphical area - eg to help |
|
271 // squeeze into smaller available vertical space on small screens (as we know there is blank space of |
|
272 // indicatorTextHeight pixels above the space for the value text). |
|
273 // |
|
274 // The value text also shows this->_valText. |
|
275 // |
|
276 |
|
277 QFontMetrics indicatorTextFontMetrics(this->indicatorTextFont); |
|
278 int indicatorTextHeight = indicatorTextFontMetrics.lineSpacing(); |
|
279 |
|
280 // The heights of the slider graphic and the value text are usually the same, but we calculate them differently in |
|
281 // case, in future, we want to squeeze things up a bit. |
|
282 QFontMetrics valueTextFontMetrics(this->valueTextFont); |
|
283 int valueTextHeight = valueTextFontMetrics.lineSpacing(); |
|
284 |
|
285 int graphicalAreaHeight = this->height() - indicatorTextHeight; |
|
286 |
|
287 // Although the Qt calls take an x- and a y- radius, we want the radius on the rectangle corners to be the same |
|
288 // vertically and horizontally, so only define one measure here. |
|
289 int rectangleCornerRadius = graphicalAreaHeight / 4; |
|
290 |
|
291 static const QPalette palette(QApplication::palette()); |
|
292 static const int indicatorLineWidth = 4; |
|
293 static const QColor fgRectColor(0,127,0); |
|
294 static const QColor indicatorTextColor(0,0,0); |
|
295 static const QColor valueTextColor(0,127,0); |
|
296 |
|
297 // We need to allow for the width of the text that displays to the right of the slider showing the current value. |
|
298 // If there were just one slider, we might ask Qt for the width of this text with one of the following calls: |
|
299 // const int valueTextWidth = valueTextFontMetrics.width(_valText); // Pre Qt 5.13 |
|
300 // const int valueTextWidth = valueTextFontMetrics.horizontalAdvance(_valText); // Since Qt 5.13 |
|
301 // However, we want all the sliders to have exact same width, so we choose some representative text to measure the |
|
302 // width of. We assume that all sliders show no more than 4 digits and a decimal point, and then add a space to |
|
303 // ensure a gap between the value text and the graphical area. (Note that digits are all the same width in the font |
|
304 // we are using. |
|
305 int valueTextWidth = |
|
306 #if QT_VERSION < QT_VERSION_CHECK(5,13,0) |
|
307 valueTextFontMetrics.width(" 1.000"); |
|
308 #else |
|
309 valueTextFontMetrics.horizontalAdvance(" 1.000"); |
|
310 #endif |
|
311 |
|
312 QLinearGradient glassGrad( QPointF(0,0), QPointF(0,graphicalAreaHeight) ); |
|
313 glassGrad.setColorAt( 0, QColor(255,255,255,127) ); |
|
314 glassGrad.setColorAt( 1, QColor(255,255,255,0) ); |
|
315 QBrush glassBrush(glassGrad); |
|
316 |
|
317 // Per https://doc.qt.io/qt-5/highdpi.html, for best High DPI display support, we need to: |
|
318 // • Always use the qreal versions of the QPainter drawing API |
|
319 // • Size windows and dialogs in relation to the corresponding screen size |
|
320 // • Replace hard-coded sizes in layouts and drawing code with values calculated from font metrics or screen size |
|
321 QPainter painter(this); |
|
322 |
|
323 // Work out the left-to-right (ie x-coordinate) positions of things in the graphical area |
|
324 double graphicalAreaWidth = this->width() - valueTextWidth; |
|
325 double range = this->_max - this->_min; |
|
326 double fgRectLeft = graphicalAreaWidth * ((this->_prefMin - this->_min )/range); |
|
327 double fgRectWidth = graphicalAreaWidth * ((this->_prefMax - this->_prefMin)/range); |
|
328 double indicatorLineMiddle = graphicalAreaWidth * ((this->_val - this->_min )/range); |
|
329 double indicatorLineLeft = indicatorLineMiddle - (indicatorLineWidth / 2); |
|
330 |
|
331 // Make sure all coordinates are valid. |
|
332 fgRectLeft = qBound(0.0, fgRectLeft, graphicalAreaWidth); |
|
333 fgRectWidth = qBound(0.0, fgRectWidth, graphicalAreaWidth - fgRectLeft); |
|
334 indicatorLineMiddle = qBound(0.0, indicatorLineMiddle, graphicalAreaWidth - (indicatorLineWidth / 2)); |
|
335 indicatorLineLeft = qBound(0.0, indicatorLineLeft, graphicalAreaWidth - indicatorLineWidth); |
|
336 |
|
337 // The left-to-right position of the indicator text (also known as marker text) depends on where the slider is. |
|
338 // First we ask the painter what size rectangle it will need to display this text |
|
339 painter.setPen(indicatorTextColor); |
|
340 painter.setFont(this->indicatorTextFont); |
|
341 QRectF indicatorTextRect = painter.boundingRect(QRectF(), |
|
342 Qt::AlignCenter | Qt::AlignBottom, |
|
343 this->_markerTextIsValue ? this->_valText : this->_markerText); |
|
344 |
|
345 // Then we use the size of this rectangle to try to make the middle of the text sit over the indicator marker on |
|
346 // the slider - but bounding things so that the text doesn't go off the edge of the slider. |
|
347 double indicatorTextLeft = qBound(0.0, |
|
348 indicatorLineMiddle - (indicatorTextRect.width() / 2), |
|
349 graphicalAreaWidth - indicatorTextRect.width()); |
|
350 |
|
351 // Now we can draw the indicator text |
|
352 painter.drawText( |
|
353 indicatorTextLeft, 0, |
|
354 indicatorTextRect.width(), indicatorTextRect.height(), |
|
355 Qt::AlignCenter | Qt::AlignBottom, |
|
356 this->_markerTextIsValue ? this->_valText : this->_markerText |
|
357 ); |
|
358 |
|
359 // Next draw the value text |
|
360 // We work out its vertical position relative to the bottom of the graphical area in case (in future) we want to be |
|
361 // able to use some of the blank space above it (to the right of the indicator text). |
|
362 painter.setPen(valueTextColor); |
|
363 painter.setFont(this->valueTextFont); |
|
364 painter.drawText(graphicalAreaWidth, this->height() - valueTextHeight, |
|
365 valueTextWidth, valueTextHeight, |
|
366 Qt::AlignRight | Qt::AlignVCenter, |
|
367 this->_valText ); |
|
368 |
|
369 // All the rest of what we need to do is inside the graphical area, so move the origin to the top-left corner of it |
|
370 painter.translate(0, indicatorTextRect.height()); |
|
371 painter.setPen(Qt::NoPen); |
|
372 |
|
373 // Make sure anything we draw "inside" the "glass rectangle" stays inside. |
|
374 QPainterPath clipRect; |
|
375 clipRect.addRoundedRect( QRectF(0, 0, graphicalAreaWidth, graphicalAreaHeight), |
|
376 rectangleCornerRadius, |
|
377 rectangleCornerRadius ); |
|
378 painter.setClipPath(clipRect); |
|
379 |
|
380 // Draw the background rectangle. |
|
381 painter.setBrush(_bgBrush); |
|
382 painter.setRenderHint(QPainter::Antialiasing); |
|
383 painter.drawRoundedRect( QRectF(0, 0, graphicalAreaWidth, graphicalAreaHeight), |
|
384 rectangleCornerRadius, |
|
385 rectangleCornerRadius ); |
|
386 painter.setRenderHint(QPainter::Antialiasing, false); |
|
387 |
|
388 // Draw the style "foreground" rectangle. |
|
389 painter.save(); |
|
390 painter.setBrush(_prefRangeBrush); |
|
391 painter.setPen(_prefRangePen); |
|
392 painter.setRenderHint(QPainter::Antialiasing); |
|
393 painter.drawRoundedRect( QRectF(fgRectLeft, 0, fgRectWidth, graphicalAreaHeight), |
|
394 rectangleCornerRadius, |
|
395 rectangleCornerRadius ); |
|
396 painter.restore(); |
|
397 |
|
398 // Draw the indicator. |
|
399 painter.setBrush(_markerBrush); |
|
400 painter.drawRect( QRectF(indicatorLineLeft, 0, indicatorLineWidth, graphicalAreaHeight) ); |
|
401 |
|
402 // Draw a white-to-clear gradient to suggest "glassy." |
|
403 painter.setBrush(glassBrush); |
|
404 painter.setRenderHint(QPainter::Antialiasing); |
|
405 painter.drawRoundedRect( QRectF(0, 0, graphicalAreaWidth, graphicalAreaHeight), |
|
406 rectangleCornerRadius, |
|
407 rectangleCornerRadius ); |
|
408 painter.setRenderHint(QPainter::Antialiasing, false); |
|
409 |
|
410 // Draw the ticks. |
|
411 painter.setPen(Qt::black); |
|
412 if( _tickInterval > 0.0 ) |
|
413 { |
|
414 int secTick = 1; |
|
415 for( double currentTick = _min+_tickInterval; _max - currentTick > _tickInterval-1e-6; currentTick += _tickInterval ) |
|
416 { |
|
417 painter.translate( graphicalAreaWidth/(_max-_min) * _tickInterval, 0); |
|
418 if( secTick == _secondaryTicks ) |
|
419 { |
|
420 painter.drawLine( QPointF(0,0.25*graphicalAreaHeight), QPointF(0,0.75*graphicalAreaHeight) ); |
|
421 secTick = 1; |
|
422 } |
|
423 else |
|
424 { |
|
425 painter.drawLine( QPointF(0,0.333*graphicalAreaHeight), QPointF(0,0.666*graphicalAreaHeight) ); |
|
426 ++secTick; |
|
427 } |
|
428 } |
|
429 } |
|
430 |
|
431 return; |
|
432 } |
|
433 |
|
434 |
|
435 void RangedSlider::moveEvent(QMoveEvent *event) { |
|
436 // If we've moved, we might be on a new screen with a different DPI resolution... |
|
437 // .:TBD:. This almost certainly needs further work and further testing. It's far from clear whether our font size |
|
438 // querying will give different answers just because the app has been moved from one screen to another. |
|
439 this->recalculateHeightInPixels(); |
|
440 |
|
441 QWidget::moveEvent(event); |
|
442 return; |
|
443 } |