views:

392

answers:

3

I'm using a JSlider to show progress in my application, so that it updates as a certain process computes.

I want a user to be able to drag the thumb backwards to a tick mark in the past, but when this happens, I want the progress indicator to remain at its current position.

// A dismal attempt at drawing the situation
Progress: ---------
Thumb:            |

// User drags backwards.  What I have:
Progress: ---
Thumb:       |

// What I want instead:
Progress: ---------
Thumb:       |

Can anyone offer any guidance for how to best accomplish this? Thanks in advance for all help!

A: 

Well I don't see how this is possible. You will need to customize the slider UI and change the model because no you need to store two pieces of model information, the "track value" and the "thumb value".

If you want a big hack, then you paint two sliders on top of one another. The bottom slider would be for the track value and the top would be for the thumb value.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.plaf.metal.*;

public class SliderTest extends JFrame
{
    public SliderTest()
    {
     JSlider bottom = createSlider();
     bottom.setUI( new MySliderUI1() );

     JSlider top = createSlider();
     top.setUI( new MySliderUI2() );
     top.setOpaque(false);

     JPanel panel = new JPanel();
     panel.setLayout( new OverlapLayout() );
     panel.add( bottom );
     panel.add( top );

     getContentPane().add( panel );
    }

    private JSlider createSlider()
    {
     JSlider slider = new JSlider(2, 50);
     slider.createStandardLabels(10, 5);
     slider.setMajorTickSpacing(10);
     slider.setPaintTicks(true);
     slider.setPaintLabels(true);
     slider.setValue(20);

     return slider;
    }

    class MySliderUI1 extends MetalSliderUI
    {
     public void paintThumb(Graphics g) {}
    }

    class MySliderUI2 extends MetalSliderUI
    {
     public void paintTrack(Graphics g) {}
    }

    public static void main(String[] args)
    {
     JFrame frame = new SliderTest();
     frame.setDefaultCloseOperation( EXIT_ON_CLOSE );
     frame.pack();
     frame.setLocationRelativeTo( null );
     frame.setVisible( true );
     }
}

You will also need the Overlap Layout. How you keep them in sync would be up to you. The solution has many flaws, but it may give you some ideas.

camickr
I went with a solution close to what you first talked about-- extending JSlider to track the progress and overriding the MetalSliderUI to paint the track to the point I wanted. Thanks for the help.
Cuga
+1  A: 

Ah, I see what you mean. Unfortunately, there's no way to do that.

The 'blue bar" is not really part of JSlider, it's part of the Metal LAF. It's just the way the Metal LAF chose to paint the component. If you try it with another LAF you don't get the bar at all.

You'll need to couple a JProgressBar and a JSlider to get what you want.

Here's a starting point. You'll need to tweak it to get it to look right

import java.awt.*;
import java.awt.event.ActionEvent;
import javax.swing.*;
import javax.swing.event.ChangeListener;

public class ProgressSlider extends javax.swing.JPanel
{
    private JProgressBar progressBar;
    private JSlider slider;
    public ProgressSlider() {
        this(0, 100);
    }
    public ProgressSlider(int min, int max) {
        setLayout(new GridBagLayout());

        GridBagConstraints gbc;
        gbc = new GridBagConstraints();
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        slider = new JSlider(min, max, min);
        progressBar = new JProgressBar(min, max);
        add(slider, gbc);
        add(progressBar, gbc);
    }
    public void setValue(int n) {
        boolean adjSlider = slider.getValue() == progressBar.getValue();
        progressBar.setValue(n);
        if (adjSlider)
            slider.setValue(n);
    }
    public int getValue() {
        return progressBar.getValue();
    }
    public int getSliderValue() {
        return slider.getValue();
    }
    public void syncSlider() {
        slider.setValue(progressBar.getValue());
    }
    public void addChangeListener(ChangeListener l) {
        slider.addChangeListener(l);
    }
    public void removeChangeListener(ChangeListener l) {
        slider.removeChangeListener(l);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                JFrame jf = new JFrame();
                jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                final ProgressSlider ps = new ProgressSlider();
                final JTextField tf = new JTextField(4);
                final JButton jb = new JButton(new AbstractAction("Set") {
                    public void actionPerformed(ActionEvent e) {
                        ps.setValue(Integer.parseInt(tf.getText()));
                    }});
                JPanel jp = new JPanel();
                jp.add(tf);
                jp.add(jb);
                jf.getContentPane().add(ps, BorderLayout.CENTER);
                jf.getContentPane().add(jp, BorderLayout.SOUTH);
                jf.pack();
                jf.setVisible(true);
            }
        });
    }
}
Devon_C_Miller
I see what you were saying about the Metal LAF. I decided to extend its paintTrack() method per the solution I posted. Thanks for the help.
Cuga
+1  A: 

I figured it out. I extended JSlider to keep track of the progress separately from the current thumb position, and then overrode the MetalSliderUI's paintTrack method to draw fill the track to the position I want.

Here's the solution, stripped down the important parts.

The new progress bar:

public class ProgressSlider extends JSlider {
    protected int progress;
    private static final String uiClassID = "ProgressSliderUI";

    public ProgressSlider() {
        progress = 0;
        putClientProperty("JSlider.isFilled", Boolean.TRUE);
        updateUI();
    }
    public void updateUI() {
        setUI(new ProgressSliderUI(this));
    }
    public String getUIClassID() {
        return uiClassID;
    }

    public int getProgress() {
        return this.progress;
    }

    public void setProgress(int value) {
        if (value < this.getMinimum()) {
            this.progress = this.getMinimum();
        }
        else if (value > this.getMaximum()) {
            this.progress = this.getMaximum();
        }
        else {
            this.progress = value;
        }
    }
}

