296 lines
8.9 KiB
Java
296 lines
8.9 KiB
Java
/*
|
|
|
|
Copyright 2006 Eric Hakenholz
|
|
|
|
This file is part of C.a.R. software.
|
|
|
|
C.a.R. is a free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, version 3 of the License.
|
|
|
|
C.a.R. is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
package eric.controls;
|
|
|
|
import java.awt.EventQueue;
|
|
import java.awt.event.HierarchyEvent;
|
|
import java.awt.event.HierarchyListener;
|
|
import java.awt.event.MouseEvent;
|
|
import java.awt.event.MouseMotionListener;
|
|
import java.beans.PropertyChangeEvent;
|
|
import java.beans.PropertyChangeListener;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
|
|
import javax.swing.JComponent;
|
|
import javax.swing.JSlider;
|
|
import javax.swing.UIDefaults;
|
|
import javax.swing.UIManager;
|
|
import javax.swing.event.MouseInputAdapter;
|
|
import javax.swing.plaf.ComponentUI;
|
|
import javax.swing.plaf.basic.BasicSliderUI;
|
|
|
|
public class SliderSnap extends BasicSliderUI {
|
|
/**
|
|
* The UI class implements the current slider Look and Feel.
|
|
*/
|
|
private static Class sliderClass;
|
|
private static Method xForVal, yForVal;
|
|
private static ReinitListener reinitListener = new ReinitListener();
|
|
|
|
public SliderSnap() {
|
|
super(null);
|
|
}
|
|
|
|
/**
|
|
* Returns the UI as normal, but intercepts the call, so a listener can be
|
|
* attached.
|
|
*/
|
|
public static ComponentUI createUI(final JComponent c) {
|
|
if (c == null || sliderClass == null)
|
|
return null;
|
|
final UIDefaults defaults = UIManager.getLookAndFeelDefaults();
|
|
try {
|
|
Method m = (Method) defaults.get(sliderClass);
|
|
if (m == null) {
|
|
m = sliderClass.getMethod("createUI",
|
|
new Class[] { JComponent.class });
|
|
defaults.put(sliderClass, m);
|
|
}
|
|
final ComponentUI uiObject = (ComponentUI) m.invoke(null,
|
|
new Object[] { c });
|
|
if (uiObject instanceof BasicSliderUI)
|
|
c.addHierarchyListener(new MouseAttacher());
|
|
return uiObject;
|
|
} catch (final Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
public static void init() {
|
|
// check we don't initialise twice
|
|
if (sliderClass != null)
|
|
return;
|
|
final Init init = new Init();
|
|
if (EventQueue.isDispatchThread()) {
|
|
init.run();
|
|
} else {
|
|
// This code must run on the EDT for data visibility
|
|
try {
|
|
EventQueue.invokeAndWait(init);
|
|
} catch (final Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listeners for when the JSlider becomes visible then attaches the mouse
|
|
* listeners, then removes itself.
|
|
*/
|
|
private static class MouseAttacher implements HierarchyListener {
|
|
public void hierarchyChanged(final HierarchyEvent evt) {
|
|
final long flags = evt.getChangeFlags();
|
|
if ((flags & HierarchyEvent.DISPLAYABILITY_CHANGED) > 0
|
|
&& evt.getComponent() instanceof JSlider) {
|
|
final JSlider c = (JSlider) evt.getComponent();
|
|
c.removeHierarchyListener(this);
|
|
attachTo(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listens for Look and Feel changes and re-initialises the class.
|
|
*/
|
|
private static class ReinitListener implements PropertyChangeListener {
|
|
public void propertyChange(final PropertyChangeEvent evt) {
|
|
if ("lookAndFeel".equals(evt.getPropertyName())) {
|
|
// The look and feel was changed so we need to re-insert
|
|
// Our hook into the new UIDefaults map
|
|
sliderClass = null;
|
|
xForVal = yForVal = null;
|
|
UIManager.removePropertyChangeListener(reinitListener);
|
|
init();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialises the reflective methods and adjusts the current Look and Feel.
|
|
*/
|
|
private static class Init implements Runnable {
|
|
public void run() {
|
|
try {
|
|
final UIDefaults defaults = UIManager.getLookAndFeelDefaults();
|
|
sliderClass = defaults.getUIClass("SliderUI");
|
|
// Set up two reflective method calls
|
|
xForVal = BasicSliderUI.class.getDeclaredMethod(
|
|
"xPositionForValue", new Class[] { int.class });
|
|
yForVal = BasicSliderUI.class.getDeclaredMethod(
|
|
"yPositionForValue", new Class[] { int.class });
|
|
// Allow us access to the methods
|
|
xForVal.setAccessible(true);
|
|
yForVal.setAccessible(true);
|
|
// Replace UI class with ourselves
|
|
defaults.put("SliderUI", SliderSnap.class.getName());
|
|
UIManager.addPropertyChangeListener(reinitListener);
|
|
} catch (final Exception e) {
|
|
sliderClass = null;
|
|
xForVal = yForVal = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called to attach mouse listeners to the JSlider.
|
|
*/
|
|
private static void attachTo(final JSlider c) {
|
|
final MouseMotionListener[] listeners = c.getMouseMotionListeners();
|
|
for (final MouseMotionListener m : listeners) {
|
|
if (m instanceof TrackListener) {
|
|
c.removeMouseMotionListener(m); // remove original
|
|
final SnapListener listen = new SnapListener(m,
|
|
(BasicSliderUI) c.getUI(), c);
|
|
c.addMouseMotionListener(listen);
|
|
c.addMouseListener(listen);
|
|
c.addPropertyChangeListener("UI", listen);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class SnapListener extends MouseInputAdapter implements
|
|
PropertyChangeListener {
|
|
private final MouseMotionListener delegate;
|
|
/**
|
|
* Original Look and Feel implementation
|
|
*/
|
|
private final BasicSliderUI ui;
|
|
/**
|
|
* Our slider
|
|
*/
|
|
private final JSlider slider;
|
|
/**
|
|
* Offset of mouse click from centre of slider thumb
|
|
*/
|
|
private int offset;
|
|
|
|
public SnapListener(final MouseMotionListener delegate,
|
|
final BasicSliderUI ui, final JSlider slider) {
|
|
this.delegate = delegate;
|
|
this.ui = ui;
|
|
this.slider = slider;
|
|
}
|
|
|
|
/**
|
|
* UI can change at any point, so we need to listen for these events.
|
|
*/
|
|
public void propertyChange(final PropertyChangeEvent evt) {
|
|
if ("UI".equals(evt.getPropertyName())) {
|
|
// Remove old listeners and create new ones
|
|
slider.removeMouseMotionListener(this);
|
|
slider.removeMouseListener(this);
|
|
slider.removePropertyChangeListener("UI", this);
|
|
attachTo(slider);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the actual "snap while dragging" behaviour. If snap to
|
|
* ticks is enabled on this slider, then the location for the nearest
|
|
* tick/label is calculated and the click location is translated before
|
|
* being passed to the delegate.
|
|
*/
|
|
@Override
|
|
public void mouseDragged(final MouseEvent evt) {
|
|
if (slider.getSnapToTicks()) { // if we are set to snap
|
|
final int pos = getLocationForValue(getSnappedValue(evt));
|
|
// if above call fails and returns -1, take no action
|
|
if (pos > -1) {
|
|
if (slider.getOrientation() == JSlider.HORIZONTAL)
|
|
evt.translatePoint(pos - evt.getX() + offset, 0);
|
|
else
|
|
evt.translatePoint(0, pos - evt.getY() + offset);
|
|
}
|
|
}
|
|
delegate.mouseDragged(evt);
|
|
}
|
|
|
|
/**
|
|
* When the slider is clicked we need to record the offset from thumb
|
|
* center.
|
|
*/
|
|
@Override
|
|
public void mousePressed(final MouseEvent evt) {
|
|
|
|
final int pos = (slider.getOrientation() == JSlider.HORIZONTAL) ? evt
|
|
.getX()
|
|
: evt.getY();
|
|
final int loc = getLocationForValue(getSnappedValue(evt));
|
|
this.offset = (loc < 0) ? 0 : pos - loc;
|
|
}
|
|
|
|
/* Pass straight to delegate. */
|
|
@Override
|
|
public void mouseMoved(final MouseEvent evt) {
|
|
delegate.mouseMoved(evt);
|
|
}
|
|
|
|
/**
|
|
* Calculates the nearest snapable value given a MouseEvent. Code
|
|
* adapted from BasicSliderUI.
|
|
*/
|
|
public int getSnappedValue(final MouseEvent evt) {
|
|
final int value = slider.getOrientation() == JSlider.HORIZONTAL ? ui
|
|
.valueForXPosition(evt.getX())
|
|
: ui.valueForYPosition(evt.getY());
|
|
// Now calculate if we should adjust the value
|
|
int snappedValue = value;
|
|
int tickSpacing = 0;
|
|
final int majorTickSpacing = slider.getMajorTickSpacing();
|
|
final int minorTickSpacing = slider.getMinorTickSpacing();
|
|
if (minorTickSpacing > 0)
|
|
tickSpacing = minorTickSpacing;
|
|
else if (majorTickSpacing > 0)
|
|
tickSpacing = majorTickSpacing;
|
|
// If it's not on a tick, change the value
|
|
if (tickSpacing != 0) {
|
|
if ((value - slider.getMinimum()) % tickSpacing != 0) {
|
|
final float temp = (float) (value - slider.getMinimum())
|
|
/ (float) tickSpacing;
|
|
snappedValue = slider.getMinimum()
|
|
+ (Math.round(temp) * tickSpacing);
|
|
}
|
|
}
|
|
return snappedValue;
|
|
}
|
|
|
|
/**
|
|
* Provides the x or y co-ordinate for a slider value, depending on
|
|
* orientation.
|
|
*/
|
|
public int getLocationForValue(final int value) {
|
|
try {
|
|
// Reflectively call slider ui code
|
|
final Method m = slider.getOrientation() == JSlider.HORIZONTAL ? xForVal
|
|
: yForVal;
|
|
final Integer result = (Integer) m.invoke(ui,
|
|
new Object[] { new Integer(value) });
|
|
return result.intValue();
|
|
} catch (final InvocationTargetException e) {
|
|
return -1;
|
|
} catch (final IllegalAccessException e) {
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
} |