Multiple JSliders reacting to each other to always equal 100 percent

java, swing

Solution

An approach, maybe it is useful for someone:

package stackoverflow;

import java.awt.GridLayout;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class ConnectedSliders
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI()
    {
        JSlider s0 = new JSlider(0, 100, 30);
        JSlider s1 = new JSlider(0, 100, 40);
        JSlider s2 = new JSlider(0, 100, 30);

        SliderGroup sliderGroup = new SliderGroup();
        sliderGroup.add(s0);
        sliderGroup.add(s1);
        sliderGroup.add(s2);

        JPanel panel =new JPanel(new GridLayout(0,2));
        panel.add(s0);
        panel.add(createListeningLabel(s0));
        panel.add(s1);
        panel.add(createListeningLabel(s1));
        panel.add(s2);
        panel.add(createListeningLabel(s2));

        panel.add(createListeningLabel(s0, s1, s2));

        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().add(panel);
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static JLabel createListeningLabel(final JSlider ... sliders)
    {
        final JLabel label = new JLabel("");
        for (JSlider slider : sliders)
        {
            slider.addChangeListener(new ChangeListener()
            {
                @Override
                public void stateChanged(ChangeEvent e)
                {
                    int sum = 0;
                    for (JSlider slider : sliders)
                    {
                        sum += slider.getValue();
                    }
                    label.setText("Sum: "+sum);
                }
            });
        }
        return label;
    }

    private static JLabel createListeningLabel(final JSlider slider)
    {
        final JLabel label = new JLabel("");
        slider.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent e)
            {
                label.setText(String.valueOf(slider.getValue()));
            }
        });
        return label;
    }


}


class SliderGroup
{
    private final Map<JSlider, Integer> values;
    private final LinkedList<JSlider> candidates;

    private final ChangeListener changeListener;
    private boolean updating = false;

    SliderGroup()
    {
        this.values = new HashMap<JSlider, Integer>();
        this.candidates = new LinkedList<JSlider>();

        changeListener = new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent e)
            {
                JSlider source = (JSlider)e.getSource();
                update(source);
            }
        };
    }

    private void update(JSlider source)
    {
        if (updating)
        {
            return;
        }
        updating = true;

        int delta = source.getValue() - values.get(source);
        if (delta > 0)
        {
            distributeRemove(delta, source);
        }
        else
        {
            distributeAdd(delta, source);
        }


        for (JSlider slider : candidates)
        {
            values.put(slider, slider.getValue());
        }

        updating = false;
    }


    private void distributeRemove(int delta, JSlider source)
    {
        int counter = 0;
        int remaining = delta;
        while (remaining > 0)
        {
            JSlider slider = candidates.removeFirst();
            counter++;

            if (slider == source)
            {
                candidates.addLast(slider);
            }
            else
            {
                if (slider.getValue() > 0)
                {
                    slider.setValue(slider.getValue()-1);
                    remaining--;
                    counter = 0;
                }
                candidates.addLast(slider);
                if (remaining == 0)
                {
                    break;
                }
            }
            if (counter > candidates.size())
            {
                String message = "Can not distribute "+delta+" among "+candidates;
                //System.out.println(message);
                //return;
                throw new IllegalArgumentException(message);
            }
        }
    }

    private void distributeAdd(int delta, JSlider source)
    {
        int counter = 0;
        int remaining = -delta;
        while (remaining > 0)
        {
            JSlider slider = candidates.removeLast();
            counter++;

            if (slider == source)
            {
                candidates.addFirst(slider);
            }
            else
            {
                if (slider.getValue() < slider.getMaximum())
                {
                    slider.setValue(slider.getValue()+1);
                    remaining--;
                    counter = 0;
                }
                candidates.addFirst(slider);
                if (remaining == 0)
                {
                    break;
                }
            }
            if (counter > candidates.size())
            {
                String message = "Can not distribute "+delta+" among "+candidates;
                //System.out.println(message);
                //return;
                throw new IllegalArgumentException(message);
            }
        }
    }


    void add(JSlider slider)
    {
        candidates.add(slider);
        values.put(slider, slider.getValue());
        slider.addChangeListener(changeListener);
    }

    void remove(JSlider slider)
    {
        candidates.remove(slider);
        values.remove(slider);
        slider.removeChangeListener(changeListener);
    }

}

The core is the `SliderGroup` class (analogous to a `ButtonGroup`): Several JSliders may be added. Changes in one Slider will be distributed among the others. As mentioned in a comment, this distribution is somewhat tricky: When one repeatedly increases the value of Slider A by 1 (e.g. by pressing the right-arrow-cursor key), then the values of the other sliders have to be decreased repeatedly - but it should NOT chose always the same slider to receive this change, otherwise only one slider would move. I solved this using a LinkedList of "candidate" Sliders that receive the change. When the value of the other sliders has to be DEcreased, then the changes are distributed among these candidate sliders, from left to right. The sliders that received a change are put at the end of the list, so that when their values have to be DEcreased again, the slider that received the previous DEcrease will be the last in the list of candidates. (The same is done, the other way around, when their values have to be INcreased). Seems to be working well, but is not tested thoroughly: The behavior for the cases that the sliders would be enforced to have "invalid" values (e.g. when A and B have their MIN value, but C does not have its MAX value, and is increased) probably depends on the application case. At the moment, an exception is thrown in this case, but simply "ignoring" this case might also be appropriate...

Problem

