/* Copyright (c) 2006-2007 Timothy Wall, All Rights Reserved
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* <p/>
* This library 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
* Lesser General Public License for more details.
*/
package furbelow;
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeMap;
import javax.swing.AbstractListModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.border.EmptyBorder;
/** Animates moving list cells out of the way for a potential drop.
* This decorator completely over-paints the target JList, optionally
* painting a dragged item and animating creation of a space for the
* dragged item to be dropped.
* Thanks to Neil Cochran/keilly for a base visualization:
* http://jroller.com/page/swinguistuff?entry=animated_jlist
*/
public abstract class ListAnimator extends AbstractComponentDecorator {
/** Animation repaint interval. Make this larger to slow down the
* animation.
*/
private static final int INTERVAL = 1000 / 24;
private static Timer timer = new Timer(true);
/** Simple decorator to provide the ghosted drag image. */
private final class GhostedDragImage extends AbstractComponentDecorator {
private int index;
private Point location;
private Point offset;
public GhostedDragImage(int cellIndex, Point origin) {
super(list);
this.index = cellIndex;
Rectangle b = list.getCellBounds(index, index);
location = origin;
this.offset = new Point(0, origin.y - b.y);
}
public void setLocation(Point where) {
this.location = where;
getPainter().repaint();
}
public void paint(Graphics g) {
Rectangle b = list.getCellBounds(index, index);
Point origin = new Point(0, location.y-offset.y);
origin.y = Math.max(0, origin.y);
origin.y = Math.min(origin.y, list.getHeight() - b.height);
g = g.create(origin.x, origin.y, b.width, b.height);
((Graphics2D)g).translate(-b.x, -b.y);
((Graphics2D)g).setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
list.paint(g);
}
}
private final class Counter extends TimerTask {
public void run() {
synchronized(ListAnimator.this) {
if (reposition()) {
repaint();
}
}
}
}
private Counter counter;
/** Index of insertion point. */
private int insertionIndex = -1;
/** Index of object being dragged, if any. */
private int draggedIndex = -1;
private JList list;
private Map bounds = new TreeMap();
private GhostedDragImage dragImage;
private Point origin;
public ListAnimator(final JList list) {
super(list);
this.list = list;
counter = new Counter();
timer.schedule(counter, INTERVAL, INTERVAL);
}
protected Object getPlaceholder() { return ""; }
protected abstract void move(int fromIndex, int toIndex);
protected void drop(Transferable t, int index) { }
private void initialize(Point where) {
insertionIndex = draggedIndex = -1;
origin = where;
int size = list.getModel().getSize();
for (int i=0;i < size;i++) {
bounds.put(new Integer(i), getCellBoundsAfterInsertion(i));
}
}
/** Track a drag which originated somewhere else. */
public synchronized void startDragOver(Point where) {
initialize(where);
insertionIndex = getIndex(where, false);
}
/** Stop tracking an external drag. */
public synchronized void endDragOver(Point where, Transferable t) {
int idx = getIndex(where, false);
if (idx != -1) {
drop(t, idx);
}
}
/** Start an internal drag. */
public synchronized void startDrag(Point where) {
initialize(where);
draggedIndex = insertionIndex = getIndex(where, true);
dragImage = new GhostedDragImage(draggedIndex, origin);
}
/** End an internal drag. */
public synchronized void endDrag(Point where) {
int toIndex = getIndex(where, true);
int fromIndex = draggedIndex;
dragImage.dispose();
dragImage = null;
draggedIndex = insertionIndex = -1;
if (toIndex != -1 && toIndex != fromIndex) {
Map newBounds = new TreeMap();
newBounds.put(new Integer(toIndex),
bounds.get(new Integer(fromIndex)));
if (fromIndex < toIndex) {
for (int i=fromIndex+1;i <= toIndex;i++) {
newBounds.put(new Integer(i-1),
bounds.get(new Integer(i)));
}
}
else {
for (int i=toIndex;i < fromIndex;i++) {
newBounds.put(new Integer(i+1),
bounds.get(new Integer(i)));
}
}
bounds.putAll(newBounds);
move(fromIndex, toIndex);
}
}
private boolean reposition() {
boolean changed = false;
for (Iterator i=bounds.keySet().iterator();i.hasNext();) {
Integer x = (Integer)i.next();
Rectangle current = getCurrentCellBounds(x.intValue());
Rectangle end = getCellBoundsAfterInsertion(x.intValue());
if (current.x != end.x || current.y != end.y) {
int xdelta = (end.x - current.x)/2;
int ydelta = (end.y - current.y)/2;
if (xdelta == 0)
current.x = end.x;
else
current.x += xdelta;
if (ydelta == 0)
current.y = end.y;
else
current.y += ydelta;
bounds.put(x, current);
changed = true;
}
}
return changed;
}
private int getIndex(Point where, boolean restrict) {
int idx = list.locationToIndex(where);
if (!restrict) {
int size = list.getModel().getSize();
// Assumes the list considers points below the last item
// be within last item
Rectangle last = list.getCellBounds(size-1, size-1);
if (idx == size-1 && where.y > last.y + last.height) {
idx = size;
}
}
return idx;
}
public synchronized void setInsertionLocation(Point where) {
// Avoid painting focus and/or selection bgs, kind of a hack
getPainter().requestFocus();
list.clearSelection();
setInsertionIndex(getIndex(where, draggedIndex != -1));
dragImage.setLocation(where);
}
public synchronized void setInsertionIndex(int idx) {
if (idx != insertionIndex) {
insertionIndex = idx;
repaint();
}
}
private Rectangle getCellBoundsAfterInsertion(int index) {
Rectangle r = list.getCellBounds(index, index);
if (draggedIndex != -1) {
if (index > draggedIndex) {
if (index <= insertionIndex) {
Rectangle r2 = list.getCellBounds(draggedIndex, draggedIndex);
r.y -= r2.height;
}
}
else if (index < draggedIndex) {
if (index >= insertionIndex) {
Rectangle r2 = list.getCellBounds(draggedIndex, draggedIndex);
r.y += r2.height;
}
}
else {
Rectangle r2 = list.getCellBounds(insertionIndex, insertionIndex);
r.y = r2.y;
}
}
else if (insertionIndex != -1 && index > insertionIndex) {
ListCellRenderer rnd = list.getCellRenderer();
Component c = rnd.getListCellRendererComponent(list, getPlaceholder(), insertionIndex, false, false);
r.y += c.getHeight();
}
return r;
}
private Rectangle getCurrentCellBounds(int cellIndex) {
Rectangle r = getCellBoundsAfterInsertion(cellIndex);
Rectangle r2 = (Rectangle)bounds.get(new Integer(cellIndex));
if (r2 != null) {
r.x = r2.x;
r.y = r2.y;
}
return r;
}
public synchronized void paint(Graphics g) {
boolean db = list.isDoubleBuffered();
list.setDoubleBuffered(false);
try {
Rectangle b = getDecorationBounds();
g.setColor(list.getBackground());
g.fillRect(b.x, b.y, b.width, b.height);
for (int i=0;i < list.getModel().getSize();i++) {
if (i == draggedIndex)
continue;
Rectangle r = getCurrentCellBounds(i);
Graphics g2 = g.create(r.x, r.y, r.width, r.height);
Rectangle r2 = list.getCellBounds(i, i);
((Graphics2D)g2).translate(0, -r2.y);
list.paint(g2);
}
}
finally {
list.setDoubleBuffered(db);
}
}
/** Simple JList-local drag/drop handler. Invokes the animator according
* to user input. A similar method could be used to accept drags
* originating outside of the JList.
*/
static class Listener extends MouseAdapter implements MouseMotionListener {
private ListAnimator animator;
private boolean dragActive;
private Point origin;
public Listener(ListAnimator animator) {
this.animator = animator;
}
private boolean sufficientMove(Point where) {
int dx = Math.abs(origin.x-where.x);
int dy = Math.abs(origin.y-where.y);
return Math.sqrt(dx*dx+dy*dy) > 5;
}
public void mousePressed(MouseEvent e) {
origin = e.getPoint();
}
public void mouseReleased(MouseEvent e) {
if (dragActive) {
animator.endDrag(e.getPoint());
dragActive = false;
}
}
public void mouseDragged(MouseEvent e) {
if (!dragActive) {
if (sufficientMove(e.getPoint())) {
animator.startDrag(origin);
dragActive = true;
}
}
if (dragActive)
animator.setInsertionLocation(e.getPoint());
}
public void mouseExited(MouseEvent e) {
if (dragActive)
animator.setInsertionIndex(-1);
}
public void mouseEntered(MouseEvent e) {
if (dragActive)
animator.setInsertionLocation(e.getPoint());
}
public void mouseMoved(MouseEvent e) {
}
}
private static final List DATA = new ArrayList(Arrays.asList(new Object[] {
"Happy", "Bashful", "Grumpy", //"Sneezy", "Dopey", "Sleepy", "Doc",
//"Snow White", "Prince Charming", "Wicked Witch",
}));
/** Throw up a frame to demonstrate the animator at work. Funkify the
* list renderer to demonstrate that customized renderers are handled
* properly.
*/
public static void main(String[] args) {
JFrame f = new JFrame("Smooth List Drop");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final JList list = new JList(new AbstractListModel() {
public int getSize() {
return DATA.size();
}
public Object getElementAt(int index) {
return DATA.get(index);
}
});
ListAnimator smoother = new ListAnimator(list) {
protected void move(int fromIndex, int toIndex) {
Object o = DATA.remove(fromIndex);
DATA.add(toIndex, o);
list.revalidate();
list.repaint();
}
};
Listener listener = new Listener(smoother);
list.addMouseListener(listener);
list.addMouseMotionListener(listener);
list.setCellRenderer(new DefaultListCellRenderer() {
public Component getListCellRendererComponent(JList list, Object value,
int index, boolean sel, boolean focus) {
Component c = super.getListCellRendererComponent(list, value, index, sel, focus);
Color bg = c.getBackground();
if ((index % 2) == 0) {
int GRAY = 190;
c.setBackground(new Color((GRAY + bg.getRed()*2)/3,
(GRAY + bg.getGreen()*2)/3,
(GRAY + bg.getBlue()*2)/3));
((JComponent)c).setOpaque(true);
}
else if (c.getBackground().equals(list.getBackground())){
((JComponent)c).setOpaque(false);
}
if (value.toString().startsWith("S")) {
c.setForeground(Color.blue);
}
else if (value.toString().startsWith("D")) {
Font font = list.getFont();
c.setFont(font.deriveFont(Font.ITALIC));
}
return c;
}
});
JLabel label = new JLabel("Drag items to reorder");
label.setBorder(new EmptyBorder(4,4,4,4));
label.setFont(label.getFont().deriveFont(Font.BOLD, label.getFont().getSize()*2));
label.putClientProperty("decorator", new AbstractComponentDecorator(label, -1) {
public void paint(Graphics g) {
Rectangle b = getDecorationBounds();
((Graphics2D)g).setPaint(new GradientPaint(0, b.height/2, list.getSelectionBackground(),
b.width/2, b.height/2, Color.white));
g.fillRect(b.x, b.y, b.width, b.height);
}
});
f.getContentPane().add(label, BorderLayout.NORTH);
f.getContentPane().add(new JScrollPane(list));
f.pack();
f.setVisible(true);
}
}