javauser-interfaceswingjslider

Mimicking Zoom bar from Office 2007 in Java (Modified JSlider)


I'm attempting to mimic (or find a preexisting component) that mimics the zoom slider from Word 2007:

Two state zoom bar

There are two main differences between this component and a standard Java JSlider:

  1. Does not snap to ticks except at 100%, and snaps while you're sliding the bar rather than when you release mouse
  2. Slider is not linear the whole way across: The left half of slider goes from 10% to 100%; the right side goes from 100% to 500%.

Here is what I have so far: Java clone

Source:

    import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/**
 *
 * @author NDUNN
 * @date Nov 25, 2009
 */
public class ZoomBar extends JPanel implements ActionListener, ChangeListener {

    private JLabel zoomAmount;
    private JButton minus;
    private JButton plus;
    private JSlider slider;

    private static final int MIN_ZOOM = 10;
    private static final int MAX_ZOOM = 200;
    private static final int DEFAULT_ZOOM = 100;

    private static final int MAJOR_ZOOM_SPACING = 50;
    private static final int MINOR_ZOOM_SPACING = 10;

    public ZoomBar() {
        super();

        minus = new JButton("-");
        plus = new JButton("+");
        slider = new JSlider(MIN_ZOOM, MAX_ZOOM, DEFAULT_ZOOM);

        slider.setMinorTickSpacing(MINOR_ZOOM_SPACING);
        slider.setMajorTickSpacing(MAJOR_ZOOM_SPACING);
        slider.setPaintTicks(true);
        slider.setSnapToTicks(true);

        zoomAmount = new JLabel(slider.getValue() + "%");

        add(zoomAmount);
        add(minus);
        add(slider);
        add(plus);

        plus.addActionListener(this);
        minus.addActionListener(this);

        slider.addChangeListener(this);
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame("Zoom bar clone");
        frame.setContentPane(new ZoomBar());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }

    public void actionPerformed(ActionEvent e) {
        if (e.getSource() == plus) {
            slider.setValue(slider.getValue() + MINOR_ZOOM_SPACING);
        }
        else if (e.getSource() == minus) {
            slider.setValue(slider.getValue() - MINOR_ZOOM_SPACING);
        }
    }

    public void stateChanged(ChangeEvent e) {
        if (slider.getValueIsAdjusting()) {
            return;
        }
        zoomAmount.setText(slider.getValue() + "%");
    }

}

Basically just mimicking the look but without those two features mentioned above. I don't see anything in the JSlider API that allows me to get this behavior. Would I have to start from scratch to get this behavior? If so it's not worth my time, but if anyone knows of a good way to get this behavior, I think it'd be nice to have in the project I'm working on.

Thanks,

Nick


EDIT:

Thanks to PSpeed for idea about just mapping the 0..50 and 50..100 to different values. Code to do so is below.

Unfortunately the idea about changing setValue to snap did not work.

public void stateChanged(ChangeEvent e) {
    // While slider is moving, snap it to midpoint
    int value = slider.getValue();
    if (slider.getValueIsAdjusting()) {
        return;
    }

    zoomValue = fromSlider(value);
    zoomLabel.setText(zoomValue + "%");
}

public int fromSlider(int sliderValue) {
    int mappedValue = 0;
    if (sliderValue <= 50) {
        // Map from [0, 50] to [MIN ... DEFAULT]
        mappedValue = (int) map(sliderValue, 0, 50, MIN_ZOOM, DEFAULT_ZOOM);
    }
    else {
        // Convert from  (50, 100] to (DEFAULT ... MAX]
        mappedValue = (int) map(sliderValue, 50, 100, DEFAULT_ZOOM, MAX_ZOOM);
    }
    return mappedValue;
}

public int toSlider(int modelValue) {
    int mappedValue = 0;
    if (modelValue <= DEFAULT_ZOOM) {
        // Map from [MIN_ZOOM, DEFAULT_ZOOM] to [0 ... 50]
        mappedValue = (int) map(modelValue, MIN_ZOOM, DEFAULT_ZOOM, 0, 50);
    }
    else {
        // Convert from  (DEFAULT ... MAX] to (50, 100]
        mappedValue = (int) map(modelValue, DEFAULT_ZOOM, MAX_ZOOM, 50, 100);
    }
    return mappedValue;
}


/**
 * @param value The incoming value to be converted
 * @param low1  Lower bound of the value's current range
 * @param high1 Upper bound of the value's current range
 * @param low2  Lower bound of the value's target range
 * @param high2 Upper bound of the value's target range
 * @return
 */
public static final double map(double value, double low1, double high1, double low2, double high2) {

    double diff = value - low1;
    double proportion = diff / (high1 - low1);

    return lerp(low2, high2, proportion);
}

public static final double lerp(double value1, double value2, double amt) {
    return ((value2 - value1) * amt) + value1;
}

Edit 2: One other difference I notice is that a JButton doesn't let you hold it down to fire button multiple times, whereas the +/- buttons in office do. Any idea how to mimic this?


Solution

  • I believe you can get all of this behavior with a custom BoundedRangeModel. The key is to make the model report a normal type of range of values but treat it differently when you want your zoom factor.

    So for example if you let your range run from 0 - 100, you'd treat 0-50 one way and 50-100 another way (10-100% and 100-500% respectively).

    To get the snap behavior, I'm pretty sure you can just override setValue() to snap the range you want. So using 0-100 as the value range, if setValue() is called with 47-53 then just snap the value to 50.