I'm attempting to add 3 JSliders to a java swing application, such that the total value across the three sliders always adds up to 100. Each slider is a probability, slider A is the probability of adding a value to a queue, slider B is the probability of removing a value from a queue, and slider C is the probability of nothing happening. Example: Slider A is set to 40, Slider B is set to 40, and slider C is set to 20 initially. If the user changes Slider A to 50 I want the values of Slider B and C to both decrease by 5. Such that the total is still 100. I understand that there may be some issues when the user changes the value to say, 41, because I'd like the other values to remain as whole integers. Ok, so I've added some code, after reviewing some information concerning BoundedRangeModel. I've managed to get the sliders to change based on the otherse, but I'm still unsure as to how I can implement there initial values. Also, the sliders are only seeming to work in pairs, as opposed to a group of 3. I'll keep working on it and if I come up with a solution that I'm satisfied with using this method I'll post it here. ``` package stackoverflow; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.BoundedRangeModel; import javax.swing.DefaultBoundedRangeModel; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSlider; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; public class Controller { public static void main(String[] args) { //GUI up Frame f = new Frame(); f.setSize(600, 600); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setVisible(true); } @SuppressWarnings("serial") public static class Frame extends JFrame implements PropertyChangeListener, ChangeListener { //Sliders private JSlider s1; private JSlider s2; private JSlider s3; //Initial Values for fields (not sure how to implement) private int inis1 = 45; private int inis2 = 40; private int inis3 = 15; //Labels private JLabel s1Label; private JLabel s2Label; private JLabel s3Label; //Strings for labels private static String s1String = "Probability of adding one to queue"; private static String s2String = "Probability of subtracting one from queue"; private static String s3String = "Probability of doing nothing"; public Frame() { //title of window super("Sliders"); //Layout //Label Panel, GridBag Setup JPanel pane = new JPanel(new GridBagLayout()); this.add(pane); GridBagConstraints c = new GridBagConstraints(); class LimitedBoundedRangeModel extends DefaultBoundedRangeModel { BoundedRangeModel limit; public LimitedBoundedRangeModel(BoundedRangeModel limit) { this.limit = limit; } /** * @inherited <p> */ @Override public void setRangeProperties(int newValue, int newExtent, int newMin, int newMax, boolean adjusting) { if (limit != null) { int combined = newValue + limit.getValue(); if (combined > newMax) { newValue = newMax - limit.getValue(); } } boolean invoke = (adjusting != getValueIsAdjusting()) && !adjusting; super.setRangeProperties(newValue, newExtent, newMin, newMax, adjusting); if (invoke) { SwingUtilities.invokeLater(new Runnable() { public void run() { fireStateChanged(); } }); } super.setRangeProperties(newValue, newExtent, newMin, newMax, adjusting); } } // use LimitedBoundedRangeModel firstModel = new LimitedBoundedRangeModel(null); LimitedBoundedRangeModel secondModel = new LimitedBoundedRangeModel(firstModel); LimitedBoundedRangeModel thirdModel = new LimitedBoundedRangeModel(secondModel); firstModel.limit = secondModel; secondModel.limit = thirdModel; thirdModel.limit = firstModel; s1 = new JSlider(firstModel); c.gridx = 0; c.gridy = 2; c.gridwidth = 5; pane.add(s1, c); s2 = new JSlider(secondModel); c.gridx = 0; c.gridy = 5; c.gridwidth = 5; pane.add(s2, c); s3 = new JSlider(thirdModel); c.gridx = 0; c.gridy = 7; c.gridwidth = 5; pane.add(s3, c); s1Label = new JLabel(s1String); c.gridx = 0; c.gridy = 2; c.gridwidth = 5; pane.add(s1Label, c); s2Label = new JLabel(s2String); c.gridx = 0; c.gridy = 4; c.gridwidth = 5; pane.add(s2Label, c); s3Label = new JLabel(s3String); c.gridx = 0; c.gridy = 6; c.gridwidth = 5; pane.add(s3Label, c); //enable 'tick' markers s1.setMajorTickSpacing(25); s1.setMinorTickSpacing(1); s1.setPaintTicks(true); s1.setPaintLabels(true); s1.setSnapToTicks(true); s2.setMajorTickSpacing(25); s2.setMinorTickSpacing(1); s2.setPaintTicks(true); s2.setPaintLabels(true); s2.setSnapToTicks(true); s3.setMajorTickSpacing(25); s3.setMinorTickSpacing(1); s3.setPaintTicks(true); s3.setPaintLabels(true); s3.setSnapToTicks(true); //Listening to sliders s1.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent ce) { JSlider source = (JSlider)ce.getSource(); if (!source.getValueIsAdjusting()) { int P = (int)source.getValue(); System.out.println("Probability of adding to queue is now: " + P); /* * TODO * Would setting the values of the other sliders here, * instead of using the BoundedRangeModel work? * Tricky math would have to be used. */ // s2Slider.setValue(); } } }); s2.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent ce) { JSlider source = (JSlider)ce.getSource(); if (!source.getValueIsAdjusting()) { int P = (int)source.getValue(); System.out.println("Probability of subtracting from queue is now: " + P); } } }); s3.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent ce) { JSlider source = (JSlider)ce.getSource(); if (!source.getValueIsAdjusting()) { int P = (int)source.getValue(); System.out.println("Probability of doing nothing to the queue is now: " + P); } } }); } @Override public void propertyChange(PropertyChangeEvent evt) { // TODO Auto-generated method stub } @Override public void stateChanged(ChangeEvent e) { // TODO Auto-generated method stub } } } ```

Original source