/* 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; } }