// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// This program is 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;
// either version 2 of the License, or (at your option) any later version.
//
// This program 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, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id: DNDList.java,v 1.23 2007/03/07 18:48:43 spyromus Exp $
//
package com.salas.bb.utils.dnd;
import com.jgoodies.uif.util.ResourceUtils;
import com.jgoodies.uif.util.SystemUtils;
import javax.swing.*;
import javax.swing.plaf.basic.BasicHTML;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.net.URL;
/**
* List component with advanced dragging support.
*/
public class DNDList extends JList
{
/** Name of dragging property. */
public static final String PROP_DRAGGING = "dragging";
/** Mouse dragged fake property used to broadcast the dragging event. */
public static final String PROP_MOUSE_DRAGGED_EVENT = "mouseDragged";
/** Number of pixels to deviate from original mouse pressure position to start dragging. */
private static final int DND_DEVIATION_GESTURE = 8;
private static final int DIVIDER_X = 2;
private static final int DIVIDER_WIDTH_SUB = 3;
public static final String[] STATES = { "Normal", "Armed", "Dragging" };
public static final int STATE_NORMAL = 0;
public static final int STATE_ARMED = 1;
public static final int STATE_DRAGGING = 2;
private int state;
private int pressPointX;
private int pressPointY;
private DNDPopup popup;
/** <code>TRUE</code> if dragging is performed within the list, <code>FALSE</code> otherwise. */
private boolean draggingInternal;
private int insertPosition;
/** <code>TRUE</code> if copy-dragging is allowed from this list. */
private final boolean copyingAllowed;
/**
* Constructs list with given data model.
*
* @param dataModel data model.
*/
public DNDList(ListModel dataModel)
{
this(dataModel, true);
}
/**
* Constructs list with given data model.
*
* @param dataModel data model.
* @param aCopyingAllowed <code>TRUE</code> if drag-copying is allowed in this list.
*/
public DNDList(ListModel dataModel, boolean aCopyingAllowed)
{
super(dataModel);
copyingAllowed = aCopyingAllowed;
setState(STATE_NORMAL);
popup = new DNDPopup();
popup.setInvoker(this);
addMouseMotionListener(popup);
draggingInternal = false;
setInsertionPosition(-1);
enableEvents(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
}
/**
* Returns <code>TRUE</code> when in dragging state.
*
* @return <code>TRUE</code> when in dragging state.
*/
private boolean isDragging()
{
return state == STATE_DRAGGING;
}
/**
* Returns <code>TRUE</code> if dragging is performed within the list.
*
* @return <code>TRUE</code> if dragging is performed within the list.
*/
public boolean isDraggingInternal()
{
return draggingInternal;
}
/**
* Returns the index within the list where to put items which has been dragged.
*
* @return index.
*/
public int getInsertPosition()
{
return insertPosition;
}
/**
* Changes the state of the list.
*
* @param state the state.
*
* @see #STATE_ARMED
* @see #STATE_DRAGGING
* @see #STATE_NORMAL
*/
private void setState(int state)
{
boolean oldIsDragging = isDragging();
this.state = state;
firePropertyChange(PROP_DRAGGING, oldIsDragging, isDragging());
}
/**
* Processes mouse buttons events (pressed, clicked, released).
*
* @param e even.
*/
protected void processMouseEvent(MouseEvent e)
{
boolean entered = e.getID() == MouseEvent.MOUSE_ENTERED;
boolean exited = e.getID() == MouseEvent.MOUSE_EXITED;
boolean released = e.getID() == MouseEvent.MOUSE_RELEASED;
boolean pressed = e.getID() == MouseEvent.MOUSE_PRESSED;
// Skip processing of event when entering the list in dragging mode
if (!entered || !isDragging()) super.processMouseEvent(e);
if (entered)
{
// Mark as internal dragging
draggingInternal = true;
if (isDragging()) onDraggingMove(e);
} else if (exited)
{
// Mark as external dragging
draggingInternal = false;
setInsertionPosition(-1);
} else
{
// Buttons events
// Depending on the state and event came process clicks differently
switch (state)
{
case STATE_NORMAL:
if (pressed && SwingUtilities.isLeftMouseButton(e) &&
(!SystemUtils.IS_OS_MAC ||
(e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) == 0)) onArm(e);
break;
case STATE_ARMED:
if (released) onDisarm();
break;
case STATE_DRAGGING:
if (released) onFinishDragging();
break;
}
}
}
/**
* Invoked when user has pressed the button and ready to release it or continue
* with dragging.
*
* @param e event.
*/
private void onArm(MouseEvent e)
{
if (!isSelectionEmpty())
{
setState(STATE_ARMED);
pressPointX = (int)e.getPoint().getX();
pressPointY = (int)e.getPoint().getY();
}
}
/**
* Invoked when user releases mouse button.
*/
private void onDisarm()
{
setState(STATE_NORMAL);
}
/**
* Invoked when we have an approval of user's intention to drag something.
*
* @param e event.
*/
private void onStartDragging(MouseEvent e)
{
setState(STATE_DRAGGING);
// Show popup with image of items we are going to drag
Image dndObjectImage = createDragImage();
popup.setImage(dndObjectImage);
popup.setLocation(e.getPoint());
popup.setVisible(true);
// Register dragging start in context
DNDListContext.startDragging(this, createDNDObjectFromSelectedItems(getSelectedValues()));
}
/**
* Invoked when dragging has finished.
*/
private void onFinishDragging()
{
// Register finish in context.
DNDListContext.finishDragging();
// Hide popup and repaint the list.
popup.setVisible(false);
repaint();
onDisarm();
}
/**
* Creates image of items being dragged.
*
* @return image.
*/
private Image createDragImage()
{
BufferedImage bi = null;
int itemsSelected = getSelectedIndices().length;
int selectedIndex = getSelectedIndex();
// get renderer for curretly selected cell and record its image
ListCellRenderer cr = getCellRenderer();
Component c = cr.getListCellRendererComponent(this, this.getSelectedValue(),
selectedIndex, true, true);
if (c != null)
{
Rectangle r = getCellBounds(selectedIndex, selectedIndex);
bi = new BufferedImage(r.width + 5 * (itemsSelected - 1),
r.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = bi.createGraphics();
c.setSize(r.width, r.height);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 1f));
c.paint(g);
for (int i = 1; i < itemsSelected; i++)
{
g.copyArea(r.width - 5, 0, 5, r.height, 5 * i, 0);
}
}
return bi;
}
/**
* This is a hook to customize creation of dragging context object. Each implementation
* or specific list can tune this to return own class of draggables. A quick example can
* be having two lists (lorries and vegetables). We can't drag cars to vegetables, but it's
* possible to drag vegetables to cars. So in each list it's possible to define appropriate
* holder and analyze it on the recipient's side.
*
* @param selectedValues list of selected values, which are going to be dragged.
*
* @return drag object for context.
*/
protected IDNDObject createDNDObjectFromSelectedItems(Object[] selectedValues)
{
return new DefaultDNDObject(selectedValues);
}
/**
* Processing of motion events. When in armed state we verify if it's time to start
* dragging and in dragging state we compute insertion point and keep an eye on where
* the pointer is -- inside or outside the control.
*
* @param e event.
*/
protected void processMouseMotionEvent(MouseEvent e)
{
boolean consume = false;
switch (state)
{
case STATE_ARMED:
verifyDraggingGesture(e);
consume = true;
break;
case STATE_DRAGGING:
boolean dragged = e.getID() == MouseEvent.MOUSE_DRAGGED;
if (dragged || e.getID() == MouseEvent.MOUSE_MOVED)
{
onDraggingMove(e);
// We are required to fire this event to let the components
// we drag object over receive mouse dragging/moving events.
if (dragged) firePropertyChange(PROP_MOUSE_DRAGGED_EVENT, null, e);
consume = !dragged;
}
break;
}
if (!consume) super.processMouseMotionEvent(e);
}
/**
* Invoked when mouse pointer has been moved while in dragging mode.
*
* @param e event.
*/
private void onDraggingMove(MouseEvent e)
{
// Move popup to the pointer.
popup.setLocation(e.getPoint());
popup.setVisible(true);
// Calculate new insertion point.
int index = -1;
if (contains(e.getPoint()))
{
index = locationToIndex(e.getPoint());
if (index != -1)
{
// if we have row there then calculate the nearest insertion point
final Rectangle r = getCellBounds(index, index);
if (e.getPoint().y - r.y > r.height / 2) index++;
}
}
// Update insertion point and repaint the list if necessary.
setInsertionPosition(index);
}
/**
* Updates the insertion point.
*
* @param aIndex new point index or (<code>-1</code>).
*/
private void setInsertionPosition(int aIndex)
{
if (insertPosition != aIndex)
{
insertPosition = aIndex;
if (isDragging()) repaint();
}
}
/**
* Verifies if it's time to start dragging. We start dragging something from a mouse button
* press event. User can then continue to move the pointer a bit without actually having an
* intent to drag the item -- just a slight tremble in the hands. If we detect that the mouse
* pointer has moved far enough from press position to think of it as intentional dragging,
* we continue with dragging initiation.
*
* @param e event.
*/
private void verifyDraggingGesture(MouseEvent e)
{
int newX = (int)e.getPoint().getX();
int newY = (int)e.getPoint().getY();
if (Math.abs(newX - pressPointX) > DND_DEVIATION_GESTURE ||
Math.abs(newY - pressPointY) > DND_DEVIATION_GESTURE)
{
onStartDragging(e);
}
}
/**
* Invoked by Swing to draw components.
* Applications should not invoke <code>paint</code> directly,
* but should instead use the <code>repaint</code> method to
* schedule the component for redrawing.
*
* @param g the <code>Graphics</code> context in which to paint
*/
public void paint(Graphics g)
{
super.paint(g);
// if in DND mode and position positive then draw insertion line
if (isDragging() && insertPosition >= 0)
{
Point origin;
final int size = getModel().getSize();
if (insertPosition >= size)
{
// after the last row
final int newIndex = size - 1;
if (newIndex != -1)
{
// not an empty list
Point p = indexToLocation(newIndex);
Rectangle r = getCellBounds(newIndex, newIndex);
origin = new Point(0, p.y + r.height);
} else
{
// list is empty
origin = new Point(2, 2);
}
} else
{
// somewhere in the list
origin = indexToLocation(insertPosition);
}
// draw the line
g.drawLine(DIVIDER_X, origin.y - 1, getWidth() - DIVIDER_WIDTH_SUB, origin.y - 1);
g.drawLine(DIVIDER_X, origin.y, getWidth() - DIVIDER_WIDTH_SUB, origin.y);
}
}
/**
* Creates tooltip.
*
* @return tooltip.
*/
public JToolTip createToolTip()
{
JToolTip toolTip = super.createToolTip();
URL url = ResourceUtils.getURL("resources");
if (url != null)
{
toolTip.putClientProperty(BasicHTML.documentBaseKey, url);
}
return toolTip;
}
/**
* Called back by d'n'd context when the copying status changes.
*
* @param copying <code>TRUE</code> when copying.
*/
public void copyModeStateChanged(boolean copying)
{
popup.setCopying(copying);
}
/**
* Returns <code>TRUE</code> if drag-copying is allowed in this list.
*
* @return <code>TRUE</code> if drag-copying is allowed in this list.
*/
public boolean isCopyingAllowed()
{
return copyingAllowed;
}
}