/* 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.AWTEvent; import java.awt.Component; import java.awt.Container; import java.awt.EventQueue; import java.awt.Frame; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; import java.awt.dnd.Autoscroll; 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.DragSourceMotionListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EmptyStackException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.JTabbedPane; import javax.swing.JTable; import javax.swing.JTree; import javax.swing.RootPaneContainer; import javax.swing.Scrollable; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.text.JTextComponent; import javax.swing.tree.TreePath; /** Provides auto-scrolling behavior on scrollable components which do not * implement {@link Autoscroll}, and various other aids to positioning * drop targets during a drag. * <ul> * <li>Switch tabs in a {@link JTabbedPane} * <li>Expand nodes in a {@link JTree} * <li>Autoscroll components that wouldn't be otherwise * </ul> * Ideally, there would be a <i>DropTargetNavigable</i> interface similar * to {@link Navigator} here, and components would implement it as needed. * For this implementation, custom classes requiring drop target navigation * should register a handler for themselves in the static initializer for * that class. */ public class DropTargetNavigator implements DragSourceListener, DragSourceMotionListener { private static final int DEFAULT_NAVIGATION_DELAY = 500; /** This performs a more general navigation function than Autoscroll. */ public interface Navigator { /** Return whether the given component should be subject to drop * target navigation, given the current set of items under the cursor. */ boolean isNavigating(Component c, Collection underCursor); /** Navigate the given drop target as appropriate, returning * a {@link Runnable} capable of restoring the component to its original * state when the component becomes no longer active either by moving * the cursor out or canceling the drag operation. */ Runnable navigate(Component c, Point where, Runnable previousUndo); } private static DropTargetNavigator INSTANCE; private static int navigationDelay = DEFAULT_NAVIGATION_DELAY; private static Map navigators = new WeakHashMap(); static { try { navigationDelay = Integer.getInteger("DropTargetNavigator.delay", DEFAULT_NAVIGATION_DELAY).intValue(); } catch(SecurityException e) { } register(JTabbedPane.class, new JTabbedPaneNavigator()); register(JTree.class, new JTreeNavigator()); } /** Change the delay (in ms) in starting an automatic navigation. */ public static synchronized void setNavigationDelay(int ms) { navigationDelay = ms; } /** Returns the current setting of the auto-navigation delay. */ public static synchronized int getNavigationDelay() { return navigationDelay; } /** Register a new navigator for the given component class. */ public static synchronized void register(Class cls, Navigator navigator) { navigators.put(cls, navigator); } /** Enable system-wide drop target navigation. */ public static synchronized void enableDropTargetNavigation() { if (INSTANCE == null) { INSTANCE = new DropTargetNavigator(DragSource.getDefaultDragSource()); } } /** Disable system-wide drop target navigation. */ public static synchronized void disableDropTargetNavigation() { if (INSTANCE != null) { INSTANCE.dispose(); } } private DragSource dragSource; private Map restoreMap = new HashMap(); private Map timers = new WeakHashMap(); private DropTrackingQueue queue; public DropTargetNavigator() { this(DragSource.getDefaultDragSource()); } public DropTargetNavigator(DragSource src) { this.dragSource = src; //System.err.println("Install listeners"); src.addDragSourceListener(this); src.addDragSourceMotionListener(this); //System.err.println("Install queue"); try { queue = new DropTrackingQueue(); } catch(Exception e) { // won't be able to track } //System.err.println("queue installed"); } private List getWindows(Window root) { List list = new ArrayList(); if (root == null) { Frame[] frames = Frame.getFrames(); for (int i=0;i < frames.length;i++) { list.addAll(getWindows(frames[i])); } } else { list.add(root); Window[] subs = root.getOwnedWindows(); for (int i=0;i < subs.length;i++) { list.addAll(getWindows(subs[i])); } } return list; } private Map getComponents(Point screen) { List windows = getWindows(null); Map comps = new HashMap(); for (Iterator i=windows.iterator();i.hasNext();) { Window w = (Window)i.next(); Point loc = new Point(screen.x-w.getX(), screen.y-w.getY()); Component c = findComponentAt(w, loc.x, loc.y); if (c != null) comps.put(c, SwingUtilities.convertPoint(w, loc, c)); } return comps; } private JTabbedPane getTabbedPaneForTab(Component c) { JTabbedPane p = (JTabbedPane) SwingUtilities.getAncestorOfClass(JTabbedPane.class, c); if (p != null) { int count = p.getTabCount(); for (int i=0;i < count;i++) { if (p.getComponentAt(i) == c) { break; } } } return p; } /** Find the most likely drop target component at the given coordinate. */ private Component findComponentAt(Window w, int wx, int wy) { // Prefer content pane contents, then expand to other contents; // this prevents stuff in other layers (glass pane, menus, drag images) // from interfering with the drop targeting. Container root = w; RootPaneContainer rpc = findRootPaneContainer(w); if (rpc != null) { root = rpc.getContentPane(); } Point where = SwingUtilities.convertPoint(w, wx, wy, root); Component target = SwingUtilities.getDeepestComponentAt(root, where.x, where.y); if (target == null) target = SwingUtilities.getDeepestComponentAt(w, wx, wy); // Special case JTabbedPane tabs JTabbedPane p = getTabbedPaneForTab(target); if (p != null) { target = p; } return target; } private void update(Point screen) { if (screen != null) { Map comps = getComponents(screen); restore(comps.keySet()); for (Iterator i=comps.keySet().iterator();i.hasNext();) { Component c = (Component)i.next(); update(c, (Point)comps.get(c)); } } } private boolean needsAutoscroll(Component c) { if (c instanceof JTree || c instanceof JTable || c instanceof JList || c instanceof JTextComponent) { return false; } return !(c instanceof Autoscroll); } private void update(Component c, Point where) { if (needsAutoscroll(c) && c instanceof JComponent && c instanceof Scrollable) { JComponent jc = (JComponent)c; Rectangle visible = jc.getVisibleRect(); Autoscroller scroller = new Autoscroller(jc); scroller.autoscroll(where); Rectangle visible2 = jc.getVisibleRect(); if (!visible.equals(visible2)) { return; } } Navigator navigator = getNavigator(c); if (navigator != null) { NavigationTimer timer = (NavigationTimer)timers.get(c); if (timer != null) { if (!timer.update(where)) { timer = null; } } else { timer = new NavigationTimer(c, where); timers.put(c, timer); timer.start(); } } } public static abstract class AbstractNavigator implements Navigator { public boolean isNavigating(Component comp, Collection active) { return active.contains(comp); } } private static class JTabbedPaneNavigator extends AbstractNavigator { /** A tabbed pane should stay "active" if we're over one of its panes. */ public boolean isNavigating(Component comp, Collection active) { for (Iterator i=active.iterator();i.hasNext();) { Component c = (Component)i.next(); if (SwingUtilities.isDescendingFrom(c, comp)) { return true; } } return false; } public Runnable navigate(Component c, Point where, Runnable prev) { final JTabbedPane tab = (JTabbedPane)c; final int current = tab.getSelectedIndex(); int idx = tab.indexAtLocation(where.x, where.y); Runnable undo = prev; if (idx != -1 && current != idx) { if (undo == null) { undo = new Runnable() { public void run() { tab.setSelectedIndex(current); } }; } tab.setSelectedIndex(idx); } return undo; } } private static class JTreeNavigator extends AbstractNavigator { public Runnable navigate(Component c, Point where, final Runnable prev) { final JTree tree = (JTree)c; Runnable undo = prev; final TreePath path = tree.getPathForLocation(where.x, where.y); if (path != null) { if (!tree.isExpanded(path)) { undo = new Runnable() { public void run() { tree.collapsePath(path); if (prev != null) prev.run(); } }; tree.expandPath(path); } } return undo; } } public void dragEnter(DragSourceDragEvent e) { update(e.getLocation()); } public void dragOver(DragSourceDragEvent e) { update(e.getLocation()); } public void dropActionChanged(DragSourceDragEvent e) { } public void dragExit(DragSourceEvent e) { update(e.getLocation()); } public void dragMouseMoved(DragSourceDragEvent e) { update(e.getLocation()); } public void dragDropEnd(DragSourceDropEvent e) { update(e.getLocation()); if (e.getDropSuccess()) { // Remove the actual drop target from the restore list restoreMap.clear(); } restore(Collections.EMPTY_LIST); } private Navigator getNavigator(Component c) { Class cls = c.getClass(); while (cls != null) { Navigator n = (Navigator)navigators.get(cls); if (n != null) { return n; } cls = cls.getSuperclass(); } return null; } /** Return whether the given component is still being navigated * Usually this just means the cursor is over the component, * but a JTabbedPane is still being navigated if the cursor * is over one of its tabbed panes. */ private boolean isNavigating(Component comp, Collection active) { Navigator n = getNavigator(comp); if (n != null) { return n.isNavigating(comp, active); } return active.contains(comp); } /** Restore any components that have been navigated that are no longer * navigating. */ private void restore(Collection active) { for (Iterator i=restoreMap.keySet().iterator();i.hasNext();) { Component c = (Component)i.next(); if (!isNavigating(c, active)) { Runnable action = (Runnable)restoreMap.get(c); action.run(); i.remove(); NavigationTimer timer = (NavigationTimer)timers.get(c); if (timer != null) timer.dispose(); } } } public void dispose() { dragSource.removeDragSourceListener(this); dragSource.removeDragSourceMotionListener(this); dragSource = null; if (queue != null) { queue.dispose(); queue = null; } } private class DropTrackingQueue extends EventQueue { public DropTrackingQueue() { Toolkit.getDefaultToolkit().getSystemEventQueue().push(this); } protected void dispatchEvent(AWTEvent e) { if (e instanceof MouseEvent) { // May not have security access to package sun.awt.dnd if (e.getClass().getName().indexOf("SunDropTargetEvent") != -1) { MouseEvent me = (MouseEvent)e; Point loc = me.getComponent().getLocationOnScreen(); loc.translate(me.getX(), me.getY()); update(loc); } } super.dispatchEvent(e); } public void dispose() { try { pop(); } catch(EmptyStackException e) { } } } private class NavigationTimer extends Timer implements ActionListener { private Component component; private Point origin, current; private Runnable undo; public NavigationTimer(Component c, Point where) { super(navigationDelay, null); this.component = c; this.origin = current = where; this.undo = (Runnable)restoreMap.get(c); addActionListener(this); setRepeats(false); } public boolean update(Point current) { this.current = current; int dx = Math.abs(current.x - origin.x); int dy = Math.abs(current.y - origin.y); if (dx > 5 || dy > 5) { dispose(); return false; } return true; } public void dispose() { stop(); timers.remove(component); } public void actionPerformed(ActionEvent e) { Navigator navigator = getNavigator(component); Runnable r = navigator.navigate(component, current, undo); if (r != null) { restoreMap.put(component, r); } dispose(); } } /** Find the first instance of {@link RootPaneContainer} in the given * container. Basically finds applets. */ public static RootPaneContainer findRootPaneContainer(Container c) { if (c instanceof RootPaneContainer) { return (RootPaneContainer)c; } Component[] kids = c.getComponents(); for (int i=0;i < kids.length;i++) { if (kids[i] instanceof RootPaneContainer) return (RootPaneContainer)kids[i]; if (kids[i] instanceof Container) { RootPaneContainer rcp = findRootPaneContainer((Container)kids[i]); if (rcp != null) return rcp; } } return null; } }