#pragma once #include #include #include #include #include // for std::max #include #include #include #include /* * LabeledSlider * Horizontal QSlider that draws numeric labels at tick intervals and (optionally) a value bubble. * Usage: * auto *s = new LabeledSlider(parent); * s->setMinimum(20); * s->setMaximum(250); * s->setTickInterval(20); * s->setTickStartOffset(10); // ticks start at 10, then increment by 20 * s->setTickPosition(QSlider::TicksBelow); * s->setShowValueBubble(true); */ class LabeledSlider : public QSlider { Q_OBJECT Q_PROPERTY(bool showValueBubble READ showValueBubble WRITE setShowValueBubble) // decimalShift: number of places to shift the decimal point left for display ONLY. // Example: raw value 250 with decimalShift=2 displays as 2.50. Underlying slider value // (signals, stored config) remains 250. Q_PROPERTY(int decimalShift READ decimalShift WRITE setDecimalShift) // tickStartOffset: starting offset for label positions relative to minimum(). // Example: minimum=0, tickInterval=20, tickStartOffset=10 -> labels at 10,30,50,... Q_PROPERTY(int tickStartOffset READ tickStartOffset WRITE setTickStartOffset) // Optional units suffix shown ONLY in the floating value bubble. // Tick labels never include units. Q_PROPERTY(QString valueUnitsSuffix READ valueUnitsSuffix WRITE setValueUnitsSuffix) public: using ValueToDisplayStringFn = std::function; explicit LabeledSlider(QWidget *parent = nullptr) : QSlider(Qt::Horizontal, parent) { setTickPosition(QSlider::TicksBelow); } bool showValueBubble() const { return m_showValueBubble; } void setShowValueBubble(bool on) { if (m_showValueBubble == on) return; m_showValueBubble = on; update(); } // Custom text for specific integer values void setValueText(int value, const QString &text) { // If the same text already set, ignore; allow empty string as valid label auto it = m_valueTexts.constFind(value); if (it != m_valueTexts.constEnd() && *it == text) return; m_valueTexts.insert(value, text); update(); } void removeValueText(int value) { if (m_valueTexts.remove(value) > 0) update(); } void clearValueTexts() { if (m_valueTexts.isEmpty()) return; m_valueTexts.clear(); update(); } void setValueTexts(const QMap &map) { if (m_valueTexts == map) return; m_valueTexts = map; update(); } QMap valueTexts() const { return m_valueTexts; } QString valueUnitsSuffix() const { return m_valueUnitsSuffix; } void setValueUnitsSuffix(const QString &suffix) { if (m_valueUnitsSuffix == suffix) return; m_valueUnitsSuffix = suffix; updateGeometry(); update(); } // Optional custom formatter for displayed values. // If set, it is consulted for values without an explicit setValueText() override. // Returning a null QString (QString()) falls back to the built-in formatting. void setValueToDisplayStringFn(ValueToDisplayStringFn fn) { m_valueToDisplayStringFn = std::move(fn); updateGeometry(); update(); } void clearValueToDisplayStringFn() { if (!m_valueToDisplayStringFn) return; m_valueToDisplayStringFn = nullptr; updateGeometry(); update(); } bool hasValueToDisplayStringFn() const { return static_cast(m_valueToDisplayStringFn); } int decimalShift() const { return m_decimalShift; } void setDecimalShift(int shift) { // clamp to sensible range if (shift < 0) shift = 0; if (shift > 6) shift = 6; // avoid large power-of-10 overflow if (m_decimalShift == shift) return; m_decimalShift = shift; updateGeometry(); update(); } int tickStartOffset() const { return m_tickStartOffset; } void setTickStartOffset(int offset) { if (m_tickStartOffset == offset) return; m_tickStartOffset = offset; update(); } QSize sizeHint() const override { QSize sz = QSlider::sizeHint(); int extraH = 0; if (labelInterval() > 0) { // Reserve space for bottom labels QFontMetrics fm(font()); extraH += fm.height() + 4; } if (m_showValueBubble) { QFontMetrics fm(font()); extraH = std::max(extraH, fm.height() + 8); // bubble might be above } sz.setHeight(sz.height() + extraH); return sz; } protected: void paintEvent(QPaintEvent *e) override { QSlider::paintEvent(e); QStyleOptionSlider opt; initStyleOption(&opt); QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); const int minV = minimum(); const int maxV = maximum(); // Draw labels below ticks if (labelInterval() > 0) { QFontMetrics fm(font()); const int baselineY = height() - fm.descent() - 1; int interval = labelInterval(); // Determine first labeled value based on tickStartOffset int startOffset = 0; if (interval > 0) { startOffset = m_tickStartOffset % interval; if (startOffset < 0) startOffset += interval; } int first = minV + startOffset; if (first < minV) first = minV; // safety for (int v = first; v <= maxV; v += interval) { // Use style geometry for handle at this position to match tick placement. QStyleOptionSlider optPos = opt; optPos.sliderPosition = v; optPos.sliderValue = v; QRect handleAtVal = style()->subControlRect(QStyle::CC_Slider, &optPos, QStyle::SC_SliderHandle, this); int x = handleAtVal.center().x(); QString text = valueToDisplayString(v, /*forValueBubble=*/false); int halfW = fm.horizontalAdvance(text) / 2; QRect r(x - halfW, baselineY - fm.ascent(), fm.horizontalAdvance(text), fm.height()); p.drawText(r, Qt::AlignCenter, text); } } // Draw floating value bubble over handle if (m_showValueBubble) { // Handle rect const QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); QString valText = valueToDisplayString(value(), /*forValueBubble=*/true); QFontMetrics fm(font()); QRect textRect = fm.boundingRect(valText); textRect.adjust(-6, -4, 6, 4); // Position bubble above the handle; add extra lift const int extraLift = 10; // requested additional pixels const int gap = 4; // minimal gap between handle top and bubble int topY = handle.top() - gap - extraLift - textRect.height(); if (topY < 0) topY = 0; // clamp to widget textRect.moveTop(topY); // Center horizontally over handle textRect.moveLeft(handle.center().x() - textRect.width()/2); // Clamp horizontally so we don't paint outside and get clipped if (textRect.left() < 0) { textRect.moveLeft(0); } if (textRect.right() > width()) { textRect.moveLeft(width() - textRect.width()); } // Bubble shape QPainterPath path; path.addRoundedRect(textRect, 6, 6); p.setPen(Qt::NoPen); p.setBrush(palette().toolTipBase()); p.drawPath(path); p.setPen(palette().toolTipText().color()); p.drawText(textRect, Qt::AlignCenter, valText); } } private: QString valueToDisplayString(int raw, bool forValueBubble) const { // Use custom text if provided for this exact integer value auto it = m_valueTexts.constFind(raw); if (it != m_valueTexts.constEnd()) { return *it; } auto decimalShiftString = [this](int r) -> QString { if (m_decimalShift == 0) { return QString::number(r); } int divisor = 1; for (int i = 0; i < m_decimalShift; ++i) divisor *= 10; int whole = r / divisor; int frac = std::abs(r % divisor); QString fracStr = QString::number(frac).rightJustified(m_decimalShift, QLatin1Char('0')); QString result = QString::number(std::abs(whole)) + QLatin1Char('.') + fracStr; if (r < 0) result.prepend(QLatin1Char('-')); return result; }; QString base; if (m_valueToDisplayStringFn) { base = m_valueToDisplayStringFn(raw); } if (base.isNull()) { base = decimalShiftString(raw); } if (!forValueBubble) { return base; } const QString suffix = m_valueUnitsSuffix.trimmed(); if (suffix.isEmpty() || base.isEmpty()) { return base; } // Avoid double-appending if caller included units in the formatter. const QString baseTrimmed = base.trimmed(); if (baseTrimmed.endsWith(suffix) || baseTrimmed.endsWith(QLatin1Char(' ') + suffix)) { return base; } return baseTrimmed + QLatin1Char(' ') + suffix; } bool m_showValueBubble = true; int m_decimalShift = 0; // display-only decimal shift int m_tickStartOffset = 0; // label positions start offset relative to minimum QMap m_valueTexts; // optional text overrides for specific values ValueToDisplayStringFn m_valueToDisplayStringFn; // optional custom formatter QString m_valueUnitsSuffix; // shown only in the value bubble private: int labelInterval() const { int ti = tickInterval(); if (ti > 0) return ti; // Heuristic fallback: divide range into ~10 segments. int range = maximum() - minimum(); if (range <= 0) return 0; int approx = range / 10; if (approx <= 0) approx = range; // single label at ends return approx; } };