js8call/JS8_Main/AttenuationSlider.cpp
2026-01-18 19:19:15 -06:00

310 lines
10 KiB
C++

/**
* @file AttenuationSlider.cpp
* @brief Implementation of AttenuationSlider custom QSlider
*/
#include "AttenuationSlider.h"
#include <QApplication>
#include <QPainter>
#include <QPixmapCache>
#include <QStyle>
/******************************************************************************/
// Private Implementation
/******************************************************************************/
namespace {
// Tunable dimensions. Note that we're going to fill the handle with
// strings of the format -##.#, e.g., 0, -22, -16.7, using the default
// system font, which will generally be 12 point; define accordingly.
constexpr auto grooveWidth = 10;
constexpr auto tickLength = 8;
constexpr auto handleSize = QSize(40, 20);
// Colors; the overall flavor of the app is like that of the fusion
// style in terms of color choices, etc.; these are colors that should
// feel at home there. Note that we're not addressing dark mode here,
// since that's work that overall we've not addressed yet for the app.
constexpr auto grooveColor = QColor(192, 192, 192);
constexpr auto activeColor = QColor(10, 129, 254);
constexpr auto handleStartColor = QColor(0, 255, 0);
constexpr auto handleStopColor = QColor(39, 174, 96);
constexpr auto outlineColor = QColor(0, 0, 0, 160);
constexpr auto contrastColor = QColor(255, 255, 255, 30);
// Given a size, return a transparently-filled pixmap, with a pixel
// ratio appropriate to the device in play; if the size is martian,
// return a null pixmap.
/**
* @private
* @brief Make a pixmap of the given size
*
* @param size
* @return auto
*/
auto makePixmap(QSize const size) {
if (size.isEmpty())
return QPixmap();
auto const pixelRatio = qApp->devicePixelRatio();
auto pixmap = QPixmap(size * pixelRatio);
pixmap.setDevicePixelRatio(pixelRatio);
pixmap.fill(Qt::transparent);
return pixmap;
}
// Create and return a pixmap for the groove, using the provided size.
/**
* @private
* @brief Make a groove pixmap of the given size
*
* @param size
* @return auto
*/
auto makeGroovePixmap(QSize const size) {
if (auto pixmap = makePixmap(size); pixmap.isNull()) {
return pixmap;
} else {
auto const rect = QRect(QPoint(), size);
auto gradient = QLinearGradient(rect.left(), rect.center().y(),
rect.right(), rect.center().y());
gradient.setColorAt(0, grooveColor.darker(110));
gradient.setColorAt(1, grooveColor.lighter(110));
QPainter p(&pixmap);
p.setRenderHint(QPainter::Antialiasing, true);
p.translate(0.5, 0.5);
p.setPen(outlineColor);
p.setBrush(gradient);
p.drawRoundedRect(rect.adjusted(1, 1, -2, -2), 1, 1);
return pixmap;
}
}
// Create and return a pixmap for the groove active highlight, using
// the provided size.
/**
* @private
* @brief Make an active groove pixmap of the given size
*
* @param size
* @return auto
*/
auto makeActivePixmap(QSize const size) {
if (auto pixmap = makePixmap(size); pixmap.isNull()) {
return pixmap;
} else {
auto const rect = QRect(QPoint(), size);
auto gradient = QLinearGradient(rect.left(), rect.center().y(),
rect.right(), rect.center().y());
gradient.setColorAt(0, activeColor);
gradient.setColorAt(1, activeColor.lighter(130));
QPainter p(&pixmap);
p.setRenderHint(QPainter::Antialiasing, true);
p.translate(0.5, 0.5);
p.setPen(outlineColor);
p.setBrush(gradient);
p.drawRoundedRect(rect.adjusted(1, 1, -2, -2), 1, 1);
p.setPen(Qt::darkGray);
p.setBrush(Qt::NoBrush);
p.drawRoundedRect(rect.adjusted(2, 2, -3, -3), 1, 1);
return pixmap;
}
}
// Create and return a slider handle, using the provided size.
/**
* @private
* @brief Make a handle pixmap of the given size
*
* @param size
* @return auto
*/
auto makeHandlePixmap(QSize const size) {
if (auto pixmap = makePixmap(size); pixmap.isNull()) {
return pixmap;
} else {
auto const rect = QRect(QPoint(), size);
auto const r = rect.adjusted(1, 1, -2, -2);
auto const gradRect = rect.adjusted(2, 2, -2, -2);
auto gradient =
QLinearGradient(gradRect.center().x(), gradRect.top(),
gradRect.center().x(), gradRect.bottom());
gradient.setColorAt(0, handleStartColor);
gradient.setColorAt(1, handleStopColor);
QPainter p(&pixmap);
p.setRenderHint(QPainter::Antialiasing, true);
p.translate(0.5, 0.5);
p.setPen(Qt::NoPen);
p.setBrush(QColor(0, 0, 0, 40));
p.drawRect(r.adjusted(-1, 2, 1, -2));
p.setPen(outlineColor);
p.setBrush(gradient);
p.drawRoundedRect(r, 2, 2);
p.setBrush(Qt::NoBrush);
p.setPen(contrastColor);
p.drawRoundedRect(r.adjusted(1, 1, -1, -1), 2, 2);
p.setPen(QColor(0, 0, 0, 10));
p.drawLine(QPoint(r.left() + 2, r.bottom() + 1),
QPoint(r.right() - 2, r.bottom() + 1));
p.drawLine(QPoint(r.right() + 1, r.bottom() - 3),
QPoint(r.right() + 1, r.top() + 4));
p.drawLine(QPoint(r.right() - 1, r.bottom()),
QPoint(r.right() + 1, r.bottom() - 2));
return pixmap;
}
}
// Convenience type definition for the three element-specific pixmap
// creation functions above.
using MakePixmap = QPixmap (*)(QSize);
// Look for a matching pixmap in the global pixmap cache, returning it
// if found, creating and caching it if it wasn't present in the cache.
// Note that the cache is of limited size, so a pixmap not being present
// doesn't mean we've never created one; it could have been purged since
// the last time we did so.
/**
* @private
* @brief Get a cached pixmap, or create and cache it if not present
*
* @param size
* @param name
* @param make
* @return auto
*/
auto cachedPixmap(QSize const size, const char *const name,
MakePixmap const make) {
QPixmap pixmap;
if (auto const key = QString("attenuation_slider_%1(%2,%3)")
.arg(name)
.arg(size.width())
.arg(size.height());
!QPixmapCache::find(key, &pixmap)) {
pixmap = make(size);
QPixmapCache::insert(key, pixmap);
}
return pixmap;
}
} // namespace
/******************************************************************************/
// Public Implementation
/******************************************************************************/
// The default QSlider implementation is platform style specific, and is
// unfortunately very inconsistent in application of custom styling, which
// makes it difficult to achieve our goal of making it look like an audio
// fader control.
//
// The platform implementations work by delegating to a platform style in
// their paintEvent(). Here, we instead just draw everything in a custom
// manner, regardless of the platform style, so it should look the same
// on every platform.
//
// Note that as opposed to the standard QSlider, we ignore horizontal
// orientation here; a fader control is always vertical in orientation.
/**
* @brief Paint the attenuation slider
*
* @param
*/
void AttenuationSlider::paintEvent(QPaintEvent *) {
auto const handle = QRect(QPoint((rect().width() - handleSize.width()) / 2,
yValue(sliderPosition())),
handleSize);
auto const groove = QRect((rect().width() - grooveWidth) / 2,
rect().y() + handleSize.height() / 2, grooveWidth,
rect().height() - handleSize.height());
QPainter p(this);
// Set color for tick marks and attenuation text.
p.setPen(Qt::black);
// Draw groove.
p.drawPixmap(groove.topLeft(),
cachedPixmap(groove.size(), "groove", &makeGroovePixmap));
// Draw groove active highlight, clipping it to the active portion;
// we want to draw the full size and clip here so that we can take
// advantage of pixmap caching. Note that the standard control puts
// this above the handle, not below, but we're attenuating here, so
// below is what makes sense for us.
auto const clipRect =
QRect(QPoint(groove.left(), handle.bottom()), groove.bottomRight());
p.save();
p.setClipRect(clipRect.adjusted(0, 0, 1, 1), Qt::IntersectClip);
p.drawPixmap(groove.topLeft(),
cachedPixmap(groove.size(), "active", &makeActivePixmap));
p.restore();
// Draw tick marks, if any are specified. Typically, both sides.
if (auto const position = tickPosition(); position != NoTicks) {
auto const left = position & TicksLeft;
auto const right = position & TicksRight;
for (auto value = minimum(); value <= maximum();
value += tickInterval()) {
auto const y = yValue(value) + handleSize.height() / 2;
if (left)
p.drawLine(rect().left(), y, rect().left() + tickLength, y);
if (right)
p.drawLine(rect().right(), y, rect().right() - tickLength, y);
}
}
// Draw slider handle and attenuation level text; our value is 10x
// that of the attenuation level in dB. Note that we don't do anything
// special here for keyboard focused state, the computation for which is:
//
// hasFocus() && window()->testAttribute(Qt::WA_KeyboardFocusChange)
//
// However, if we wanted to do so, one option would be to do something
// like inverting the pixmap, tinting it, etc., or we could use a
// different pen color for the text.
p.drawPixmap(handle.topLeft(),
cachedPixmap(handle.size(), "handle", &makeHandlePixmap));
p.drawText(handle, Qt::AlignCenter, QString::number(-(value() / 10.0)));
}
// Given an attenuation value, compute and return the corresponding Y value.
/**
* @brief Given an attenuation value, compute and return the corresponding Y
* value
*
* @param value
* @return int
*/
int AttenuationSlider::yValue(int const value) const {
return QStyle::sliderPositionFromValue(
minimum(), maximum(), value, rect().height() - handleSize.height(),
!invertedAppearance());
}
/******************************************************************************/