src/RangedSlider.cpp

changeset 92
fb0bb9a2a7e1
child 96
c36fef8bb088
equal deleted inserted replaced
91:409d9c7214be 92:fb0bb9a2a7e1
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 }

mercurial