15.1.1013. tablet_qt/widgets/tickslider.h

/*
    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS 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.

    CamCOPS 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 CamCOPS. If not, see <https://www.gnu.org/licenses/>.
*/

#pragma once
#include <QMap>
#include <QSlider>
#include <QStyle>
#include <QStyleOptionSlider>
#include "common/uiconst.h"
#include "lib/margins.h"

class QHoverEvent;
class QKeyEvent;
class QMouseEvent;


// Slider with tick marks/labels.
//
// Tick marks vanish on a styled QSlider. So:
// http://stackoverflow.com/questions/27531542/tick-marks-disappear-on-styled-qslider
// ... and then modification including labels, and making it work with
// vertical sliders.

/*

Terminology:

Horizontal:

        1  two  3   4       6           tick labels
        |   |   |   |   |   |           ticks
    ----------------XX------------      the slider, with its handle
        |   |   |   |   |   |           ticks
        1  two  3   4       6           tick labels

Vertical:

     |
    -|- 6
     |
    -|- 5
     |
    -X- 4
     |
    -|- 4
     |
    -|- 3
     |
    -|- two
     |
    -|- 1
     |

*/

class TickSlider : public QWidget
{
    // As soon as we start re-jigging the layout of the slider itself, e.g.
    // to ensure labels have enough space at the edges, we need to override
    // functions like initStyleOption() that are not virtual in QSlider.
    // At that point, we have to go the whole hog and inherit from
    // QAbstractSlider instead, and re-implement QSlider.
    //
    // ... Unless we can fake the mouse coordinates.
    // ... Nope; mouse hovering involves QSlider::event(), which calls
    //     private functions.
    // ... Although maybe we only need to re-implement
    //     QSliderPrivate::updateHoverControl().
    // ... Tried hard; failed.
    // Reimplementation it is.
    //
    // The thing that makes it very hard is that QSlider has deep connections
    // to QSliderPrivate, which inherits from QAbstractSliderPrivate (with
    // deep links to QAbstractSlider), which inherits from QWidgetPrivate...
    // etc. (Even if we wanted to cheat, none of the *private*.h files are
    // exposed in Qt builds...)
    // No... it's an absolute nightmare; we regress into the bowels of
    // QWidgetPrivate and can't modify the data.
    // This is perhaps the weakest aspect of Qt.
    //
    // So: it has to be layouts, or layout-free widget composition.
    //
    // And that worked beautifully.
    //
    // Re "jump navigation":
    // - We embed a QSlider. Clicks within it are not visible to our mouse
    //   event captures. So no fiddling with that, unless we completely
    //   re-implement QSlider.

    Q_OBJECT

    // ========================================================================
    // Public interface
    // ========================================================================
public:
    // Create a TickSlider with the default (vertical) orientation.
    // - groove_margin_px is the width of the margin of the slider's groove, in
    //   pixels. (We can't read this, so we need to be told.)
    // - Note that default arguments are evaluated at call time. Good C++.
    TickSlider(QWidget* parent = nullptr,
               int groove_margin_px = uiconst::SLIDER_GROOVE_MARGIN_PX);

    // Create a TickSlider, specifying the orientation.
    TickSlider(Qt::Orientation orientation, QWidget* parent = nullptr,
               int groove_margin_px = uiconst::SLIDER_GROOVE_MARGIN_PX);

    // Set the tick colour.
    virtual void setTickColor(const QColor& colour);

    // Set the tick thickness (width), in pixels.
    virtual void setTickThickness(int thickness);

    // Sets the tick length, in pixels (perpendicular to the slider).
    virtual void setTickLength(int length);

    // Set the gap between ticks and tick labels, in pixels
    // (measured perpendicular to the slider).
    virtual void setTickLabelGap(int gap);

    // Set the minimum gap between labels, in pixels
    // (measured parallel to the slider).
    virtual void setInterlabelGap(int gap);

    // Set the gap between the slider and labels/ticks (whichever comes first),
    // in pixels (measured perpendicular to the slider).
    virtual void setGapToSlider(int gap);

    // Make the extreme labels align with the ends of the slider?
    // For a horizontal slider, that means "left-align the left label with its
    // tick, and right-align the right label with its tick" (rather than the
    // default of centre-aligning all labels with their ticks).
    // Similarly for vertical sliders.
    // Default is false.
    virtual void setEdgeInExtremeLabels(bool edge_in_extreme_labels);

