/* Copyright 2008-2010 Gephi Authors : Mathieu Bastian <mathieu.bastian@gephi.org> Website : http://www.gephi.org This file is part of Gephi. DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. Copyright 2011 Gephi Consortium. All rights reserved. The contents of this file are subject to the terms of either the GNU General Public License Version 3 only ("GPL") or the Common Development and Distribution License("CDDL") (collectively, the "License"). You may not use this file except in compliance with the License. You can obtain a copy of the License at http://gephi.org/about/legal/license-notice/ or /cddl-1.0.txt and /gpl-3.0.txt. See the License for the specific language governing permissions and limitations under the License. When distributing the software, include this License Header Notice in each file and include the License files at /cddl-1.0.txt and /gpl-3.0.txt. If applicable, add the following below the License Header, with the fields enclosed by brackets [] replaced by your own identifying information: "Portions Copyrighted [year] [name of copyright owner]" If you wish your version of this file to be governed by only the CDDL or only the GPL Version 3, indicate your decision by adding "[Contributor] elects to include this software in this distribution under the [CDDL or GPL Version 3] license." If you do not indicate a single choice of license, a recipient has the option to distribute your version of this file under either the CDDL, the GPL Version 3 or to extend the choice of license to its licensees as provided above. However, if you add GPL Version 3 code and therefore, elected the GPL Version 3 license, then the option applies only if the new code is made subject to such option by the copyright holder. Contributor(s): Portions Copyrighted 2011 Gephi Consortium. */ 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; } boolean mousDownFox = mouseIsDown; //Mousedown fix mouseIsDown = false; if (mousDownFox && 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); } }