Occasionally its needed to have a slider that can be used to increment/decrement values on drag, so that you could wire it up to function as a ‘trim’ control for other parameters such as a set of EQ or Volume faders that you might want to ‘bump’ a little or ‘trim’ a little.
Here’s how to do that with a regular Slider component - note that scrollImage.png is a little .png containing ‘stroke lines’ that render normal trim steps, transformed on movement to give user feedback:
//
// EndlessSlider.h
// PolarDesigner - All
//
// Created by Jay Vaughan on 23.02.22.
// Copyright © 2022 Austrian Audio. All rights reserved.
//
// This Component implements an 'endless slider', which can be useful for implementing
// 'trim' controls, i.e. for applying trim to a set of EQ's, volume sliders, etc.
//
// To adjust the rate of trim, use the EndlessSlider.step value in the slider inc/dec
// callbacks.
#ifndef EndlessSlider_h
#define EndlessSlider_h
class EndlessSlider : public Slider {
public:
EndlessSlider () :
Slider()
{
setChangeNotificationOnlyOnRelease(false);
sliderImage = getImageFromAssets("scrollImage.png");
setTextBoxStyle (Slider::NoTextBox, false, 0, 0);
setSliderStyle (Slider::LinearBarVertical);
setScrollWheelEnabled(true);
};
// Trim step value - modify it freely as needed
double step = 0.012725f;
// set these callbacks where you use this class in order to get inc/dec messages
std::function<void()> sliderIncremented;
std::function<void()> sliderDecremented;
// calculate whether to callback to an increment or decrement, and update UI
void mouseDrag(const MouseEvent &e) override
{
int currentMoved;
static int lastMoved;
if (e.mouseWasDraggedSinceMouseDown()) {
currentMoved = e.getDistanceFromDragStartY();
sliderImageTransform = (AffineTransform::translation ((float) (sliderImage.getWidth()),
(float) (sliderImage.getHeight()) + currentMoved)
.followedBy (getTransform()));
if ((currentMoved > lastMoved)){
sliderDecremented();
} else
if (currentMoved < lastMoved) {
sliderIncremented();
}
lastMoved = currentMoved;
repaint();
}
}
void paint (Graphics&g) override
{
Rectangle<int> bounds = getLocalBounds();
Path endlessPath;
g.setColour(getRandomColour());
g.setFillType(juce::FillType(sliderImage, sliderImageTransform));
g.fillRect(bounds);
}
void mouseExit (const MouseEvent& e) override
{
repaint();
}
~EndlessSlider () {}
void resized() override
{
Slider::resized();
auto& lf = getLookAndFeel();
auto layout = lf.getSliderLayout (*this);
sliderRect = layout.sliderBounds;
}
private:
Rectangle<int> sliderRect;
Image sliderImage;
AffineTransform sliderImageTransform;
// utility functions - from DemoRunner utilities
static juce::Colour getRandomColour()
{
auto& random = juce::Random::getSystemRandom();
return juce::Colour ((juce::uint8) random.nextInt (256),
(juce::uint8) random.nextInt (256),
(juce::uint8) random.nextInt (256));
}
// creats a usable image asset from a file stream
inline std::unique_ptr<InputStream> createAssetInputStream (const char* resourcePath)
{
#if JUCE_ANDROID
ZipFile apkZip (File::getSpecialLocation (File::invokedExecutableFile));
return std::unique_ptr<InputStream> (apkZip.createStreamForEntry (apkZip.getIndexOfFileName ("assets/" + String (resourcePath))));
#else
#if JUCE_IOS || JUCE_MAC
auto assetsDir = File::getSpecialLocation (File::currentExecutableFile)
.getParentDirectory().getChildFile ("Assets");
#endif
auto resourceFile = assetsDir.getChildFile (resourcePath);
jassert (resourceFile.existsAsFile());
return resourceFile.createInputStream();
#endif
}
// creates an image asset from cache if possible
inline Image getImageFromAssets (const char* assetName)
{
auto hashCode = (String (assetName) + "@endless_slider_assets").hashCode64();
auto img = ImageCache::getFromHashCode (hashCode);
if (img.isNull())
{
std::unique_ptr<InputStream> juceIconStream (createAssetInputStream (assetName));
if (juceIconStream == nullptr)
return {};
img = ImageFileFormat::loadFrom (*juceIconStream);
ImageCache::addImageToCache (img, hashCode);
}
return img;
}
};
#endif /* EndlessSlider_h */
scrollImage.png:
Note how to use it somewhere:
EndlessSlider trimSlider;
// ...
addAndMakeVisible(&trimSlider);
trimSlider.sliderIncremented = [this] { incrementTrim(); };
trimSlider.sliderDecremented = [this] { decrementTrim(); };
// ...
// example - apply trim to a set of EQ bands:
// Handle the trimSlider increment/decrement calls by appling the trimSlider.step to the active EQ bands
void SomeProcessorEditor::incrementTrim() {
for (int i = 0; i < nActiveBands; i++)
{
slDir[i].setValue(slDir[i].getValue() + trimSlider.step);
}
}
void SomeProcessorEditor::decrementTrim() {
for (int i = 0; i < nActiveBands; i++)
{
slDir[i].setValue(slDir[i].getValue() - trimSlider.step);
}
}