Three Position Switch


#1

Hi,

I am trying to do a three-position switch with custom graphics.   It's a switch with a left, middle and right position.

I thought about overriding the button class, but once I got into it, it looked like more work than this would need to be.  Then, I thought about overriding the combobox class but would seem to be more complicated.

Also, I did alot of searching as it would seem that this kind of thing would already exist somewhere.

Any quick thoughts on the best way to accomplish this?  Anyone know of a three-position switch already out there?


How about to add this type of button?
#2

I’ve done it using a custom Slider derived class with a LookAndFeel for drawing the 3 state images.

Rail


#3

Thanks, that may be the best way to go about it.

I originally envisioned the user action to be clicking to cycle through, but I'm guessing it would be intuitive enough for the user to drag a 3 state image.  Does the action with your implementation feel pretty intuitive?

Another thought is that maybe I just need to redo the graphics to be a skinned combo box instead of doing a three position switch.  Although, I feel defeated when graphics must be redone because implementation is a challenge.

..trying to get all my thoughts out here in case someone else searches this topic and needs ideas.

Truly appreciate your reply as I always do here on the forum.  So great hearing from others when working on these things.


#4

My implementation works exactly as you would anticipate… you can either drag the switch to change selection or click left of center or right of center to change position… or above & below if it’s vertical.

Rail


#5

That really sounds spot on!

Would you mind to share it?  If you do, it's no problem, your help in pointing me towards the slider rather than cusomizing a button or combo box is already a big help. 


#6

+1 for the implementation using the Slider class.

Also, you could achieve the "discrete positions" feel by setting the interval in your setRange() call, i. e.

slider->setRange (0.0, 2.0, 1.0)

will give you a slider whose values will always be 0.0, 1.0 or 2.0 no matter how dragged/clicked, and the "thumb" of the slider will be discretely positioned at the left/middle/right position as well


#7

I can’t share it 'cause it’s set up to use ComponentBuilder… but it’s fairly simple…

I created a base class CSwitchComponent…

class CSwitchComponent : public Slider
{
public:
    
    CSwitchComponent();
    ~CSwitchComponent();
    
    void paint (Graphics& g) override;
    
    void mouseDown (const MouseEvent& event) override;
    
    void refreshFromValueTree (const ValueTree& tree, ComponentBuilder& builder); 
    
    int getNumberOfPoles() const noexcept               { return m_iNumberOfPoles;      }
    
    Image getImage (int iPole) const noexcept;
    
    void setImages (const String& szPole1Path, const String& szPole2Path, const String& szPole3Path);
    
    bool areImagesLoaded() const noexcept   { return m_bImagesLoaded;   }
    
    int  getSwitchPosition() const noexcept;
    
protected:
    
    CSliderSwitchLookAndFeel    m_Look;
    
    bool                        m_bIsHorizontal;
    
    double                      m_dSliderMin;
    double                      m_dSliderMax;
    double                      m_dSliderDefault;
    
private:
    
    StringArray                 m_ImagePathArray;
    
    int                         m_iNumberOfPoles;
    
    bool                        m_bImagesLoaded;
    
    Image                       m_Pole1Image;   // Left or Top
    Image                       m_Pole2Image;   // Center (for 3 pole, Right for 2 pole)
    Image                       m_Pole3Image;   // Right or Bottom (for 3 pole, not used for 2 pole)
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CSwitchComponent)
};

You can either have 3 separate Image objects or create an OwnedArray of Images…

I have 2 other classes: CHorizontalSwitchComponent & CVerticalSwitchComponent derived from CSwitchComponent which initialize the Slider either Horizontal or Vertical

void CHorizontalSwitchComponent::init()
{
    setSliderStyle (Slider::SliderStyle::LinearHorizontal);
    
    CSwitchComponent::setPopupDisplayEnabled (false, getParentComponent());
    
    m_bIsHorizontal = true;
}

The CSwitchComponent constructor initializes the Slider and sets the LookAndFeel class….

CSwitchComponent::CSwitchComponent() : m_bIsHorizontal  (true),
                                       m_dSliderMin     (0.0),
                                       m_dSliderMax     (100.0),
                                       m_dSliderDefault (0.0),
                                       m_iNumberOfPoles (2),
                                       m_bImagesLoaded  (false)
{
    setRange (m_dSliderMin, m_dSliderMax, 1.0);
    setValue (m_dSliderDefault);
    
    setTextBoxStyle (Slider::NoTextBox, true, 70, 35);
    
    setLookAndFeel (&m_Look);
}

in CSwitchComponent::setImages() check all the images are the same size and set the size of the object to the image size.

The other 2 relevant methods:

Image CSwitchComponent::getImage (int iPole) const noexcept
{
    switch (iPole)
    {
        case 1:     return m_Pole1Image;
        
        case 2:     return m_Pole2Image;
        
        case 3:     return m_Pole3Image;
    }
    
    return m_Pole1Image;
}
int CSwitchComponent::getSwitchPosition() const noexcept
{
    double dSliderValue = getValue();
    
    double division = 100.0 / (double) m_iNumberOfPoles;
    
    for (int i = 0; i < m_iNumberOfPoles; ++i)
        {
        if ((dSliderValue >= division * (double) i) && (dSliderValue <= division * (double) (i + 1)))
            return i + 1;
        }
    
    return -1;
}

and in the LookAndFeel…

void CSliderSwitchLookAndFeel::drawLinearSlider (Graphics& g, int x, int y, int width, int height,
                                                    float sliderPos, float minSliderPos, float maxSliderPos,
                                                    const Slider::SliderStyle style, Slider& slider)
{
    CSwitchComponent*  pSliderSwitch = dynamic_cast<CSwitchComponent*>(&slider);
    
    if (pSliderSwitch != nullptr)
        {
        int iNumberOfPoles = pSliderSwitch->getNumberOfPoles();
        
        if (! pSliderSwitch->areImagesLoaded())
            {
            LookAndFeel_V3::drawLinearSlider (g, x, y, width, height, sliderPos, minSliderPos, maxSliderPos, style, slider);
            
            return;
            }
        
        // Only worry about 2 and 3 poles for now...
        
        if (iNumberOfPoles < 2 || iNumberOfPoles > 3)
            return;
        
        Image theImage;
        
        theImage = pSliderSwitch->getImage (pSliderSwitch->getSwitchPosition());
        
        if (theImage.isValid())
            {
            g.drawImage (theImage, 0, 0, theImage.getWidth(), theImage.getHeight(), 0, 0, theImage.getWidth(), theImage.getHeight());
            }
        
        return;
        }
    
    LookAndFeel_V3::drawLinearSlider (g, x, y, width, height, sliderPos, minSliderPos, maxSliderPos, style, slider);
}

That should get you going.

Rail


#8

As many others I’ve also wanted to make something, especially for modes switching so I’ve made the following. it also has Listener friendly with AudioProcessorValueTree.

It still has some cases where it could be buggish but it’s useful for my needs allowing 2 modes:

  • buttons switching the exact same boundaries.
  • buttons aligned (imagine 1176 compression mode buttons).

It does however expect you to apply your own LookAndFeel :wink: