001/* TransferHandler.java --
002   Copyright (C) 2004, 2005, 2006, Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.swing;
040
041import java.awt.Toolkit;
042import java.awt.datatransfer.Clipboard;
043import java.awt.datatransfer.DataFlavor;
044import java.awt.datatransfer.Transferable;
045import java.awt.datatransfer.UnsupportedFlavorException;
046import java.awt.dnd.DragGestureEvent;
047import java.awt.dnd.DragGestureListener;
048import java.awt.dnd.DragGestureRecognizer;
049import java.awt.dnd.DragSource;
050import java.awt.dnd.DragSourceContext;
051import java.awt.dnd.DragSourceDragEvent;
052import java.awt.dnd.DragSourceDropEvent;
053import java.awt.dnd.DragSourceEvent;
054import java.awt.dnd.DragSourceListener;
055import java.awt.event.ActionEvent;
056import java.awt.event.InputEvent;
057import java.awt.event.MouseEvent;
058import java.beans.BeanInfo;
059import java.beans.IntrospectionException;
060import java.beans.Introspector;
061import java.beans.PropertyDescriptor;
062import java.io.IOException;
063import java.io.Serializable;
064import java.lang.reflect.Method;
065
066public class TransferHandler implements Serializable
067{
068
069  /**
070   * An implementation of {@link Transferable} that can be used to export
071   * data from a component's property.
072   */
073  private static class PropertyTransferable
074    implements Transferable
075  {
076    /**
077     * The component from which we export.
078     */
079    private JComponent component;
080
081    /**
082     * The property descriptor of the property that we handle.
083     */
084    private PropertyDescriptor property;
085
086    /**
087     * Creates a new PropertyTransferable.
088     *
089     * @param c the component from which we export
090     * @param prop the property from which we export
091     */
092    PropertyTransferable(JComponent c, PropertyDescriptor prop)
093    {
094      component = c;
095      property = prop;
096    }
097
098    /**
099     * Returns the data flavors supported by the Transferable.
100     *
101     * @return the data flavors supported by the Transferable
102     */
103    public DataFlavor[] getTransferDataFlavors()
104    {
105      DataFlavor[] flavors;
106      Class propClass = property.getPropertyType();
107      String mime = DataFlavor.javaJVMLocalObjectMimeType + "; class="
108                    + propClass.getName();
109      try
110        {
111          DataFlavor flavor = new DataFlavor(mime);
112          flavors = new DataFlavor[]{ flavor };
113        }
114      catch (ClassNotFoundException ex)
115        {
116          flavors = new DataFlavor[0];
117        }
118      return flavors;
119    }
120
121    /**
122     * Returns <code>true</code> when the specified data flavor is supported,
123     * <code>false</code> otherwise.
124     *
125     * @return <code>true</code> when the specified data flavor is supported,
126     *         <code>false</code> otherwise
127     */
128    public boolean isDataFlavorSupported(DataFlavor flavor)
129    {
130      Class propClass = property.getPropertyType();
131      return flavor.getPrimaryType().equals("application")
132        && flavor.getSubType().equals("x-java-jvm-local-objectref")
133        && propClass.isAssignableFrom(flavor.getRepresentationClass());
134    }
135
136    /**
137     * Returns the actual transfer data.
138     *
139     * @param flavor the data flavor
140     *
141     * @return the actual transfer data
142     */
143    public Object getTransferData(DataFlavor flavor)
144      throws UnsupportedFlavorException, IOException
145    {
146      if (isDataFlavorSupported(flavor))
147        {
148          Method getter = property.getReadMethod();
149          Object o;
150          try
151            {
152              o = getter.invoke(component);
153              return o;
154            }
155          catch (Exception ex)
156            {
157              throw new IOException("Property read failed: "
158                                    + property.getName());
159            }
160        }
161      else
162        throw new UnsupportedFlavorException(flavor);
163    }
164  }
165
166  static class TransferAction extends AbstractAction
167  {
168    private String command;
169
170    public TransferAction(String command)
171    {
172      super(command);
173      this.command = command;
174    }
175
176    public void actionPerformed(ActionEvent event)
177    {
178      JComponent component = (JComponent) event.getSource();
179      TransferHandler transferHandler = component.getTransferHandler();
180      Clipboard clipboard = getClipboard(component);
181
182      if (clipboard == null)
183        {
184          // Access denied!
185          Toolkit.getDefaultToolkit().beep();
186          return;
187        }
188
189      if (command.equals(COMMAND_COPY))
190        transferHandler.exportToClipboard(component, clipboard, COPY);
191      else if (command.equals(COMMAND_CUT))
192        transferHandler.exportToClipboard(component, clipboard, MOVE);
193      else if (command.equals(COMMAND_PASTE))
194        {
195          Transferable transferable = clipboard.getContents(null);
196
197          if (transferable != null)
198            transferHandler.importData(component, transferable);
199        }
200    }
201
202    /**
203     * Get the system cliboard or null if the caller isn't allowed to
204     * access the system clipboard.
205     *
206     * @param component a component, used to get the toolkit.
207     * @return the clipboard
208     */
209    private static Clipboard getClipboard(JComponent component)
210    {
211      try
212        {
213          return component.getToolkit().getSystemClipboard();
214        }
215      catch (SecurityException se)
216        {
217          return null;
218        }
219    }
220  }
221
222  private static class SwingDragGestureRecognizer extends DragGestureRecognizer
223  {
224
225    protected SwingDragGestureRecognizer(DragGestureListener dgl)
226    {
227      super(DragSource.getDefaultDragSource(), null, NONE, dgl);
228    }
229
230    void gesture(JComponent c, MouseEvent e, int src, int drag)
231    {
232      setComponent(c);
233      setSourceActions(src);
234      appendEvent(e);
235      fireDragGestureRecognized(drag, e.getPoint());
236    }
237
238    protected void registerListeners()
239    {
240      // Nothing to do here.
241    }
242
243    protected void unregisterListeners()
244    {
245      // Nothing to do here.
246    }
247
248  }
249
250  private static class SwingDragHandler
251    implements DragGestureListener, DragSourceListener
252  {
253
254    private boolean autoscrolls;
255
256    public void dragGestureRecognized(DragGestureEvent e)
257    {
258      JComponent c = (JComponent) e.getComponent();
259      TransferHandler th = c.getTransferHandler();
260      Transferable t = th.createTransferable(c);
261      if (t != null)
262        {
263          autoscrolls = c.getAutoscrolls();
264          c.setAutoscrolls(false);
265          try
266            {
267              e.startDrag(null, t, this);
268              return;
269            }
270          finally
271            {
272              c.setAutoscrolls(autoscrolls);
273            }
274        }
275      th.exportDone(c, t, NONE);
276    }
277
278    public void dragDropEnd(DragSourceDropEvent e)
279    {
280      DragSourceContext ctx = e.getDragSourceContext();
281      JComponent c = (JComponent) ctx.getComponent();
282      TransferHandler th = c.getTransferHandler();
283      if (e.getDropSuccess())
284        {
285          th.exportDone(c, ctx.getTransferable(), e.getDropAction());
286        }
287      else
288        {
289          th.exportDone(c, ctx.getTransferable(), e.getDropAction());
290        }
291      c.setAutoscrolls(autoscrolls);
292    }
293
294    public void dragEnter(DragSourceDragEvent e)
295    {
296      // Nothing to do here.
297    }
298
299    public void dragExit(DragSourceEvent e)
300    {
301      // Nothing to do here.
302    }
303
304    public void dragOver(DragSourceDragEvent e)
305    {
306      // Nothing to do here.
307    }
308
309    public void dropActionChanged(DragSourceDragEvent e)
310    {
311      // Nothing to do here.
312    }
313
314  }
315
316  private static final long serialVersionUID = -967749805571669910L;
317
318  private static final String COMMAND_COPY = "copy";
319  private static final String COMMAND_CUT = "cut";
320  private static final String COMMAND_PASTE = "paste";
321
322  public static final int NONE = 0;
323  public static final int COPY = 1;
324  public static final int MOVE = 2;
325  public static final int COPY_OR_MOVE = 3;
326
327  private static Action copyAction = new TransferAction(COMMAND_COPY);
328  private static Action cutAction = new TransferAction(COMMAND_CUT);
329  private static Action pasteAction = new TransferAction(COMMAND_PASTE);
330
331  private int sourceActions;
332  private Icon visualRepresentation;
333
334  /**
335   * The name of the property into/from which this TransferHandler
336   * imports/exports.
337   */
338  private String propertyName;
339
340  /**
341   * The DragGestureRecognizer for Swing.
342   */
343  private SwingDragGestureRecognizer recognizer;
344
345  public static Action getCopyAction()
346  {
347    return copyAction;
348  }
349
350  public static Action getCutAction()
351  {
352    return cutAction;
353  }
354
355  public static Action getPasteAction()
356  {
357    return pasteAction;
358  }
359
360  protected TransferHandler()
361  {
362    this.sourceActions = NONE;
363  }
364
365  public TransferHandler(String property)
366  {
367    propertyName = property;
368    this.sourceActions = property != null ? COPY : NONE;
369  }
370
371  /**
372   * Returns <code>true</code> if the data in this TransferHandler can be
373   * imported into the specified component. This will be the case when:
374   * <ul>
375   *   <li>The component has a readable and writable property with the property
376   *   name specified in the TransferHandler constructor.</li>
377   *   <li>There is a dataflavor with a mime type of
378   *     <code>application/x-java-jvm-local-object-ref</code>.</li>
379   *   <li>The dataflavor's representation class matches the class of the
380   *    property in the component.</li>
381   * </li>
382   *
383   * @param c the component to check
384   * @param flavors the possible data flavors
385   *
386   * @return <code>true</code> if the data in this TransferHandler can be
387   *         imported into the specified component, <code>false</code>
388   *         otherwise
389   */
390  public boolean canImport(JComponent c, DataFlavor[] flavors)
391  {
392    PropertyDescriptor propDesc = getPropertyDescriptor(c);
393    boolean canImport = false;
394    if (propDesc != null)
395      {
396        // Check if the property is writable. The readable check is already
397        // done in getPropertyDescriptor().
398        Method writer = propDesc.getWriteMethod();
399        if (writer != null)
400          {
401            Class[] params = writer.getParameterTypes();
402            if (params.length == 1)
403              {
404                // Number of parameters ok, now check mime type and
405                // representation class.
406                DataFlavor flavor = getPropertyDataFlavor(params[0], flavors);
407                if (flavor != null)
408                  canImport = true;
409              }
410          }
411      }
412    return canImport;
413  }
414
415  /**
416   * Creates a {@link Transferable} that can be used to export data
417   * from the specified component.
418   *
419   * This method returns <code>null</code> when the specified component
420   * doesn't have a readable property that matches the property name
421   * specified in the <code>TransferHandler</code> constructor.
422   *
423   * @param c the component to create a transferable for
424   *
425   * @return a {@link Transferable} that can be used to export data
426   *         from the specified component, or null if the component doesn't
427   *         have a readable property like the transfer handler
428   */
429  protected Transferable createTransferable(JComponent c)
430  {
431    Transferable transferable = null;
432    if (propertyName != null)
433      {
434        PropertyDescriptor prop = getPropertyDescriptor(c);
435        if (prop != null)
436          transferable = new PropertyTransferable(c, prop);
437      }
438    return transferable;
439  }
440
441  public void exportAsDrag(JComponent c, InputEvent e, int action)
442  {
443    int src = getSourceActions(c);
444    int drag = src & action;
445    if (! (e instanceof MouseEvent))
446      {
447        drag = NONE;
448      }
449    if (drag != NONE)
450      {
451        if (recognizer == null)
452          {
453            SwingDragHandler ds = new SwingDragHandler();
454            recognizer = new SwingDragGestureRecognizer(ds);
455          }
456        recognizer.gesture(c, (MouseEvent) e, src, drag);
457      }
458    else
459      {
460        exportDone(c, null, NONE);
461      }
462  }
463
464  /**
465   * This method is invoked after data has been exported.
466   * Subclasses should implement this method to remove the data that has been
467   * transferred when the action was <code>MOVE</code>.
468   *
469   * The default implementation does nothing because MOVE is not supported.
470   *
471   * @param c the source component
472   * @param data the data that has been transferred or <code>null</code>
473   *        when the action is NONE
474   * @param action the action that has been performed
475   */
476  protected void exportDone(JComponent c, Transferable data, int action)
477  {
478    // Nothing to do in the default implementation.
479  }
480
481  /**
482   * Exports the property of the component <code>c</code> that was
483   * specified for this TransferHandler to the clipboard, performing
484   * the specified action.
485   *
486   * This will check if the action is allowed by calling
487   * {@link #getSourceActions(JComponent)}. If the action is not allowed,
488   * then no export is performed.
489   *
490   * In either case the method {@link #exportDone} will be called with
491   * the action that has been performed, or {@link #NONE} if the action
492   * was not allowed or could otherwise not be completed.
493   * Any IllegalStateException that is thrown by the Clipboard due to
494   * beeing unavailable will be propagated through this method.
495   *
496   * @param c the component from which to export
497   * @param clip the clipboard to which the data will be exported
498   * @param action the action to perform
499   *
500   * @throws IllegalStateException when the clipboard is not available
501   */
502  public void exportToClipboard(JComponent c, Clipboard clip, int action)
503    throws IllegalStateException
504  {
505    action &= getSourceActions(c);
506    Transferable transferable = createTransferable(c);
507    if (transferable != null && action != NONE)
508      {
509        try
510          {
511            clip.setContents(transferable, null);
512            exportDone(c, transferable, action);
513          }
514        catch (IllegalStateException ex)
515          {
516            exportDone(c, transferable, NONE);
517            throw ex;
518          }
519      }
520    else
521      exportDone(c, null, NONE);
522  }
523
524  public int getSourceActions(JComponent c)
525  {
526    return sourceActions;
527  }
528
529  public Icon getVisualRepresentation(Transferable t)
530  {
531    return visualRepresentation;
532  }
533
534  /**
535   * Imports the transfer data represented by <code>t</code> into the specified
536   * component <code>c</code> by setting the property of this TransferHandler
537   * on that component. If this succeeds, this method returns
538   * <code>true</code>, otherwise <code>false</code>.
539   *
540   *
541   * @param c the component to import into
542   * @param t the transfer data to import
543   *
544   * @return <code>true</code> if the transfer succeeds, <code>false</code>
545   *         otherwise
546   */
547  public boolean importData(JComponent c, Transferable t)
548  {
549    boolean ok = false;
550    PropertyDescriptor prop = getPropertyDescriptor(c);
551    if (prop != null)
552      {
553        Method writer = prop.getWriteMethod();
554        if (writer != null)
555          {
556            Class[] params = writer.getParameterTypes();
557            if (params.length == 1)
558              {
559                DataFlavor flavor = getPropertyDataFlavor(params[0],
560                                                   t.getTransferDataFlavors());
561                if (flavor != null)
562                  {
563                    try
564                      {
565                        Object value = t.getTransferData(flavor);
566                        writer.invoke(c, new Object[]{ value });
567                        ok = true;
568                      }
569                    catch (Exception ex)
570                      {
571                        // If anything goes wrong here, do nothing and return
572                        // false;
573                      }
574                  }
575              }
576          }
577      }
578    return ok;
579  }
580
581  /**
582   * Returns the property descriptor for the property of this TransferHandler
583   * in the specified component, or <code>null</code> if no such property
584   * exists in the component. This method only returns properties that are
585   * at least readable (that is, it has a public no-arg getter method).
586   *
587   * @param c the component to check
588   *
589   * @return the property descriptor for the property of this TransferHandler
590   *         in the specified component, or <code>null</code> if no such
591   *         property exists in the component
592   */
593  private PropertyDescriptor getPropertyDescriptor(JComponent c)
594  {
595    PropertyDescriptor prop = null;
596    if (propertyName != null)
597      {
598        Class clazz = c.getClass();
599        BeanInfo beanInfo;
600        try
601          {
602            beanInfo = Introspector.getBeanInfo(clazz);
603          }
604        catch (IntrospectionException ex)
605          {
606            beanInfo = null;
607          }
608        if (beanInfo != null)
609          {
610            PropertyDescriptor[] props = beanInfo.getPropertyDescriptors();
611            for (int i = 0; i < props.length && prop == null; i++)
612              {
613                PropertyDescriptor desc = props[i];
614                if (desc.getName().equals(propertyName))
615                  {
616                    Method reader = desc.getReadMethod();
617                    if (reader != null)
618                      {
619                        Class[] params = reader.getParameterTypes();
620                        if (params == null || params.length == 0)
621                          prop = desc;
622                      }
623                  }
624              }
625          }
626      }
627    return prop;
628  }
629
630  /**
631   * Searches <code>flavors</code> to find a suitable data flavor that
632   * has the mime type application/x-java-jvm-local-objectref and a
633   * representation class that is the same as the specified <code>clazz</code>.
634   * When no such data flavor is found, this returns <code>null</code>.
635   *
636   * @param clazz the representation class required for the data flavor
637   * @param flavors the possible data flavors
638   *
639   * @return the suitable data flavor or null if none is found
640   */
641  private DataFlavor getPropertyDataFlavor(Class clazz, DataFlavor[] flavors)
642  {
643    DataFlavor found = null;
644    for (int i = 0; i < flavors.length && found == null; i++)
645      {
646        DataFlavor flavor = flavors[i];
647        if (flavor.getPrimaryType().equals("application")
648            && flavor.getSubType().equals("x-java-jvm-local-objectref")
649            && clazz.isAssignableFrom(flavor.getRepresentationClass()))
650          found = flavor;
651      }
652    return found;
653  }
654}