src/RangedSlider.cpp

Thu, 31 Mar 2022 23:10:57 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Thu, 31 Mar 2022 23:10:57 +0200
changeset 97
8283bbf95806
parent 96
c36fef8bb088
child 99
053c0578cf58
permissions
-rw-r--r--

Stripped down the RangedSlider. It is more compact and still does everything. Base colors (red and green) are fixed inside, also automatisc setting of outer limits. The tooltip shows the current ranges. Still some finetuning to be done.

/*
 * RangedSlider.cpp is part bmsapp.
 *
 * Original written for Brewtarget, and is Copyright the following
 * authors 2009-2020
 * - Matt Young <mfsy@yahoo.com>
 * - Mik Firestone <mikfire@gmail.com>
 * - Philip G. Lee <rocketman768@gmail.com>
 *
 * bmsapp is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * bmsapp is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "RangedSlider.h"
#include <QPaintEvent>
#include <QPainter>
#include <QColor>
#include <QPalette>
#include <QApplication>
#include <QRectF>
#include <QFont>
#include <QFontMetrics>
#include <QMouseEvent>
#include <QLabel>
#include <QToolTip>
#include <QLinearGradient>
#include <QPainterPath>

#include <QDebug>

RangedSlider::RangedSlider(QWidget* parent)
   : QWidget(parent),
    _min(0.0),
    _max(1.0),
    _prefMin(0.25),
    _prefMax(0.75),
    _val(0.5),
    _prec(3),
    _tooltipText(""),
    _bgBrush(QColor(255,0,0)),
    _prefRangeBrush(QColor(0,127,0)),
    _prefRangePen(Qt::NoPen),
    _markerBrush(QColor(255,255,255)),
    _markerTextIsValue(false),
    indicatorTextFont("Arial",
                       10,
                       QFont::Normal) // Previously we just did the indicator text in 'default' font
{
   // Ensure this->heightInPixels is properly initialised
   this->recalculateHeightInPixels();

   // In principle we want to set our min/max sizes etc here.  However, if, say, a maximumSize property has been set
   // for this object in a Designer UI File (eg ui/mainWindow.ui) then that setting will override this one, because it
   // will be applied later (in fact pretty much straight after this constructor returns).  So we also make this call
   // inside setValue(), which will be invoked _after_ the setter calls that were auto-generated from the Designer UI
   // File.
   this->setSizes();

   // Generate mouse move events whenever mouse movers over widget.
   this->setMouseTracking(true);

   this->repaint();
}

/**
 * @brief Set the normal limits in the _prexXxx values and calculate
 *        the real drawing limits so that we can see if a value is slightly
 *        out of range.
 */
void RangedSlider::setRange( double min, double max )
{
    _prefMin = min;
    _prefMax = max;
    // Calculate the outer limits
    _min = _prefMin - ((_prefMax - _prefMin) * 0.2);
    _max = _prefMax + ((_prefMax - _prefMin) * 0.2);

    // Set the tooltip with the ranges.
    _tooltipText = QString("%1 - %2").arg(min, 0, 'f', _prec).arg(max, 0, 'f', _prec);

    update();
}


void RangedSlider::setRange(QPair<double,double> minmax)
{
   setRange( minmax.first, minmax.second );
}


void RangedSlider::setValue(double value)
{
    _val = value;

    if (_val < _min)
	_val = _min;
    if (_val > _max)
	_val = _max;

    if (_markerTextIsValue) {
	_markerText = QString("%1").arg(value, 0, 'f', _prec);
	qDebug() << _markerText;
    }

    update();

    // See comment in constructor for why we call this here
    //this->setSizes();
    return;
}


void RangedSlider::setPrecision(int precision)
{
    _prec = precision;
    update();
}


void RangedSlider::setBackgroundBrush( QBrush const& brush )
{
    _bgBrush = brush;
    update();
}


void RangedSlider::setMarkerBrush( QBrush const& brush )
{
    _markerBrush = brush;
    update();
}

void RangedSlider::setMarkerText( QString const& text )
{
    _markerText = text;
    update();
}

void RangedSlider::setMarkerTextIsValue(bool val)
{
    _markerTextIsValue = val;
    update();
}


