/*
All programs in this directory and subdirectories are published under the
GNU General Public License as described below.
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
Further information about the GNU GPL is available at:
http://www.gnu.org/copyleft/gpl.ja.html
*/
package net.sf.jabref.groups;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.*;
import java.awt.event.InputEvent;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Vector;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.undo.AbstractUndoableEdit;
import net.sf.jabref.BibtexEntry;
import net.sf.jabref.Globals;
import net.sf.jabref.Util;
public class GroupsTree extends JTree implements DragSourceListener,
DropTargetListener, DragGestureListener {
/** distance from component borders from which on autoscrolling starts. */
private static final int dragScrollActivationMargin = 10;
/** number of pixels to scroll each time handler is called. */
private static final int dragScrollDistance = 5;
/** time of last autoscroll event (for limiting speed). */
private static long lastDragAutoscroll = 0L;
/** minimum interval between two autoscroll events (for limiting speed). */
private static final long minAutoscrollInterval = 50L;
/**
* the point on which the cursor is currently idling during a drag
* operation.
*/
private Point idlePoint;
/** time since which cursor is idling. */
private long idleStartTime = 0L;
/** max. distance cursor may move in x or y direction while idling. */
private static final int idleMargin = 1;
/** idle time after which the node below is expanded. */
private static final long idleTimeToExpandNode = 1000L;
private GroupSelector groupSelector;
private GroupTreeNode dragNode = null;
private final GroupTreeCellRenderer cellRenderer = new GroupTreeCellRenderer();
public GroupsTree(GroupSelector groupSelector) {
this.groupSelector = groupSelector;
DragGestureRecognizer dgr = DragSource.getDefaultDragSource()
.createDefaultDragGestureRecognizer(this,
DnDConstants.ACTION_MOVE, this);
// Eliminates right mouse clicks as valid actions
dgr.setSourceActions(dgr.getSourceActions() & ~InputEvent.BUTTON3_MASK);
new DropTarget(this, this);
setCellRenderer(cellRenderer);
setFocusable(false);
setToggleClickCount(0);
ToolTipManager.sharedInstance().registerComponent(this);
setShowsRootHandles(false);
setVisibleRowCount(Globals.prefs.getInt("groupsVisibleRows"));
getSelectionModel().setSelectionMode(
TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
}
public void dragEnter(DragSourceDragEvent dsde) {
// ignore
}
/** This is for moving of nodes within myself */
public void dragOver(DragSourceDragEvent dsde) {
final Point p = dsde.getLocation(); // screen coordinates!
SwingUtilities.convertPointFromScreen(p, this);
final TreePath path = getPathForLocation(p.x, p.y);
if (path == null) {
dsde.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
return;
}
final GroupTreeNode target = (GroupTreeNode) path
.getLastPathComponent();
if (target == null || dragNode.isNodeDescendant(target)
|| dragNode == target) {
dsde.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
return;
}
dsde.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop);
}
public void dropActionChanged(DragSourceDragEvent dsde) {
// ignore
}
public void dragDropEnd(DragSourceDropEvent dsde) {
dragNode = null;
}
public void dragExit(DragSourceEvent dse) {
// ignore
}
public void dragEnter(DropTargetDragEvent dtde) {
// ignore
}
/** This handles dragging of nodes (from myself) or entries (from the table) */
public void dragOver(DropTargetDragEvent dtde) {
final Point cursor = dtde.getLocation();
final long currentTime = System.currentTimeMillis();
if (idlePoint == null)
idlePoint = cursor;
// determine node over which the user is dragging
final TreePath path = getPathForLocation(cursor.x, cursor.y);
final GroupTreeNode target = path == null ? null : (GroupTreeNode) path
.getLastPathComponent();
setHighlight1Cell(target);
// accept or reject
if (dtde.isDataFlavorSupported(GroupTreeNode.flavor)) {
// accept: move nodes within tree
dtde.acceptDrag(DnDConstants.ACTION_MOVE);
} else if (dtde
.isDataFlavorSupported(TransferableEntrySelection.flavorInternal)) {
// check if node accepts explicit assignment
if (path == null) {
dtde.rejectDrag();
} else {
// this would be the place to check if the dragging entries
// maybe are in this group already, but I think that's not
// worth the bother (DropTargetDragEvent does not provide
// access to the drag object)...
// it might even be irritating to the user.
if (target.getGroup().supportsAdd()) {
// accept: assignment from EntryTable
dtde.acceptDrag(DnDConstants.ACTION_LINK);
} else {
dtde.rejectDrag();
}
}
} else {
dtde.rejectDrag();
}
// auto open
if (Math.abs(cursor.x - idlePoint.x) < idleMargin
&& Math.abs(cursor.y - idlePoint.y) < idleMargin) {
if (currentTime - idleStartTime >= idleTimeToExpandNode) {
if (path != null) {
expandPath(path);
}
}
} else {
idlePoint = cursor;
idleStartTime = currentTime;
}
// autoscrolling
if (currentTime - lastDragAutoscroll < minAutoscrollInterval)
return;
final Rectangle r = getVisibleRect();
final boolean scrollUp = cursor.y - r.y < dragScrollActivationMargin;
final boolean scrollDown = r.y + r.height - cursor.y < dragScrollActivationMargin;
final boolean scrollLeft = cursor.x - r.x < dragScrollActivationMargin;
final boolean scrollRight = r.x + r.width - cursor.x < dragScrollActivationMargin;
if (scrollUp)
r.translate(0, -dragScrollDistance);
else if (scrollDown)
r.translate(0, +dragScrollDistance);
if (scrollLeft)
r.translate(-dragScrollDistance, 0);
else if (scrollRight)
r.translate(+dragScrollDistance, 0);
scrollRectToVisible(r);
lastDragAutoscroll = currentTime;
}
public void dropActionChanged(DropTargetDragEvent dtde) {
// ignore
}
public void drop(DropTargetDropEvent dtde) {
setHighlight1Cell(null);
try {
// initializations common to all flavors
final Transferable transferable = dtde.getTransferable();
final Point p = dtde.getLocation();
final TreePath path = getPathForLocation(p.x, p.y);
if (path == null) {
dtde.rejectDrop();
return;
}
final GroupTreeNode target = (GroupTreeNode) path
.getLastPathComponent();
// check supported flavors
if (transferable.isDataFlavorSupported(GroupTreeNode.flavor)) {
GroupTreeNode source = (GroupTreeNode) transferable
.getTransferData(GroupTreeNode.flavor);
if (source == target) {
dtde.rejectDrop(); // ignore this
return;
}
if (source.isNodeDescendant(target)) {
dtde.rejectDrop();
return;
}
Enumeration<TreePath> expandedPaths = groupSelector.getExpandedPaths();
UndoableMoveGroup undo = new UndoableMoveGroup(groupSelector,
groupSelector.getGroupTreeRoot(), source, target,
target.getChildCount());
target.add(source);
dtde.getDropTargetContext().dropComplete(true);
// update selection/expansion state
groupSelector.revalidateGroups(new TreePath[] { new TreePath(
source.getPath()) }, refreshPaths(expandedPaths));
groupSelector.concludeMoveGroup(undo, source);
} else if (transferable
.isDataFlavorSupported(TransferableEntrySelection.flavorInternal)) {
final AbstractGroup group = target.getGroup();
if (!group.supportsAdd()) {
// this should never happen, because the same condition
// is checked in dragOver already
dtde.rejectDrop();
return;
}
final TransferableEntrySelection selection = (TransferableEntrySelection) transferable
.getTransferData(TransferableEntrySelection.flavorInternal);
final BibtexEntry[] entries = selection.getSelection();
int assignedEntries = 0;
for (int i = 0; i < entries.length; ++i) {
if (!target.getGroup().contains(entries[i]))
++assignedEntries;
}
// warn if assignment has undesired side effects (modifies a
// field != keywords)
if (!Util.warnAssignmentSideEffects(
new AbstractGroup[] { group },
selection.getSelection(), groupSelector
.getActiveBasePanel().getDatabase(),
groupSelector.frame))
return; // user aborted operation
// if an editor is showing, its fields must be updated
// after the assignment, and before that, the current
// edit has to be stored:
groupSelector.getActiveBasePanel().storeCurrentEdit();
AbstractUndoableEdit undo = group.add(selection.getSelection());
if (undo instanceof UndoableChangeAssignment)
((UndoableChangeAssignment) undo).setEditedNode(target);
dtde.getDropTargetContext().dropComplete(true);
groupSelector.revalidateGroups();
groupSelector.concludeAssignment(undo, target, assignedEntries);
} else {
dtde.rejectDrop();
return;
}
} catch (IOException ioe) {
// ignore
} catch (UnsupportedFlavorException e) {
// ignore
}
}
public void dragExit(DropTargetEvent dte) {
setHighlight1Cell(null);
}
public void dragGestureRecognized(DragGestureEvent dge) {
GroupTreeNode selectedNode = getSelectedNode();
if (selectedNode == null)
return; // nothing to transfer (select manually?)
Cursor cursor = DragSource.DefaultMoveDrop;
dragNode = selectedNode;
dge.getDragSource().startDrag(dge, cursor, selectedNode, this);
}
/** Returns the first selected node, or null if nothing is selected. */
public GroupTreeNode getSelectedNode() {
TreePath selectionPath = getSelectionPath();
return selectionPath != null ? (GroupTreeNode) selectionPath
.getLastPathComponent() : null;
}
/**
* Refresh paths that may have become invalid due to node movements within
* the tree. This method creates new paths to the last path components
* (which must still exist) of the specified paths.
*
* @param paths
* Paths that may have become invalid.
* @return Refreshed paths that are all valid.
*/
public Enumeration<TreePath> refreshPaths(Enumeration<TreePath> paths) {
Vector<TreePath> freshPaths = new Vector<TreePath>();
while (paths.hasMoreElements()) {
freshPaths.add(new TreePath(
((DefaultMutableTreeNode)paths.nextElement()
.getLastPathComponent()).getPath()));
}
return freshPaths.elements();
}
/**
* Refresh paths that may have become invalid due to node movements within
* the tree. This method creates new paths to the last path components
* (which must still exist) of the specified paths.
*
* @param paths
* Paths that may have become invalid.
* @return Refreshed paths that are all valid.
*/
public TreePath[] refreshPaths(TreePath[] paths) {
TreePath[] freshPaths = new TreePath[paths.length];
for (int i = 0; i < paths.length; ++i) {
freshPaths[i] = new TreePath(((DefaultMutableTreeNode) paths[i]
.getLastPathComponent()).getPath());
}
return freshPaths;
}
/** Highlights the specified cell or disables highlight if cell == null */
public void setHighlight1Cell(Object cell) {
cellRenderer.setHighlight1Cell(cell);
repaint();
}
/** Highlights the specified cells or disables highlight if cells == null */
public void setHighlight2Cells(Object[] cells) {
cellRenderer.setHighlight2Cells(cells);
repaint();
}
/** Highlights the specified cells or disables highlight if cells == null */
public void setHighlight3Cells(Object[] cells) {
cellRenderer.setHighlight3Cells(cells);
repaint();
}
/** Highlights the specified cell or disables highlight if cell == null */
public void setHighlightBorderCell(GroupTreeNode node) {
cellRenderer.setHighlightBorderCell(node);
repaint();
}
/** Sort immediate children of the specified node alphabetically. */
public void sort(GroupTreeNode node, boolean recursive) {
sortWithoutRevalidate(node, recursive);
groupSelector.revalidateGroups();
}
/** This sorts without revalidation of groups */
protected void sortWithoutRevalidate(GroupTreeNode node, boolean recursive) {
if (node.isLeaf())
return; // nothing to sort
GroupTreeNode child1, child2;
int j = node.getChildCount() - 1;
int lastModified;
while (j > 0) {
lastModified = j + 1;
j = -1;
for (int i = 1; i < lastModified; ++i) {
child1 = (GroupTreeNode) node.getChildAt(i - 1);
child2 = (GroupTreeNode) node.getChildAt(i);
if (child2.getGroup().getName().compareToIgnoreCase(
child1.getGroup().getName()) < 0) {
node.remove(child1);
node.insert(child1, i);
j = i;
}
}
}
if (recursive) {
for (int i = 0; i < node.getChildCount(); ++i) {
sortWithoutRevalidate((GroupTreeNode) node.getChildAt(i), true);
}
}
}
/** Expand this node and all its children. */
public void expandSubtree(GroupTreeNode node) {
for (Enumeration<GroupTreeNode> e = node.depthFirstEnumeration(); e.hasMoreElements();)
expandPath(new TreePath(e.nextElement().getPath()));
}
/** Collapse this node and all its children. */
public void collapseSubtree(GroupTreeNode node) {
for (Enumeration<GroupTreeNode> e = node.depthFirstEnumeration(); e.hasMoreElements();)
collapsePath(new TreePath((e.nextElement())
.getPath()));
}
/**
* Returns true if the node specified by path has at least one descendant
* that is currently expanded.
*/
public boolean hasExpandedDescendant(TreePath path) {
GroupTreeNode node = (GroupTreeNode) path.getLastPathComponent();
for (Enumeration<GroupTreeNode> e = node.children(); e.hasMoreElements();) {
GroupTreeNode child = e.nextElement();
if (child.isLeaf())
continue; // don't care about this case
TreePath pathToChild = path.pathByAddingChild(child);
if (isExpanded(pathToChild) || hasExpandedDescendant(pathToChild))
return true;
}
return false;
}
/**
* Returns true if the node specified by path has at least one descendant
* that is currently collapsed.
*/
public boolean hasCollapsedDescendant(TreePath path) {
GroupTreeNode node = (GroupTreeNode) path.getLastPathComponent();
for (Enumeration<GroupTreeNode> e = node.children(); e.hasMoreElements();) {
GroupTreeNode child = e.nextElement();
if (child.isLeaf())
continue; // don't care about this case
TreePath pathToChild = path.pathByAddingChild(child);
if (isCollapsed(pathToChild) || hasCollapsedDescendant(pathToChild))
return true;
}
return false;
}
}