/*
Copyright 2008-2010 Gephi
Authors : Mathieu Bastian <mathieu.bastian@gephi.org>
Website : http://www.gephi.org
This file is part of Gephi.
Gephi is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
Gephi 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Gephi. If not, see <http://www.gnu.org/licenses/>.
*/
package org.gephi.ui.components.gradientslider;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JComponent;
import javax.swing.JSlider;
import javax.swing.UIManager;
import javax.swing.plaf.ComponentUI;
/** This is the abstract UI for <code>MultiThumbSliders</code>
*
*/
//Author Jeremy Wood
public abstract class MultiThumbSliderUI extends ComponentUI implements MouseListener, MouseMotionListener {
protected MultiThumbSlider slider;
/** The maximum width returned by <code>getMaximumSize()</code>.
* (or if the slider is vertical, this is the maximum height.)
*/
int MAX_LENGTH = 300;
/** The minimum width returned by <code>getMinimumSize()</code>.
* (or if the slider is vertical, this is the minimum height.)
*/
int MIN_LENGTH = 50;
/** The maximum width returned by <code>getPreferredSize()</code>.
* (or if the slider is vertical, this is the preferred height.)
*/
int PREF_LENGTH = 140;
/** The height of a horizontal slider -- or width of a vertical slider.
*/
int DEPTH = 15;
/** The pixel position of the thumbs. This may be x or y coordinates, depending on
* whether this slider is horizontal or vertical
*/
int[] thumbPositions = new int[0];
/** A float from zero to one, indicating whether that thumb should be highlighted
* or not.
*/
protected float[] thumbIndications = new float[0];
/** This is used by the animating thread. The field indication is updated until it equals this value. */
private float indicationGoal = 0;
/** The overall indication of the thumbs. At one they should be opaque,
* at zero they should be transparent.
*/
float indication = 0;
/** The rectangle the track should be painted in. */
protected Rectangle trackRect = new Rectangle(0, 0, 0, 0);
public MultiThumbSliderUI(MultiThumbSlider slider) {
this.slider = slider;
}
public Dimension getMaximumSize(JComponent s) {
MultiThumbSlider mySlider = (MultiThumbSlider) s;
if (mySlider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
return new Dimension(MAX_LENGTH, DEPTH);
}
return new Dimension(DEPTH, MAX_LENGTH);
}
public Dimension getMinimumSize(JComponent s) {
MultiThumbSlider mySlider = (MultiThumbSlider) s;
if (mySlider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
return new Dimension(MIN_LENGTH, DEPTH);
}
return new Dimension(DEPTH, MIN_LENGTH);
}
public Dimension getPreferredSize(JComponent s) {
MultiThumbSlider mySlider = (MultiThumbSlider) s;
if (mySlider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
return new Dimension(PREF_LENGTH, DEPTH);
}
return new Dimension(DEPTH, PREF_LENGTH);
}
/** This records the positions/values of each thumb.
* This is used when the mouse is pressed, so as the mouse
* is dragged values can get replaced and rearranged freely.
* (Including removing and adding thumbs)
*
*/
class State {
Object[] values;
float[] positions;
int selectedThumb;
public State() {
values = slider.getValues();
positions = slider.getThumbPositions();
selectedThumb = slider.getSelectedThumb(false);
}
public State(State s) {
selectedThumb = s.selectedThumb;
positions = new float[s.positions.length];
values = new Object[s.values.length];
System.arraycopy(s.positions, 0, positions, 0, positions.length);
System.arraycopy(s.values, 0, values, 0, values.length);
}
/** Strip values outside of [0,1] */
private void polish() {
while (positions[0] < 0) {
float[] f2 = new float[positions.length - 1];
System.arraycopy(positions, 1, f2, 0, positions.length - 1);
Object[] c2 = new Object[values.length - 1];
System.arraycopy(values, 1, c2, 0, positions.length - 1);
positions = f2;
values = c2;
selectedThumb++;
}
while (positions[positions.length - 1] > 1) {
float[] f2 = new float[positions.length - 1];
System.arraycopy(positions, 0, f2, 0, positions.length - 1);
Object[] c2 = new Object[values.length - 1];
System.arraycopy(values, 0, c2, 0, positions.length - 1);
positions = f2;
values = c2;
selectedThumb--;
}
if (selectedThumb >= positions.length) {
selectedThumb = -1;
}
}
/** Make the slider reflect this object */
public void install() {
polish();
slider.setValues(positions, values);
slider.setSelectedThumb(selectedThumb);
}
public void removeThumb(int index) {
float[] f = new float[positions.length - 1];
Object[] c = new Object[values.length - 1];
System.arraycopy(positions, 0, f, 0, index);
System.arraycopy(values, 0, c, 0, index);
System.arraycopy(positions, index + 1, f, index, f.length - index);
System.arraycopy(values, index + 1, c, index, f.length - index);
positions = f;
values = c;
selectedThumb = -1;
}
}
Thread animatingThread = null;
Runnable animatingRunnable = new Runnable() {
public void run() {
boolean finished = false;
while (!finished) {
synchronized (MultiThumbSliderUI.this) {
finished = true;
for (int a = 0; a < thumbIndications.length; a++) {
if (a != slider.getSelectedThumb()) {
if (a == currentIndicatedThumb) {
if (thumbIndications[a] < 1) {
thumbIndications[a] = Math.min(1, thumbIndications[a] + .025f);
finished = false;
}
} else {
if (thumbIndications[a] > 0) {
thumbIndications[a] = Math.max(0, thumbIndications[a] - .025f);
finished = false;
}
}
} else {
//the selected thumb is painted as selected,
//so there's no indication to animate.
//just set the indication to whatever it should
//be and move on. No repainting.
if (a == currentIndicatedThumb) {
thumbIndications[a] = 1;
} else {
thumbIndications[a] = 0;
}
}
}
if (indicationGoal > indication + .01f) {
if (indication < .99f) {
indication = Math.min(1, indication + .1f);
finished = false;
}
} else if (indicationGoal < indication - .01f) {
if (indication > .01f) {
indication = Math.max(0, indication - .1f);
finished = false;
}
}
}
if (!finished) {
slider.repaint();
}
//rest a little bit
long t = System.currentTimeMillis();
while (System.currentTimeMillis() - t < 20) {
try {
Thread.sleep(10);
} catch (Exception e) {
Thread.yield();
}
}
}
}
};
private int currentIndicatedThumb = -1;
private boolean mouseInside = false;
private boolean mouseIsDown = false;
private State pressedState;
private int dx, dy;
public void mousePressed(MouseEvent e) {
dx = 0;
dy = 0;
if (slider.isEnabled() == false) {
return;
}
if (e.getClickCount() >= 2) {
if (slider.doDoubleClick(e.getX(), e.getY())) {
e.consume();
return;
}
} else if (e.isPopupTrigger()) {
int x = e.getX();
int y = e.getY();
if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
if (x < trackRect.x || x > trackRect.x + trackRect.width) {
return;
}
y = trackRect.y + trackRect.height;
} else {
if (y < trackRect.y || y > trackRect.y + trackRect.height) {
return;
}
x = trackRect.x + trackRect.width;
}
if (slider.doPopup(x, y)) {
e.consume();
return;
}
}
mouseIsDown = true;
mouseMoved(e);
if (e.getSource() != slider) {
throw new RuntimeException("only install this UI on the GradientSlider it was constructed with");
}
slider.requestFocus();
int index = getIndex(e);
if (index != -1) {
if (slider.getOrientation() == JSlider.HORIZONTAL) {
dx = -e.getX() + thumbPositions[index];
} else {
dy = -e.getY() + thumbPositions[index];
}
}
if (index != -1) {
slider.setSelectedThumb(index);
e.consume();
} else {
if (slider.isAutoAdding()) {
float k;
int v;
if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
v = e.getX();
} else {
v = e.getY();
}
if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
k = ((float) (v - trackRect.x)) / ((float) trackRect.width);
if (slider.isInverted()) {
k = 1 - k;
}
} else {
k = ((float) (v - trackRect.y)) / ((float) trackRect.height);
if (slider.isInverted() == false) {
k = 1 - k;
}
}
if (k > 0 && k < 1 && !slider.isBlocked()) {
int added = slider.addThumb(k);
slider.setSelectedThumb(added);
}
e.consume();
} else {
if (slider.getSelectedThumb() != -1) {
slider.setSelectedThumb(-1);
e.consume();
}
}
}
pressedState = new State();
}
private int getIndex(MouseEvent e) {
int v;
if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
v = e.getX();
if (v < trackRect.x - getClickLocationTolerance() + 1 || v > trackRect.x + trackRect.width + getClickLocationTolerance() - 1) {
return -1; // didn't click in the track;
}
} else {
v = e.getY();
if (v < trackRect.y - getClickLocationTolerance() + 1 || v > trackRect.y + trackRect.height + getClickLocationTolerance() - 1) {
return -1;
}
}
if(thumbPositions.length==0) {
return -1;
}
int min = Math.abs(v - thumbPositions[0]);
int minIndex = 0;
for (int a = 1; a < thumbPositions.length; a++) {
int distance = Math.abs(v - thumbPositions[a]);
if (distance < min) {
min = distance;
minIndex = a;
}
}
if (min < getClickLocationTolerance()) {
return minIndex;
}
return -1;
}
public void mouseEntered(MouseEvent e) {
mouseMoved(e);
}
public void mouseExited(MouseEvent e) {
setCurrentIndicatedThumb(-1);
setMouseInside(false);
}
public void mouseClicked(MouseEvent e) {
}
public void mouseMoved(MouseEvent e) {
if (slider.isEnabled() == false) {
return;
}
int i = getIndex(e);
setCurrentIndicatedThumb(i);
boolean b = (e.getX() >= 0 && e.getX() < slider.getWidth() && e.getY() >= 0 && e.getY() < slider.getHeight());
if (mouseIsDown) {
b = true;
}
setMouseInside(b);
}
private void setCurrentIndicatedThumb(int i) {
if (getProperty(slider, "MultiThumbSlider.indicateThumb", "true").equals("false")) {
//never activate a specific thumb
i = -1;
}
currentIndicatedThumb = i;
boolean finished = true;
for (int a = 0; a < thumbIndications.length; a++) {
if (a == currentIndicatedThumb) {
if (thumbIndications[a] != 1) {
finished = false;
}
} else {
if (thumbIndications[a] != 0) {
finished = false;
}
}
}
if (!finished) {
synchronized (MultiThumbSliderUI.this) {
if (animatingThread == null || animatingThread.isAlive() == false) {
animatingThread = new Thread(animatingRunnable);
animatingThread.start();
}
}
}
}
private void setMouseInside(boolean b) {
mouseInside = b;
updateIndication();
}
public void mouseDragged(MouseEvent e) {
if (slider.isEnabled() == false) {
return;
}
e.translatePoint(dx, dy);
mouseMoved(e);
if (pressedState != null && pressedState.selectedThumb != -1) {
slider.setValueIsAdjusting(true);
State newState = new State(pressedState);
float v;
boolean outside;
if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
v = ((float) (e.getX() - trackRect.x)) / ((float) trackRect.width);
if (slider.isInverted()) {
v = 1 - v;
}
outside = (e.getY() < trackRect.y - 10) || (e.getY() > trackRect.y + trackRect.height + 10);
//don't whack the thumb off the slider if you happen to be *near* the edge:
if (e.getX() > trackRect.x - 10 && e.getX() < trackRect.x + trackRect.width + 10) {
if (v < 0) {
v = 0;
}
if (v > 1) {
v = 1;
}
}
} else {
v = ((float) (e.getY() - trackRect.y)) / ((float) trackRect.height);
if (slider.isInverted() == false) {
v = 1 - v;
}
outside = (e.getX() < trackRect.x - 10) || (e.getX() > trackRect.x + trackRect.width + 10);
if (e.getY() > trackRect.y - 10 && e.getY() < trackRect.y + trackRect.height + 10) {
if (v < 0) {
v = 0;
}
if (v > 1) {
v = 1;
}
}
}
if (newState.positions.length <= 2) {
outside = false; //I don't care if you are outside: no removing!
}
newState.positions[newState.selectedThumb] = v;
//because we delegate mouseReleased() to this method:
if (outside) {
newState.removeThumb(newState.selectedThumb);
}
if (validatePositions(newState)) {
newState.install();
}
e.consume();
}
}
public void mouseReleased(MouseEvent e) {
if (slider.isEnabled() == false) {
return;
}
mouseIsDown = false;
if (pressedState != null && slider.getThumbCount() <= pressedState.positions.length) {
mouseDragged(e); //go ahead and commit this final location
}
if (slider.isValueAdjusting()) {
slider.setValueIsAdjusting(false);
}
if (e.isPopupTrigger() && slider.doPopup(e.getX(), e.getY())) {
//on windows popuptriggers happen on mouseRelease
e.consume();
return;
}
}
/** This retrieves a property.
* If the component has this property manually set (by calling
* <code>component.putClientProperty()</code), then that value will be returned.
* Otherwise this method refers to <code>UIManager.get()</code>. If that
* value is missing, this returns <code>defaultValue</code>
*
* @param jc
* @param propertyName the property name
* @param defaultValue if no other value is found, this is returned
* @return the property value
*/
public static String getProperty(JComponent jc, String propertyName, String defaultValue) {
Object jcValue = jc.getClientProperty(propertyName);
if (jcValue != null) {
return jcValue.toString();
}
Object uiValue = UIManager.get(propertyName);
if (uiValue != null) {
return uiValue.toString();
}
return defaultValue;
}
/** How many pixels can you deviate from a thumb and and still "click" it.*/
public abstract int getClickLocationTolerance();
/** Makes sure the thumbs are in the right order.
*
* @param state
* @return true if the thumbs are valid. False if there are two
* thumbs with the same value (this is not allowed)
*/
protected static boolean validatePositions(State state) {
float[] p = state.positions;
Object[] c = state.values;
/** Don't let the user position a thumb outside of
* [0,1] if there are only 2 colors:
* colors outside [0,1] are deleted, and we can't delete
* colors so we get less than 2.
*/
if (p.length <= 2) {
/** Since the user can only manipulate 1 thumb at a time,
* only 1 thumb should be outside the domain of [0,1].
* So we *don't* have to reorganize c when we change p
*/
for (int a = 0; a < p.length; a++) {
if (p[a] < 0) {
p[a] = 0;
} else if (p[a] > 1) {
p[a] = 1;
}
}
}
//validate the new positions:
boolean checkAgain = true;
while (checkAgain) {
checkAgain = false;
for (int a = 0; a < p.length - 1; a++) {
if (p[a] == p[a + 1]) {
return false; //we can't make two equal
}
if (p[a] > p[a + 1]) {
checkAgain = true;
float swap1 = p[a];
p[a] = p[a + 1];
p[a + 1] = swap1;
Object swap2 = c[a];
c[a] = c[a + 1];
c[a + 1] = swap2;
if (a == state.selectedThumb) {
state.selectedThumb = a + 1;
} else if (a + 1 == state.selectedThumb) {
state.selectedThumb = a;
}
}
}
}
return true;
}
FocusListener focusListener = new FocusListener() {
public void focusLost(FocusEvent e) {
Component c = (Component) e.getSource();
if (getProperty(slider, "MultiThumbSlider.indicateComponent", "true").toString().equals("true")) {
slider.setSelectedThumb(-1);
}
updateIndication();
c.repaint();
}
public void focusGained(FocusEvent e) {
Component c = (Component) e.getSource();
int i = slider.getSelectedThumb(false);
if (i == -1) {
int direction = 1;
if (slider.getOrientation() == MultiThumbSlider.VERTICAL) {
direction *= -1;
}
if (slider.isInverted()) {
direction *= -1;
}
slider.setSelectedThumb((direction == 1) ? 0 : slider.getThumbCount() - 1);
}
updateIndication();
c.repaint();
}
};
/** This will try to add a thumb between index1 and index2.
* <P>This method will not add a thumb if there is already a very
* small distance between these two endpoints
*
* @param index1
* @param index2
* @return true if a new thumb was added
*/
protected boolean addThumb(int index1, int index2) {
float pos1 = 0;
float pos2 = 1;
int min;
int max;
if (index1 < index2) {
min = index1;
max = index2;
} else {
min = index2;
max = index1;
}
float[] positions = slider.getThumbPositions();
if (min >= 0) {
pos1 = positions[min];
}
if (max < positions.length) {
pos2 = positions[max];
}
if (pos2 - pos1 < .05) {
return false;
}
float newPosition = (pos1 + pos2) / 2f;
slider.setSelectedThumb(slider.addThumb(newPosition));
return true;
}
KeyListener keyListener = new KeyListener() {
public void keyPressed(KeyEvent e) {
if (slider.isEnabled() == false) {
return;
}
if (e.getSource() != slider) {
throw new RuntimeException("only install this UI on the GradientSlider it was constructed with");
}
int i = slider.getSelectedThumb();
int code = e.getKeyCode();
int orientation = slider.getOrientation();
if (i != -1 &&
(code == KeyEvent.VK_RIGHT || code == KeyEvent.VK_LEFT) &&
orientation == MultiThumbSlider.HORIZONTAL &&
e.getModifiers() == Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()) {
//insert a new thumb
int i2;
if ((code == KeyEvent.VK_RIGHT && slider.isInverted() == false) ||
(code == KeyEvent.VK_LEFT && slider.isInverted() == true)) {
i2 = i + 1;
} else {
i2 = i - 1;
}
addThumb(i, i2);
e.consume();
return;
} else if (i != -1 &&
(code == KeyEvent.VK_UP || code == KeyEvent.VK_DOWN) &&
orientation == MultiThumbSlider.VERTICAL &&
e.getModifiers() == Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()) {
//insert a new thumb
int i2;
if ((code == KeyEvent.VK_UP && slider.isInverted() == false) ||
(code == KeyEvent.VK_DOWN && slider.isInverted() == true)) {
i2 = i + 1;
} else {
i2 = i - 1;
}
addThumb(i, i2);
e.consume();
return;
} else if (code == KeyEvent.VK_DOWN &&
orientation == MultiThumbSlider.HORIZONTAL &&
i != -1) {
//popup up!
int x = slider.isInverted() ? (int) (trackRect.x + trackRect.width * (1 - slider.getThumbPositions()[i])) : (int) (trackRect.x + trackRect.width * slider.getThumbPositions()[i]);
int y = trackRect.y + trackRect.height;
if (slider.doPopup(x, y)) {
e.consume();
return;
}
} else if (code == KeyEvent.VK_RIGHT &&
orientation == MultiThumbSlider.VERTICAL &&
i != -1) {
//popup up!
int y = slider.isInverted() ? (int) (trackRect.y + trackRect.height * slider.getThumbPositions()[i]) : (int) (trackRect.y + trackRect.height * (1 - slider.getThumbPositions()[i]));
int x = trackRect.x + trackRect.width;
if (slider.doPopup(x, y)) {
e.consume();
return;
}
}
if (i != -1) {
//move the selected thumb
if (code == KeyEvent.VK_RIGHT || code == KeyEvent.VK_DOWN) {
nudge(i, 1);
e.consume();
} else if (code == KeyEvent.VK_LEFT || code == KeyEvent.VK_UP) {
nudge(i, -1);
e.consume();
} else if (code == KeyEvent.VK_DELETE || code == KeyEvent.VK_BACK_SPACE) {
if (slider.getThumbCount() > 2) {
slider.removeThumb(i);
e.consume();
}
} else if (code == KeyEvent.VK_SPACE || code == KeyEvent.VK_ENTER) {
slider.doDoubleClick(-1, -1);
}
}
}
public void keyReleased(KeyEvent e) {
}
public void keyTyped(KeyEvent e) {
}
};
PropertyChangeListener propertyListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent e) {
String name = e.getPropertyName();
if (name.equals(MultiThumbSlider.VALUES_PROPERTY) ||
name.equals(MultiThumbSlider.ORIENTATION_PROPERTY) ||
name.equals(MultiThumbSlider.INVERTED_PROPERTY)) {
calculateGeometry();
slider.repaint();
} else if (name.equals(MultiThumbSlider.SELECTED_THUMB_PROPERTY) ||
name.equals(MultiThumbSlider.PAINT_TICKS_PROPERTY)) {
slider.repaint();
} else if (name.equals("MultiThumbSlider.indicateComponent")) {
setMouseInside(mouseInside);
slider.repaint();
}
}
};
ComponentListener compListener = new ComponentListener() {
public void componentHidden(ComponentEvent e) {
}
public void componentMoved(ComponentEvent e) {
}
public void componentResized(ComponentEvent e) {
calculateGeometry();
Component c = (Component) e.getSource();
c.repaint();
}
public void componentShown(ComponentEvent e) {
}
};
protected void updateIndication() {
synchronized (MultiThumbSliderUI.this) {
if (slider.isEnabled() && (slider.hasFocus() || mouseInside)) {
indicationGoal = 1;
} else {
indicationGoal = 0;
}
if (getProperty(slider, "MultiThumbSlider.indicateComponent", "true").equals("false")) {
//always turn on the "indication", so controls are always visible
indicationGoal = 1;
if (slider.isVisible() == false) { //when the component isn't yet initialized
indication = 1; //initialize it to fully indicated
}
}
if (indication != indicationGoal) {
if (animatingThread == null || animatingThread.isAlive() == false) {
animatingThread = new Thread(animatingRunnable);
animatingThread.start();
}
}
}
}
protected synchronized void calculateGeometry() {
trackRect = calculateTrackRect();
float[] pos = slider.getThumbPositions();
if (thumbPositions.length != pos.length) {
thumbPositions = new int[pos.length];
thumbIndications = new float[pos.length];
}
if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
for (int a = 0; a < thumbPositions.length; a++) {
if (slider.isInverted() == false) {
thumbPositions[a] = trackRect.x + (int) (trackRect.width * pos[a]);
} else {
thumbPositions[a] = trackRect.x + (int) (trackRect.width * (1 - pos[a]));
}
thumbIndications[a] = 0;
}
} else {
for (int a = 0; a < thumbPositions.length; a++) {
if (slider.isInverted()) {
thumbPositions[a] = trackRect.y + (int) (trackRect.height * pos[a]);
} else {
thumbPositions[a] = trackRect.y + (int) (trackRect.height * (1 - pos[a]));
}
thumbIndications[a] = 0;
}
}
}
protected Rectangle calculateTrackRect() {
Insets i = new Insets(5, 5, 5, 5);
int w, h;
if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
w = slider.getWidth() - i.left - i.right;
h = Math.min(DEPTH, slider.getHeight() - i.top - i.bottom);
} else {
h = slider.getHeight() - i.top - i.bottom;
w = Math.min(DEPTH, slider.getWidth() - i.left - i.right);
}
return new Rectangle(slider.getWidth() / 2 - w / 2, slider.getHeight() / 2 - h / 2, w, h);
}
private void nudge(int thumbIndex, int direction) {
float pixelFraction;
if (slider.getOrientation() == GradientSlider.HORIZONTAL) {
pixelFraction = 1f / ((float) trackRect.width);
} else {
pixelFraction = 1f / ((float) trackRect.height);
}
if (direction < 0) {
pixelFraction *= -1;
}
if (slider.isInverted()) {
pixelFraction *= -1;
}
if (slider.getOrientation() == MultiThumbSlider.VERTICAL) {
pixelFraction *= -1;
}
//repeat a couple of times: it's possible we'll nudge two values
//so they're exactly equal, which will make validate() fail.
//in that case: move the value ANOTHER nudge to the left/right
//to really make a change. But make sure we still respect the [0,1] limits.
State state = new State();
while (state.positions[thumbIndex] >= 0 && state.positions[thumbIndex] <= 1) {
state.positions[thumbIndex] += pixelFraction;
if (validatePositions(state)) {
state.install();
return;
}
}
}
public void installUI(JComponent slider) {
slider.addMouseListener(this);
slider.addMouseMotionListener(this);
slider.addFocusListener(focusListener);
slider.addKeyListener(keyListener);
slider.addComponentListener(compListener);
slider.addPropertyChangeListener(propertyListener);
}
public void paint(Graphics g, JComponent slider2) {
if (slider2 != slider) {
throw new RuntimeException("only use this UI on the GradientSlider it was constructed with");
}
Graphics2D g2 = (Graphics2D) g;
int w = slider.getWidth();
int h = slider.getHeight();
if (slider.isOpaque()) {
g.setColor(slider.getBackground());
g.fillRect(0, 0, w, h);
}
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
paintTrack(g2);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
paintFocus(g2);
paintThumbs(g2);
}
protected abstract void paintTrack(Graphics2D g);
protected abstract void paintFocus(Graphics2D g);
protected abstract void paintThumbs(Graphics2D g);
public void uninstallUI(JComponent slider) {
slider.removeMouseListener(this);
slider.removeMouseMotionListener(this);
slider.removeFocusListener(focusListener);
slider.removeKeyListener(keyListener);
slider.removeComponentListener(compListener);
slider.removePropertyChangeListener(propertyListener);
super.uninstallUI(slider);
}
}