void RangedSlider::recalculateHeightInPixels() const {
   //
   // We need to be able to tell Qt about our minimum and preferred size in pixels.  This is something that's going
   // to depend on the dots-per-inch (DPI) resolution of the monitor we're being displayed on.  Advice at
   // https://doc.qt.io/qt-5/highdpi.html is that, for best High DPI display support, we should replace hard-coded
   // sizes in layouts and drawing code with values calculated from font metrics or screen size.  For this widget, we
   // use font sizes (as described in more detail later in this comment) as it seems simpler and the height of the
   // widget really is determined by the size of the text it contains.
   //
   // In theory, someone might be running the app on a system with multiple screens with different DPI resolutions,
   // so, in principle we ought to do redo size calculations every time the widget is moved, in case it moves from one
   // screen to another and the resolution changes.  In practice, I'm not sure how big a requirement this is.  So, for
   // now, have just tried to organise things so that it would, in principle, be possible to implement such behaviour
   // in future.
   //
   // We want height to be fixed, as the slider does not get more readable if you make it taller.  So minimum and
   // preferred height are the same.
   //
   // For width, We are OK for them to expand and contract horizontally, within reason, as the size of the main window
   // changes.  Minimum and preferred widths are a bit of a rule-of-thumb, but 2× and 4× height are a sensible stab.
   //
   // Firstly we have to determine what the fixed height is.  We could query the DPI resolution and size of the current
   // screen, but the simplest thing is to use font sizes.  The slider is basically two lines of characters high.  Top
   // line is the "indicator" text that sits above the slider visual in the middle of the "preferred range".  Bottom
   // line is the slider visual and the "value" text that sits to the right of the slider visual.  The fonts for both
   // bits of text are set in device-independent points in the constructor, and we can just query their height etc in
   // pixels.
   //
   // Secondly, the way we tell Qt about minimum and preferred sizes is slightly different:
   //  • setMinimumSize() tells Qt not to make us smaller than the specified size when resizing windows etc, HOWEVER
   //    it does not determine what size we are initially drawn
   //  • instead, Qt calls sizeHint() when doing initial layout, and we must override this to supply our desired
   //    initial dimensions.  NB: Although it makes no sense, there is nothing to stop this method returning dimensions
   //    below the minimums already set via setMinimumSize().  (AIUI, sizeHint() is also called on resize events to
   //    find our preferred dimensions.)
   //
   // The final wrinkle is that the height of the font sort of depends what you mean.  Strictly, using the inter-line
   // spacing (= height plus leading, though the latter is often 0) gives you enough space to show any character of the
   // font.  It is helpful for the indicator text to have a bit of space below it before we draw the graphical bit, and
   // it's not a large font in any case.  However, for large value text font we don't necessarily need all this space
   // because we currently only show digits and decimal points, which don't require space below the baseline.  However,
   // assumptions about space below the baseline are locale-specific, so, say, using ascent() instead of lineSpacing()
   // could end up painting us into a corner.
   //
   //QFontMetrics indicatorTextFontMetrics(this->indicatorTextFont);
   this->heightInPixels = this->height(); //indicatorTextFontMetrics.lineSpacing();
   return;
}

void RangedSlider::setSizes() {
   // Caller's responsibility to have recently called this->recalculateHeightInPixels().  (See comment in that function
   // for how we choose minimum width.)
//   this->setMinimumSize(2 * this->heightInPixels, this->heightInPixels);
   this->setMinimumSize(60, 20);

   this->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );

   // There no particular reason to limit our horizontal size, so, in principle, this call asks that there be no such
   // (practical) limit.
   this->setMaximumWidth(QWIDGETSIZE_MAX);

   return;
}

QSize RangedSlider::sizeHint() const
{
   this->recalculateHeightInPixels();
   return QSize(4 * this->heightInPixels, this->heightInPixels);
}

void RangedSlider::mouseMoveEvent(QMouseEvent* event)
{
    event->accept();

    QPoint tipPoint( mapToGlobal(QPoint(0,0)) );
    QToolTip::showText( tipPoint, _tooltipText, this );
}


