2

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
    }
}

}
Waabbit
  • 25
  • 5
  • 4
    _"Where should I start?"_ - start by typing some code into your favorite IDE. Then come back here if you get stuck. – Paul Samsotha Jan 27 '14 at 18:15
  • 1
    When/if you do come back, have [**a Minimal, Complete, Tested and Readable example**](http://stackoverflow.com/help/mcve) for us. Then we will be able to assist your further. You seem to have the logic. Now just implement it. Without seeing any code, all we could really assist with is the logic, but you already seem to have it. So get started with the code. – Paul Samsotha Jan 27 '14 at 18:19
  • My apologies, I have _some_ code but struggling to implement the setting of variables, I'll edit the post with more information after attempting to isolate the code. Thanks for the link. – Waabbit Jan 27 '14 at 18:36
  • The tricky part might actually be to specify a "sensible" behavior. When Slider A is increased by 1, then either B or C may be DEcreased by 1. When Slider A is increased by 1 again, then ... it should probably not cause the same slider to be DEcreased again. In general, making the behavior independent of the "movement speed" and the sequence of sliders that are changed may not be soooo easy. – Marco13 Jan 27 '14 at 19:40
  • This isn't really a _programming_ problem ***yet***. Before you can write any code you have to decide on the behavior you want, and consider all the edge cases. There are at least 2 ways to distribute value changes: equal and proportional. For example, given slider values A:50 B:30 C:20, a change A->30 could result in B:40 C:30 (equal distribution) or B:42 C:28 (proportional). Dealing with rounding is another (trivial) issue as you already noted. You need to have worked out what you want before you ask questions here. – Jim Garrison Jan 27 '14 at 22:22

1 Answers1

3

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...

Marco13
  • 53,703
  • 9
  • 80
  • 159