    // [Only applicable if setEdgeInExtremeLabels() is set to false.]
    // Using a horizontal slider example, suppose the leftmost label is
    // very long and the rightmost label is very short. The label will
    // "overspill" a lot on the left and not much on the right. This will mean
    // that the whole slider is not centred. If you call
    // setSymmetricOverspill(true), extra space will be added on the right so
    // that the whole thing is symmetric.
    virtual void setSymmetricOverspill(bool symmetric_overspill);

    // Sets the absolute length of the slider's active range, in pixels.
    // If can_shrink is true, the slider can get smaller (for small screens).
    // See also setAbsoluteLengthCm().
    virtual void setAbsoluteLengthPx(int px, bool can_shrink = true);

    // Sets the absolute length of the slider's active range, in cm, given also
    // the screen's current dpi setting (which you must provide).
    // - Convenience function that calls setAbsoluteLengthPx().
    // - Use this to say "make the slider exactly 10cm".
    // - Beware on small screens! Suggest setting can_shrink to true.
    virtual void setAbsoluteLengthCm(qreal abs_length_cm, qreal dpi,
                                     bool can_shrink = true);

    // Standard QWidget size hint.
    virtual QSize sizeHint() const override;

    // Standard QWidget minimum size hint
    virtual QSize minimumSizeHint() const override;

    // Chooses whether tick labels are shown left/right/both (vertical sliders)
    // or above/below/both (horizontal sliders).
    virtual void setTickLabelPosition(QSlider::TickPosition position);

    // Reads what was set by setTickLabelPosition().
    virtual QSlider::TickPosition tickLabelPosition() const;

    // Adds a label at an integer slider position.
    virtual void addTickLabel(int position, const QString& text);

    // Sets all tick labels simultaneously. The map maps integer slider
    // position to text.
    virtual void setTickLabels(const QMap<int, QString>& labels);

    // Adds some default numerical tick labels (at the tickInterval(), or if
    // that's not set, the pageStep()).
    virtual void addDefaultTickLabels();

    // Set the CSS name of this widget and its QSlider child widget.
    virtual void setCssName(const QString& name);

    // Is it horizontal?
    bool isHorizontal() const;

    // ========================================================================
    // Passing on calls to/from our slider
    // ========================================================================
public:

    // ------------------------------------------------------------------------
    // From QAbstractSlider (q.v.):
    // ------------------------------------------------------------------------

    Qt::Orientation orientation() const { return m_slider.orientation(); }

    void setMinimum(int minimum) { m_slider.setMinimum(minimum); }
    int minimum() const { return m_slider.minimum(); }

    void setMaximum(int maximum) { m_slider.setMaximum(maximum); }
    int maximum() const { return m_slider.maximum(); }

    void setSingleStep(int step) { m_slider.setSingleStep(step); }
    int singleStep() const { return m_slider.singleStep(); }

    void setPageStep(int step) { m_slider.setPageStep(step); }
    int pageStep() const { return m_slider.pageStep(); }

    void setTracking(bool enable) { m_slider.setTracking(enable); }
    bool hasTracking() const { return m_slider.hasTracking(); }

    void setSliderDown(bool down) { m_slider.setSliderDown(down); }
    bool isSliderDown() const { return m_slider.isSliderDown(); }

    void setSliderPosition(int pos) { m_slider.setSliderPosition(pos); }
    int sliderPosition() const { return m_slider.sliderPosition(); }

    // Reverse the direction of the slider.
    // Default is left (low) -> right (high), and bottom (low) -> top (high).
    void setInvertedAppearance(bool inverted) { m_slider.setInvertedAppearance(inverted); }
    bool invertedAppearance() const { return m_slider.invertedAppearance(); }

    // Reverse the behaviour of key/mouse wheel events.
    // See https://doc.qt.io/qt-6.5/qabstractslider.html
    void setInvertedControls(bool inverted) { m_slider.setInvertedControls(inverted); }
    bool invertedControls() const { return m_slider.invertedControls(); }

    int value() const { return m_slider.value(); }

    void triggerAction(QSlider::SliderAction action) { m_slider.triggerAction(action); }

public slots:
    void setValue(int value) { m_slider.setValue(value); }
    void setOrientation(Qt::Orientation orientation);
    void setRange(int min, int max) { m_slider.setRange(min, max); }

signals:
    void valueChanged(int value);

