breezy-desktop/kwin/src/kcm/labeledslider.h

284 lines
10 KiB
C++

#pragma once
#include <QSlider>
#include <QPainter>
#include <QPainterPath>
#include <QStyleOptionSlider>
#include <algorithm> // for std::max
#include <functional>
#include <QtCore/QMap>
#include <QtCore/QString>
#include <QStringList>
/*
* 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<QString(int)>;
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<int, QString> &map) {
if (m_valueTexts == map) return;
m_valueTexts = map;
update();
}
QMap<int, QString> 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<bool>(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<int, QString> 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;
}
};