Question

Short:
Is possible to set an unknown value on a JSlider having no knob?

Long:
I'm working on a project that has a desktop client developed in Java Swing where the user is required to measure some parameters using a slider. That's a requirement, it has to be a slider.
Until the user interacts with the slider the value has to be unknown, and that means showing no knob. That's a requirement too.
When using a JSlider the knob shows always and is no way to set any value out of its bounds or set it to null as it uses the primitive type int and not the object Integer.
Is there a way to set it a null fail-value or at least some value that shows no knob? Is there some extension that would allow to do so?

Was it helpful?

Solution 2

With the great help of Harry Joy about hiding the knob, finally I've been able to solve the problem.

If you check Harry's answer you can read about overriding the method BasicSliderUI.paintThumb(Graphics) for hiding the knob. It works fine on most L&F's not based on Synth (and that means Nimbus), where the approach would be different, but doable via customizations on the L&F.

Installing it is a bit tricky: having a PropertyChangeListener on the UIManager that checks any change on the L&F and installs a proper UI delegate on the component does the magic (in this solution I'm just showing the one based in BasicSliderUI the others are copy-pastes). Also I've tweaked it a bit to make it set the value to the position where is first clicked. For the delegate to know if it must paint the knob or not I decided having a client property on the JSlider called "ready", which is to be a boolean. This way, the delegate can be installed on any JSlider, not only the one extended.

Now, how to manage unknown values? Adding another new property "selectedValue", this one is bound to both "value" and "ready" and of Integer type. "ready" is false or true depending on if it's null or not, and viceversa, and both "value" and "selectedValue" hold the same value (one being int and the other Integer) unless not ready, which would set the "selectedValue" to null. It might sound complicated, but it is not. Also there's a little tweak for clearing the value with [Esc] and propagating properties in the component implemented.

Here's the code:

BasicSliderUIExt

import java.awt.Graphics;
import java.awt.event.MouseEvent;
import javax.swing.JSlider;
import javax.swing.SwingConstants;
import javax.swing.plaf.basic.BasicSliderUI;

public class BasicSliderUIExt extends BasicSliderUI {

    public BasicSliderUIExt(JSlider slider) {
        super(slider);
    }

    @Override
    public void paintThumb(Graphics g) {
        if (isReady(super.slider)) {
            super.paintThumb(g); 
        }
    }

    @Override
    protected TrackListener createTrackListener(final JSlider slider) {
        return new TrackListener() {
            @Override
            public void mousePressed(MouseEvent event) {
                if (isReady(slider)) {
                    super.mousePressed(event);
                } else {
                    JSlider slider = (JSlider) event.getSource();
                    switch (slider.getOrientation()) {
                    case SwingConstants.VERTICAL:
                        slider.setValue(valueForYPosition(event.getY()));
                        break;
                    case SwingConstants.HORIZONTAL:
                        slider.setValue(valueForXPosition(event.getX()));
                        break;
                    }
                    super.mousePressed(event);
                    super.mouseDragged(event);
                }
            }

            @Override
            public boolean shouldScroll(int direction) {
                if (isReady(slider)) {
                    return super.shouldScroll(direction);
                }

                return false;
            }};
    }

    public static boolean isReady(JSlider slider) {
        Object ready = slider.getClientProperty(JSliderExt.READY_PROPERTY);
        return (ready == null) || (!(ready instanceof Boolean)) || (((Boolean) ready).booleanValue());
    }

}

JSliderExt

import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Constructor;
import javax.swing.BoundedRangeModel;
import javax.swing.JSlider;
import javax.swing.LookAndFeel;
import javax.swing.UIManager;
import javax.swing.plaf.SliderUI;

public class JSliderExt extends JSlider {

    private static final long serialVersionUID = 1L;

    public static final String EXTENT_PROPERTY = "extent";
    public static final String MAXIMUM_PROPERTY = "maximum";
    public static final String MINIMUM_PROPERTY = "minimum";
    public static final String READY_PROPERTY = "ready";
    public static final String SELECTED_VALUE_PROPERTY = "selectedValue";
    public static final String VALUE_PROPERTY = "value";

    public static final boolean READY_DEFAULT_VALUE = false;

    protected SliderUI uix = new BasicSliderUIExt(this);

    public JSliderExt(BoundedRangeModel model, boolean ready) {
        super(model);

        init(ready);
    }

    public JSliderExt(BoundedRangeModel model) {
        super(model);

        init(READY_DEFAULT_VALUE);
    }

    public JSliderExt(int orientation, int minimmum, int maximum, int value, boolean ready) {
        super(orientation, minimmum, maximum, value);

        init(ready);
    }

    public JSliderExt(int orientation, int minimmum, int maximum, int value) {
        super(orientation, minimmum, maximum, value);

        init(READY_DEFAULT_VALUE);
    }

    public JSliderExt(int minimmum, int maximum, int value, boolean ready) {
        super(minimmum, maximum, value);

        init(ready);
    }

    public JSliderExt(int minimmum, int maximum, int value) {
        super(minimmum, maximum, value);

        init(READY_DEFAULT_VALUE);
    }

    public JSliderExt(int minimmum, int maximum, boolean ready) {
        super(minimmum, maximum);

        init(ready);
    }

    public JSliderExt(int minimmum, int maximum) {
        super(minimmum, maximum);

        init(READY_DEFAULT_VALUE);
    }

    public JSliderExt(int orientation, boolean ready) {
        super(orientation);

        init(ready);
    }

    public JSliderExt(int orientation) {
        super(orientation);

        init(READY_DEFAULT_VALUE);
    }

    public JSliderExt(boolean ready) {
        super();

        init(ready);
    }

    public JSliderExt() {
        super();

        init(READY_DEFAULT_VALUE);
    }

    private void init(boolean ready) {
        UIManager.addPropertyChangeListener(new PropertyChangeListener() { // Changes the UI delegate in L&F change
            @Override
            public void propertyChange(PropertyChangeEvent event) {
                if ("lookAndFeel".equals(event.getPropertyName())) {
                    Object newValue = event.getNewValue();
                    if ((newValue != null) && (newValue instanceof LookAndFeel)) {
                        LookAndFeel lookAndFeel = (LookAndFeel) newValue;

                        try {
                            if (lookAndFeel instanceof MetalLookAndFeel) {
                                JSliderExt.this.uix = new MetalSliderUIExt();
                            } else if (lookAndFeel instanceof com.sun.java.swing.plaf.motif.MotifLookAndFeel) {
                                JSliderExt.this.uix = new MotifSliderUIExt(JSliderExt.this);
                            } else if (lookAndFeel instanceof com.sun.java.swing.plaf.windows.WindowsLookAndFeel) {
                                JSliderExt.this.uix = new WindowsSliderUIExt(JSliderExt.this);
                            } else {
                                throw new NullPointerException("Default Look & Feel not matched");
                            }
                        } catch (Exception e) {
                            try {
                                Package sliderPackage = JSliderExt.this.getClass().getPackage();
                                String lookAndFeelName = lookAndFeel.getName();
                                if (lookAndFeelName.equals("CDE/Motif")) {
                                    lookAndFeelName = "Motif";
                                } else if (lookAndFeelName.equals("Windows Classic")) {
                                    lookAndFeelName = "Windows";
                                } else if (lookAndFeelName.startsWith("JGoodies")) {
                                    lookAndFeelName = "Basic";
                                }

                                Class<?> sliderUiType = Class.forName(sliderPackage.getName() + "." + lookAndFeelName
                                        + "SliderUIExt");

                                Constructor<?> constructor1 = null;
                                try {
                                    constructor1 = sliderUiType.getConstructor(JSlider.class);
                                } catch (Exception e3) { // Nothing to do here
                                }
                                Constructor<?> constructor0 = null;
                                try {
                                    constructor0 = sliderUiType.getConstructor();
                                } catch (Exception e3) { // Nothing to do here
                                }

                                Object sliderUi = null;
                                if (constructor1 != null) {
                                    sliderUi = constructor1.newInstance(JSliderExt.this);
                                } else if (constructor0 != null) {
                                    sliderUi = constructor0.newInstance();
                                }

                                if ((sliderUi != null) && (sliderUi instanceof SliderUI)) {
                                    JSliderExt.this.setUI((SliderUI) sliderUi);
                                }
                            } catch (Exception e2) {
                                JSliderExt.this.uix = new BasicSliderUIExt(JSliderExt.this);
                            }
                        }
                    } else {
                        JSliderExt.this.uix = new BasicSliderUIExt(JSliderExt.this);
                    }
                    updateUI();
                }
            }});

        addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent event) {
                String propertyName = event.getPropertyName();

                if (READY_PROPERTY.equals(propertyName)) {
                    Object newValue = event.getNewValue();

                    if ((newValue == null) || (!(newValue instanceof Boolean)) || (((Boolean) newValue).booleanValue())) {
                        setSelectedValue(Integer.valueOf(getValue()));
                    } else {
                        setSelectedValue(null);
                    }
                } else if (SELECTED_VALUE_PROPERTY.equals(propertyName)) {
                    Object newValue = event.getNewValue();
                    System.out.println(newValue);

                    if ((newValue != null) && (newValue instanceof Integer)) {
                        int value = getValue();
                        int newSelectedValue = ((Integer) newValue).intValue();

                        if (value != newSelectedValue) {
                            setValue(newSelectedValue);
                        }

                        setReady(true);
                    } else {
                        setReady(false);
                    }

                    repaint();
                } else if (VALUE_PROPERTY.equals(propertyName)) {
                    setReady(true);

                    Object newValue = event.getNewValue();

                    if ((newValue != null) && (newValue instanceof Integer)) {
                        setSelectedValue((Integer) newValue);
                    }
                }
            }});

        addKeyListener(new KeyAdapter() { // Enables escape key for clearing value
            @Override
            public void keyPressed(KeyEvent event) {
                if (event.getKeyCode() == KeyEvent.VK_ESCAPE) {
                    JSliderExt.this.setReady(false);
                }
            }});

        setReady(ready);
    }

    @Override
    public void setValue(int value) {
        int oldValue = getValue();
        super.setValue(value);
        firePropertyChange(VALUE_PROPERTY, Integer.valueOf(oldValue), Integer.valueOf(value));
    }

    @Override
    public void setExtent(int extent) {
        int oldExtent = getExtent();
        super.setExtent(extent);
        firePropertyChange(EXTENT_PROPERTY, Integer.valueOf(oldExtent), Integer.valueOf(extent));
    }

    @Override
    public void setMinimum(int minimum) {
        int oldMinimum = getMinimum();
        super.setMinimum(minimum);
        firePropertyChange(MINIMUM_PROPERTY, Integer.valueOf(oldMinimum), Integer.valueOf(minimum));
    }

    @Override
    public void setMaximum(int maximum) {
        int oldMaximum = getMaximum();
        super.setMaximum(maximum);
        firePropertyChange(MAXIMUM_PROPERTY, Integer.valueOf(oldMaximum), Integer.valueOf(maximum));
    }

    public Integer getSelectedValue() {
        return (Integer) getClientProperty(SELECTED_VALUE_PROPERTY);
    }

    public void setSelectedValue(Integer selectedValue) {
        putClientProperty(SELECTED_VALUE_PROPERTY, selectedValue);
    }

    public boolean isReady() {
        Object ready = getClientProperty(READY_PROPERTY);
        return ((ready != null) && (ready instanceof Boolean)) ? ((Boolean) ready).booleanValue()
                : READY_DEFAULT_VALUE;
    }

    public void setReady(boolean waiting) {
        putClientProperty(READY_PROPERTY, Boolean.valueOf(waiting));
    }

    @Override
    public void updateUI() {
        setUI(this.uix);
        updateLabelUIs();
    }

}

Please note that using this code, might require some changes on selecting the delegates depending on your application, since this is intended for a Windows system. As said, Synth/Nimbus has to be worked in a different manner, but also any custom L&F or for OSX, needs the proper delegate to be extended and added on the listener.

OTHER TIPS

Make your own implementation of BasicSliderUI and override paintThumb(Graphics g) to do what you require.

Also take a look here: How to hide the knob of jSlider?

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top