/*
*------------------------------------------------------------------------------
* Copyright (C) 2006-2015 University of Dundee & Open Microscopy Environment.
* All rights reserved.
*
*
* 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.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
*------------------------------------------------------------------------------
*/
package org.openmicroscopy.shoola.agents.util.dnd;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.SystemColor;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.activation.ActivationDataFlavor;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JTree;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import org.openmicroscopy.shoola.agents.util.EditorUtil;
import org.openmicroscopy.shoola.agents.util.browser.TreeImageDisplay;
import org.openmicroscopy.shoola.agents.util.browser.TreeImageNode;
import org.openmicroscopy.shoola.agents.util.browser.TreeImageTimeSet;
import org.openmicroscopy.shoola.util.ui.IconManager;
import omero.gateway.model.ExperimenterData;
import omero.gateway.model.GroupData;
/**
* Adds Drag and Drop facility to the tree.
*
* @author Jean-Marie Burel
* <a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a>
* @since Beta4.4
*/
public class DnDTree
extends JTree
implements DragSourceListener, DropTargetListener, DragGestureListener
{
/** Bound property indicating that the D&D is completed.*/
public static final String DRAGGED_PROPERTY = "dragged";
/** The supported flavors.*/
static DataFlavor[] supportedFlavors;
/** The flavor.*/
static DataFlavor localFlavor;
/** The default color.*/
private static Color DEFAULT_COLOR = new Color(255, 255, 255, 0);
static {
try {
localFlavor = new ActivationDataFlavor(String.class, "Dummy Flavor");
} catch (Exception e) {
}
if (localFlavor != null) {
supportedFlavors = new DataFlavor[1];
supportedFlavors[0] = localFlavor;
}
}
/** The dragging source.*/
private DragSource dragSource;
/** The node selected as a target.*/
private TreeNode dropTargetNode;
/** The image representing the dragged object.*/
private BufferedImage imgGhost;
/**
* Flag indicating if the user currently logged in an administrator or not.
*/
private boolean administrator;
/** The identifier of the user currently logged in.*/
private long userID;
/** Cursor indicating that the drop action is not permitted.*/
private Cursor cursor;
/** The default cursor.*/
private Cursor defaultCursor;
/** Flag indicating if the drop action is allowed or not.*/
private boolean dropAllowed;
/** The location of the drop.*/
private int dropLocation;
/** Track the last mouse drag position */
private Point lastPosition;
/** outer DnD autoscroll rectable */
private Rectangle outer;
/** inner DnD autoscroll rectable */
private Rectangle inner;
/** DnD autoscroll timer */
private Timer timer;
/** DnD cursorHysteresis */
private int hysteresis = 10;
/** DnD autoscroll insets (this defines the scroll sensitive area) */
private static final int AUTOSCROLL_INSET = 10;
/** The data currently dragged */
private List<TreeImageDisplay> toTransfer = new ArrayList<TreeImageDisplay>();
/**
* Sets the cursor depending on the selected node.
*
* @param node
* The destination node.
* @param transferable
* The object hosting the nodes to move.
*/
private void handleMouseOver(TreeImageDisplay node,
Transferable transferable) {
TreeImageDisplay parent = node;
if (node.isLeaf() && node instanceof TreeImageNode) {
parent = (TreeImageDisplay) node.getParent();
}
Object ot = parent.getUserObject();
if (!canLink(ot)
&& !(ot instanceof ExperimenterData || ot instanceof GroupData)) {
dropAllowed = false;
setCursor(createCursor());
return;
}
// Now check that the src and target are compatible.
try {
List<TreeImageDisplay> nodes = new ArrayList<TreeImageDisplay>();
nodes.addAll(toTransfer);
if (nodes.size() == 0)
return;
// Check the first node
TreeImageDisplay first = nodes.get(0);
Object child = first.getUserObject();
if (ot instanceof GroupData && child instanceof ExperimenterData
&& !administrator) {
setCursor(createCursor());
dropAllowed = false;
return;
}
List<TreeImageDisplay> list = new ArrayList<TreeImageDisplay>();
Iterator<TreeImageDisplay> i = nodes.iterator();
TreeImageDisplay n;
Object os = null;
int childCount = 0;
while (i.hasNext()) {
n = i.next();
os = n.getUserObject();
if (parent.contains(n)) {
childCount++;
} else {
if (EditorUtil.isTransferable(ot, os, userID)) {
if (ot instanceof GroupData) {
if (os instanceof ExperimenterData && administrator)
list.add(n);
else {
if (canLink(os))
list.add(n);
}
} else {
if (canLink(os))
list.add(n);
}
}
}
}
if (childCount == nodes.size() || list.size() == 0
|| (list.size() == 1 && parent == list.get(0))) {
setCursor(createCursor());
dropAllowed = false;
}
} catch (Exception e) {
dropAllowed = false;
}
}
/**
* Returns <code>true</code> if the user currently logged in is the owner
* of the object, <code>false</code> otherwise.
*
* @param ho The object to handle.
* @return See above.
*/
private boolean canLink(Object ho)
{
if (ho instanceof TreeImageTimeSet) {
TreeImageDisplay n = EditorUtil.getDataOwner((TreeImageDisplay) ho);
if (n == null) return true;
ExperimenterData exp = (ExperimenterData) n.getUserObject();
return (exp.getId() == userID);
}
return EditorUtil.isUserOwner(ho, userID);
}
/**
* Returns the cursor corresponding to the dragged action.
*
* @param action The action to handle.
* @return See above.
*/
private Cursor getCursor(int action)
{
return defaultCursor;
}
/**
* Creates or recycles the image cursor.
*
* @return See above.
*/
private Cursor createCursor()
{
if (cursor != null) return cursor;
IconManager icons = IconManager.getInstance();
Image image = icons.getImageIcon(IconManager.NO_ENTRY).getImage();
Toolkit tk = Toolkit.getDefaultToolkit();
cursor = tk.createCustomCursor(image , new Point(0, 0), "img");
return cursor;
}
/**
* Creates a ghost image.
*
* @param path The selected path.
* @param p The origin of the dragging.
*/
private void createGhostImage(Point p)
{
TreePath[] paths = getSelectionPaths();
if (paths == null || paths.length == 0) return;
Rectangle rect = new Rectangle();
rect.x = Integer.MAX_VALUE;
rect.y = Integer.MIN_VALUE;
TreePath path;
Rectangle r;
int[] values = new int[paths.length];
int y = 0;
for (int i = 0; i < paths.length; i++) {
values[i] = y;
path = paths[i];
r = getPathBounds(path);
if (rect.width < r.width) rect.width = r.width;
rect.height += r.height;
rect.x = Math.min(rect.x, r.x);
rect.y = Math.max(rect.y, r.y);
y += r.height;
}
imgGhost = new BufferedImage((int) rect.getWidth(),
(int) rect.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE);
Graphics2D g2 = imgGhost.createGraphics();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 0.5f));
JLabel label;
Icon icon = null;
int offset = -1;
int v;
for (int i = 0; i < paths.length; i++) {
path = paths[i];
r = getPathBounds(path);
label = (JLabel) getCellRenderer().getTreeCellRendererComponent(
this, path.getLastPathComponent(), false, isExpanded(path),
getModel().isLeaf(path.getLastPathComponent()), 0, false);
icon = label.getIcon();
if (icon != null) {
icon.paintIcon(label, g2, 0, values[i]);
}
v = (icon == null) ? 0 : icon.getIconWidth();
v += label.getIconTextGap();
g2.setColor(label.getForeground());
g2.setFont(label.getFont());
FontMetrics fm = g2.getFontMetrics();
g2.drawString(label.getText(), v, values[i]+fm.getAscent()+1);
if (offset < 0) {
offset = (icon == null) ? 0 : icon.getIconWidth();
offset += label.getIconTextGap();
}
}
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_OVER,
0.5f));
g2.setPaint(new GradientPaint(offset, 0,
SystemColor.controlShadow, getWidth(), 0, DEFAULT_COLOR));
g2.fillRect(offset, 0, getWidth(), imgGhost.getHeight());
g2.dispose();
}
/**
* Creates a new instance.
*
* @param userID The identifier of the user currently logged in.
* @param administrator Pass <code>true</code> to indicate that the user
* currently logged in an administrator,
* <code>false</code> otherwise.
*/
public DnDTree(long userID, boolean administrator)
{
super();
setName("project tree");
defaultCursor = getCursor();
dropLocation = -1;
reset(userID, administrator);
setDragEnabled(true);
dropTargetNode = null;
dragSource = new DragSource();
DropTarget target = new DropTarget(this, this);
target.setDefaultActions(DnDConstants.ACTION_COPY_OR_MOVE);
dragSource.createDefaultDragGestureRecognizer(this,
DnDConstants.ACTION_MOVE, this);
addFocusListener(new FocusAdapter() {
public void focusLost(FocusEvent e) {
setCursor(defaultCursor);
}
});
outer = new Rectangle();
inner = new Rectangle();
Toolkit t = Toolkit.getDefaultToolkit();
Integer prop;
ActionListener al = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
updateRegion();
Point componentPosition = new Point(lastPosition);
SwingUtilities.convertPointFromScreen(componentPosition,
DnDTree.this);
if (outer.contains(componentPosition)
&& !inner.contains(componentPosition)) {
autoscroll(componentPosition);
}
}
};
prop = (Integer) t.getDesktopProperty("DnD.Autoscroll.interval");
timer = new Timer(prop == null ? 100 : prop.intValue(), al);
prop = (Integer) t.getDesktopProperty("DnD.Autoscroll.initialDelay");
timer.setInitialDelay(prop == null ? 100 : prop.intValue());
prop = (Integer) t
.getDesktopProperty("DnD.Autoscroll.cursorHysteresis");
if (prop != null) {
hysteresis = prop.intValue();
}
}
/**
* Autoscroll to position
*/
private void autoscroll(Point position) {
Scrollable s = (Scrollable) this;
if (position.y < inner.y) {
// scroll upwards
int dy = s.getScrollableUnitIncrement(outer,
SwingConstants.VERTICAL, -1);
Rectangle r = new Rectangle(inner.x, outer.y - dy, inner.width, dy);
scrollRectToVisible(r);
} else if (position.y > (inner.y + inner.height)) {
// scroll downwards
int dy = s.getScrollableUnitIncrement(outer,
SwingConstants.VERTICAL, 1);
Rectangle r = new Rectangle(inner.x, outer.y + outer.height,
inner.width, dy);
scrollRectToVisible(r);
}
if (position.x < inner.x) {
// scroll left
int dx = s.getScrollableUnitIncrement(outer,
SwingConstants.HORIZONTAL, -1);
Rectangle r = new Rectangle(outer.x - dx, inner.y, dx, inner.height);
scrollRectToVisible(r);
} else if (position.x > (inner.x + inner.width)) {
// scroll right
int dx = s.getScrollableUnitIncrement(outer,
SwingConstants.HORIZONTAL, 1);
Rectangle r = new Rectangle(outer.x + outer.width, inner.y, dx,
inner.height);
scrollRectToVisible(r);
}
}
/**
* Updates inner/outer autoscroll regions
*/
private void updateRegion() {
// compute the outer
Rectangle visible = getVisibleRect();
outer.setBounds(visible.x, visible.y, visible.width, visible.height);
// compute the insets
Insets i = new Insets(0, 0, 0, 0);
if (this instanceof Scrollable) {
int minSize = 2 * AUTOSCROLL_INSET;
if (visible.width >= minSize) {
i.left = i.right = AUTOSCROLL_INSET;
}
if (visible.height >= minSize) {
i.top = i.bottom = AUTOSCROLL_INSET;
}
}
// set the inner from the insets
inner.setBounds(visible.x + i.left, visible.y + i.top, visible.width
- (i.left + i.right), visible.height - (i.top + i.bottom));
}
/**
* Resets the values.
*
* @param userID The identifier of the user currently logged in.
* @param administrator Pass <code>true</code> to indicate that the user
* currently logged in an administrator,
* <code>false</code> otherwise.
*/
public void reset(long userID, boolean administrator)
{
this.userID = userID;
this.administrator = administrator;
}
/** Resets.*/
public void reset() { dropTargetNode = null; }
/**
* Returns the drop target node.
*
* @return See above.
*/
public TreeNode getDropTargetNode() { return dropTargetNode; }
/**
* Returns the row where the node was dropped.
*
* @return See above.
*/
public int getRowDropLocation() { return dropLocation; }
/**
* Returns <code>true</code> if the drop action is allowed,
* <code>false</code> otherwise.
*
* @return See above.
*/
public boolean isDropAllowed() { return dropAllowed; }
/**
* Sets the target node.
* {@link DropTargetListener#dragOver(DropTargetDragEvent)}
*/
public void dragOver(DropTargetDragEvent dtde)
{
// figure out which cell it's over, no drag to self
Point dragPoint = dtde.getLocation();
TreePath path = getPathForLocation(dragPoint.x, dragPoint.y);
if (path == null)
dropTargetNode = null;
else
dropTargetNode = (TreeNode) path.getLastPathComponent();
repaint();
Point p = dtde.getLocation();
SwingUtilities.convertPointToScreen(p, this);
if (lastPosition != null) {
if (Math.abs(p.x - lastPosition.x) > hysteresis
|| Math.abs(p.y - lastPosition.y) > hysteresis) {
// no autoscroll
if (timer.isRunning()) timer.stop();
} else {
if (!timer.isRunning()) timer.start();
}
}
lastPosition = p;
setCursor(getCursor(dtde.getDropAction()));
dropAllowed = true;
if (path == null)
dropTargetNode = null;
else {
dropTargetNode = (TreeNode) path.getLastPathComponent();
// ins
Transferable trans = dtde.getTransferable();
if (trans != null && dropTargetNode instanceof TreeImageDisplay) {
handleMouseOver((TreeImageDisplay) dropTargetNode, trans);
}
}
repaint();
}
/**
* Drops the node and it to its destination.
* {@link DropTargetListener#dragOver(DropTargetDragEvent)}
*/
public void drop(DropTargetDropEvent dtde) {
timer.stop();
Point dropPoint = dtde.getLocation();
TreePath path = getPathForLocation(dropPoint.x, dropPoint.y);
dropLocation = getRowForPath(path);
setCursor(defaultCursor);
try {
if (!dropAllowed) {
dtde.rejectDrop();
repaint();
this.toTransfer.clear();
return;
}
} catch (Exception e) {
this.toTransfer.clear();
return;
}
boolean dropped = false;
try {
dtde.acceptDrop(DnDConstants.ACTION_MOVE);
List<TreeImageDisplay> nodes = new ArrayList<TreeImageDisplay>();
nodes.addAll(this.toTransfer);
if (nodes.size() == 0) {
dropped = true;
dtde.dropComplete(dropped);
repaint();
return;
}
TreeImageDisplay parent = null;
DefaultMutableTreeNode dropNode = (DefaultMutableTreeNode) path
.getLastPathComponent();
// if (dropNode instanceof TreeImageDisplay) {
if (dropNode instanceof TreeImageDisplay)
parent = (TreeImageDisplay) dropNode;
if (dropNode.isLeaf() && dropNode instanceof TreeImageNode) {
parent = (TreeImageDisplay) dropNode.getParent();
}
int action = DnDConstants.ACTION_MOVE;
ObjectToTransfer transfer = new ObjectToTransfer(parent, nodes,
action);
firePropertyChange(DRAGGED_PROPERTY, null, transfer);
this.toTransfer.clear();
// }
dropped = true;
} catch (Exception e) {
try {
dtde.rejectDrop();
} catch (Exception ex) {
}
repaint();
}
dtde.dropComplete(dropped);
repaint();
}
/**
* Starts dragging the node.
* @see DragGestureListener#dragGestureRecognized(DragGestureEvent)
*/
public void dragGestureRecognized(DragGestureEvent e)
{
Point p = e.getDragOrigin();
TreePath path = getPathForLocation(p.x, p.y);
if (path == null) return;
TreeNode draggedNode = (TreeNode) path.getLastPathComponent();
if (draggedNode == null) return;
TreePath[] paths = getSelectionPaths();
List<TreeImageDisplay> nodes = new ArrayList<TreeImageDisplay>();
TreeNode n;
if (paths != null) {
for (int i = 0; i < paths.length; i++) {
n = (TreeNode) paths[i].getLastPathComponent();
if (n instanceof TreeImageDisplay)
nodes.add((TreeImageDisplay)n);
}
}
if (nodes.size() == 0) return;
createGhostImage(p);
this.toTransfer.addAll(nodes);
Transferable trans = new TransferableNode("");
try {
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
if (DragSource.isDragImageSupported()) {
e.startDrag(getCursor(e.getDragAction()), imgGhost,
new Point(5, 5), trans, this);
} else {
e.startDrag(getCursor(e.getDragAction()), trans, this);
}
} catch (Exception ex) {
//already an on-going dragging action.
}
}
/**
* Implemented as specified by {@link DropTargetListener} I/F but
* no-operation in our case.
* {@link DropTargetListener#dropActionChanged(DropTargetDragEvent)}
*/
public void dropActionChanged(DropTargetDragEvent dtde) {}
/**
* Implemented as specified by {@link DragSourceListener} I/F but
* no-operation in our case.
* {@link DragSourceListener#dragDropEnd(DragSourceDropEvent)}
*/
public void dragDropEnd(DragSourceDropEvent dsde)
{
}
/**
* Implemented as specified by {@link DragSourceListener} I/F but
* no-operation in our case.
* {@link DragSourceListener#dragEnter(DragSourceDragEvent)}
*/
public void dragEnter(DragSourceDragEvent dsde)
{
setCursor(defaultCursor);
}
/**
* Implemented as specified by {@link DragSourceListener} I/F but
* no-operation in our case.
* {@link DragSourceListener#dragExit(DragSourceEvent)}
*/
public void dragExit(DragSourceEvent dse)
{
dse.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
}
/**
* Modifies the cursor if the elements can be dropped in the node mouse over.
* {@link DragSourceListener#dragOver(DragSourceDragEvent)}
*/
public void dragOver(DragSourceDragEvent dsde)
{
}
/**
* Implemented as specified by {@link DragSourceListener} I/F but
* no-operation in our case.
* {@link DragSourceListener#dropActionChanged(DragSourceDragEvent)}
*/
public void dropActionChanged(DragSourceDragEvent dsde)
{
dsde.getDragSourceContext().setCursor(getCursor(dsde.getDropAction()));
}
/**
* Implemented as specified by {@link DropTargetListener} I/F but
* no-operation in our case.
* {@link DropTargetListener#dragEnter(DropTargetDragEvent)}
*/
public void dragEnter(DropTargetDragEvent dtde) {}
/**
* Implemented as specified by {@link DropTargetListener} I/F but
* no-operation in our case.
* {@link DropTargetListener#dragExit(DropTargetEvent)}
*/
public void dragExit(DropTargetEvent dte) {
timer.stop();
}
}