    void sliderPressed();
    void sliderMoved(int position);
    void sliderReleased();

    void rangeChanged(int min, int max);

    void actionTriggered(int action);

    // ------------------------------------------------------------------------
    // From QSlider (q.v.):
    // ------------------------------------------------------------------------
public:

    void setTickPosition(QSlider::TickPosition pos) { m_slider.setTickPosition(pos); }
    QSlider::TickPosition tickPosition() const { return m_slider.tickPosition();}

    void setTickInterval(int ti) { m_slider.setTickInterval(ti); }
    int tickInterval() const { return m_slider.tickInterval(); }

    // ========================================================================
    // Our internals
    // ========================================================================

protected:

    // Get the size of the biggest label (more accurately, a size that will
    // hold all our labels).
    QSize maxLabelSize() const;

    // Are we using ticks?
    bool usingTicks() const;

    // Are we using labels?
    bool usingLabels() const;

    // Given an integer value from the slider, get the position along the
    // slider as a proportion (0-1), in a  standard drawing direction
    // (x left->right, y top->bottom).
    double getDrawingProportion(int value) const;

    // Tick position (vertical or horizontal) along the slider.
    int getTickPos(double drawing_proportion, int active_groove_start,
                   int active_groove_extent) const;

    // The extent to which labels "overspill" the boundaries of the slider
    // (in the direction along its length.)
    //
    // - If m_edge_in_extreme_labels, these are all zero. Otherwise:
    // - Horizontal:
    //   - If we have a label for the leftmost (minimum) value, "left" is set
    //     to half the width of the leftmost label.
    //   - If we have a label for the rightmost (maximum) value, "right" is set
    //     to half the width of the rightmost label.
    // - Vertical:
    //   - If we have a label for the topmost (maximum) value, "top" is set to
    //     half the width of the topmost label.
    //   - If we have a label for the bottom (minimum) value, "bottom" is set
    //     to half the width of the bottom label.
    virtual Margins getLabelOverspill() const;

    // Returns the size of all the extra things we draw around the QSlider.
    Margins getSurround() const;

    // Clear cached information
    void clearCache();

    // Return the "active" part of the groove.
    QRect getSliderActiveGroove() const;

    // Calculate where our slider widget should be (it is a sub-rectangle of
    // our main widget).
    QRect getSliderRect() const;

    // Tells the slider to reposition and/or resize itself.
    void repositionSlider();

    // Reset the widget size policy
    void resetSizePolicy();

    // Calculate the slider's size *including* its handle -- it seems to ignore
    // this via sizeHint() or minimumSizeHint()!
    QSize sliderSizeWithHandle(bool minimum_size) const;

    // Expand a starting "slider" size to the size required by the whole widget.
    QSize wholeWidgetSize(const QSize& slider_size) const;

    // Event handlers
    virtual bool event(QEvent* event) override;
    virtual void moveEvent(QMoveEvent* event) override;
    virtual void paintEvent(QPaintEvent* ev) override;
    virtual void resizeEvent(QResizeEvent* event) override;

    // Replicated from QSlider (where it's protected):
    void initStyleOption(QStyleOptionSlider* option) const;

protected:
    QSlider m_slider;  // our slider
    int m_groove_margin_px;  // width of the margin of the slider's groove
    QColor m_tick_colour;  // tick colour
    int m_tick_thickness;  // tick thickness (parallel to slider)
    int m_tick_length;  // tick length (perpendicular to slider)
    int m_tick_label_gap;  // gap between ticks and labels (perpendicular to slider)
    int m_min_interlabel_gap;  // minimum gap between labels (parallel to slider)
    int m_gap_to_slider;  // gap adjacent to slider (to ticks or labels)
    QSlider::TickPosition m_label_position;  // labels left/right/both (etc.)
    QMap<int, QString> m_tick_labels;  // maps from slider position to label text
    bool m_edge_in_extreme_labels;  // see setEdgeInExtremeLabels()
    bool m_symmetric_overspill;  // see setSymmetricOverspill()
    int m_slider_target_length_px;  // absolute target length; <=0 means don't use this
    bool m_absolute_size_can_shrink;
        // ... if an absolute length is set, can we shrink smaller if we have
        // to? May be preferable on physically small screens.
    mutable bool m_is_overspill_cached;  // is m_cached_overspill valid?
    mutable Margins m_cached_overspill;  // cached margins for overspill; see setSymmetricOverspill()
};