void RangedSlider::paintEvent(QPaintEvent* event)
{
   //
   // Simplistically, the high-level layout of the slider is:
   //
   //    |------------------------------------------------|
   //    | <--------------- Graphical Area -------------> |
   //    |------------------------------------------------|
   //
   // The graphical area has:
   //  - a background rectangle of the full width of the area, representing the range from this->_min to this->_max
   //  - a foreground rectangle showing the sub-range of this background from this->_prefMin to this->_prefMax
   //  - a vertical line ("the indicator") showing where this->_val lies in the (this->_min to this->_max) range
   //
   // The indicator text sits on the center of the area and shows either its value (this->_valText) or some textual
   // description (eg "Slightly Malty" on the IBU/GU scale) which comes from this->_markerText.
   //

   int graphicalAreaHeight = this->height();

   // Although the Qt calls take an x- and a y- radius, we want the radius on the rectangle corners to be the same
   // vertically and horizontally, so only define one measure here.
   int rectangleCornerRadius = graphicalAreaHeight / 4;

   static const QPalette palette(QApplication::palette());
   static const int indicatorLineWidth   = 4;
   static const QColor fgRectColor(0,127,0);
   static const QColor indicatorTextColor(0,0,0);
   static const QColor valueTextColor(0,127,0);

   QLinearGradient glassGrad( QPointF(0,0), QPointF(0,graphicalAreaHeight) );
   glassGrad.setColorAt( 0, QColor(255,255,255,127) );
   glassGrad.setColorAt( 1, QColor(255,255,255,0) );
   QBrush glassBrush(glassGrad);

   // Per https://doc.qt.io/qt-5/highdpi.html, for best High DPI display support, we need to:
   //  • Always use the qreal versions of the QPainter drawing API
   //  • Size windows and dialogs in relation to the corresponding screen size
   //  • Replace hard-coded sizes in layouts and drawing code with values calculated from font metrics or screen size
   QPainter painter(this);

   // Work out the left-to-right (ie x-coordinate) positions of things in the graphical area
   double graphicalAreaWidth  = this->width();
   double range               = this->_max - this->_min;
   double fgRectLeft          = graphicalAreaWidth * ((this->_prefMin - this->_min    )/range);
   double fgRectWidth         = graphicalAreaWidth * ((this->_prefMax - this->_prefMin)/range);
   double indicatorLineMiddle = graphicalAreaWidth * ((this->_val     - this->_min    )/range);
   double indicatorLineLeft   = indicatorLineMiddle - (indicatorLineWidth / 2);

   // Make sure all coordinates are valid.
   fgRectLeft          = qBound(0.0, fgRectLeft,          graphicalAreaWidth);
   fgRectWidth         = qBound(0.0, fgRectWidth,         graphicalAreaWidth - fgRectLeft);
   indicatorLineMiddle = qBound(0.0, indicatorLineMiddle, graphicalAreaWidth - (indicatorLineWidth / 2));
   indicatorLineLeft   = qBound(0.0, indicatorLineLeft,   graphicalAreaWidth - indicatorLineWidth);

   // 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
   painter.translate(0, 0);
   painter.setPen(Qt::NoPen);

   // Make sure anything we draw "inside" the "glass rectangle" stays inside.
   QPainterPath clipRect;
   clipRect.addRoundedRect( QRectF(0, 0, graphicalAreaWidth, graphicalAreaHeight), rectangleCornerRadius, rectangleCornerRadius );
   painter.setClipPath(clipRect);

   // Draw the background rectangle.
   painter.setBrush(_bgBrush);
   painter.setRenderHint(QPainter::Antialiasing);
   painter.drawRoundedRect( QRectF(0, 0, graphicalAreaWidth, graphicalAreaHeight), rectangleCornerRadius, rectangleCornerRadius );
   painter.setRenderHint(QPainter::Antialiasing, false);

   // Draw the style "foreground" rectangle.
   painter.save();
   painter.setBrush(_prefRangeBrush);
   painter.setPen(_prefRangePen);
   painter.setRenderHint(QPainter::Antialiasing);
   painter.drawRoundedRect( QRectF(fgRectLeft, 0, fgRectWidth, graphicalAreaHeight), rectangleCornerRadius, rectangleCornerRadius );
   painter.restore();

   // Draw the indicator.
   painter.setBrush(_markerBrush);
   painter.drawRect( QRectF(indicatorLineLeft, 0, indicatorLineWidth, graphicalAreaHeight) );

    // Draw a white-to-clear gradient to suggest "glassy."
    painter.setBrush(glassBrush);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.drawRoundedRect( QRectF(0, 0, graphicalAreaWidth, graphicalAreaHeight), rectangleCornerRadius, rectangleCornerRadius );
    painter.setRenderHint(QPainter::Antialiasing, false);

    // Draw the _markerText in the center of the widget.
    // First we ask the painter what size rectangle it will need to display this text
    painter.setPen(indicatorTextColor);
    painter.setFont(this->indicatorTextFont);
    QRectF indicatorTextRect = painter.boundingRect(QRectF(), Qt::AlignCenter | Qt::AlignBottom, this->_markerText);

    // Then we use the size of this rectangle to try to calculate the X and Y position for the markerText.
    double indicatorTextX = (graphicalAreaWidth - indicatorTextRect.width()) / 2;
    double indicatorTextY = (graphicalAreaHeight - indicatorTextRect.height()) / 2;

    // Now we can draw the indicator text
    painter.drawText( indicatorTextX, indicatorTextY, indicatorTextRect.width(), indicatorTextRect.height(), Qt::AlignCenter | Qt::AlignBottom, this->_markerText);

    return;
}


void RangedSlider::moveEvent(QMoveEvent *event) {
   // If we've moved, we might be on a new screen with a different DPI resolution...
   // .:TBD:. This almost certainly needs further work and further testing.  It's far from clear whether our font size
   //         querying will give different answers just because the app has been moved from one screen to another.
   this->recalculateHeightInPixels();

   QWidget::moveEvent(event);
   return;
}

mercurial