The new UI: Note, there were only 2 lines added to the paintTrack() method in the UI class, immediately following the comment saying such.

public class ProgressSliderUI extends MetalSliderUI {

    public ProgressSliderUI() {
        super();
    }

    public ProgressSliderUI(JSlider b) {
    }

    @Override
    public void paintTrack(Graphics g) {
        Color trackColor = !slider.isEnabled() ? MetalLookAndFeel
                .getControlShadow() : Color.blue;

        boolean leftToRight = true;

        g.translate(trackRect.x, trackRect.y);

        int trackLeft = 0;
        int trackTop = 0;
        int trackRight = 0;
        int trackBottom = 0;

        // Draw the track
        if (slider.getOrientation() == JSlider.HORIZONTAL) {
            trackBottom = (trackRect.height - 1) - getThumbOverhang();
            trackTop = trackBottom - (getTrackWidth() - 1);
            trackRight = trackRect.width - 1;
        }
        else {
            if (leftToRight) {
                trackLeft = (trackRect.width - getThumbOverhang())
                        - getTrackWidth();
                trackRight = (trackRect.width - getThumbOverhang()) - 1;
            }
            else {
                trackLeft = getThumbOverhang();
                trackRight = getThumbOverhang() + getTrackWidth() - 1;
            }
            trackBottom = trackRect.height - 1;
        }

        if (slider.isEnabled()) {
            g.setColor(MetalLookAndFeel.getControlDarkShadow());
            g.drawRect(trackLeft, trackTop, (trackRight - trackLeft) - 1, 
                                           (trackBottom - trackTop) - 1);

            g.setColor(MetalLookAndFeel.getControlHighlight());
            g.drawLine(trackLeft + 1, trackBottom, trackRight, trackBottom);
            g.drawLine(trackRight, trackTop + 1, trackRight, trackBottom);

            g.setColor(MetalLookAndFeel.getControlShadow());
            g.drawLine(trackLeft + 1, trackTop + 1, 
                                            trackRight - 2, trackTop + 1);
            g.drawLine(trackLeft + 1, trackTop + 1, trackLeft + 1, 
                                                          trackBottom - 2);
        }
        else {
            g.setColor(MetalLookAndFeel.getControlShadow());
            g.drawRect(trackLeft, trackTop, (trackRight - trackLeft) - 1, 
                                            (trackBottom - trackTop) - 1);
        }

        // Draw the fill
        if (filledSlider) {
            int middleOfThumb = 0;
            int fillTop = 0;
            int fillLeft = 0;
            int fillBottom = 0;
            int fillRight = 0;

            if (slider.getOrientation() == JSlider.HORIZONTAL) {
                middleOfThumb = thumbRect.x + (thumbRect.width / 2);
                middleOfThumb -= trackRect.x; // To compensate for the
                // g.translate()
                fillTop = !slider.isEnabled() ? trackTop : trackTop + 1;
                fillBottom = !slider.isEnabled() ? trackBottom - 1
                        : trackBottom - 2;

                if (!drawInverted()) {
                    fillLeft = !slider.isEnabled() ? trackLeft : trackLeft + 1;

                    // THIS IS THE CHANGE OF NOTE:
                    // Fills the progress with the value from the custom slider
                    fillRight = xPositionForValue(((ProgressSlider) slider)
                            .getProgress());
                    fillRight -= trackRect.x;
                }
                else {
                    fillLeft = middleOfThumb;
                    fillRight = !slider.isEnabled() ? trackRight - 1
                            : trackRight - 2;
                }
            }
            else {
                middleOfThumb = thumbRect.y + (thumbRect.height / 2);
                middleOfThumb -= trackRect.y; // To compensate for the
                // g.translate()
                fillLeft = !slider.isEnabled() ? trackLeft : trackLeft + 1;
                fillRight = !slider.isEnabled() ? trackRight - 1
                        : trackRight - 2;

                if (!drawInverted()) {
                    fillTop = middleOfThumb;
                    fillBottom = !slider.isEnabled() ? trackBottom - 1
                            : trackBottom - 2;
                }
                else {
                    fillTop = !slider.isEnabled() ? trackTop : trackTop + 1;
                    fillBottom = middleOfThumb;
                }
            }

            if (slider.isEnabled()) {
                g.setColor(slider.getBackground());
                g.drawLine(fillLeft, fillTop, fillRight, fillTop);
                g.drawLine(fillLeft, fillTop, fillLeft, fillBottom);

                g.setColor(trackColor);
                g.fillRect(fillLeft + 1, fillTop + 1, fillRight
                                - fillLeft, fillBottom - fillTop);
            }
            else {
                g.setColor(MetalLookAndFeel.getControlShadow());
                g.fillRect(fillLeft, fillTop, fillRight - fillLeft, 
                                      trackBottom - trackTop);
            }
        }

        g.translate(-trackRect.x, -trackRect.y);
    }

}

And for the driver to test it:

public class ProgressExample extends JFrame
{
    public ProgressExample()
    {
        super("Progress Example");

        ProgressSlider mSlider = new ProgressSlider();
        mSlider.setMinimum(0);
        mSlider.setMaximum(100);
        mSlider.setValue(20);
        mSlider.setProgress(50);

        getContentPane().setLayout(new FlowLayout());
        getContentPane().add(slider);
        getContentPane().add(mSlider);
    }
    public static void main(String args[])
    {
        ProgressExample f = new ProgressExample();
        f.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e)
            {
                System.exit(0);
            }
        });
        f.setSize(300, 80);
        f.show();
    }
}
Cuga