001    /* TransferHandler.java --
002       Copyright (C) 2004, 2005, 2006, Free Software Foundation, Inc.
003    
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    
039    package javax.swing;
040    
041    import java.awt.Toolkit;
042    import java.awt.datatransfer.Clipboard;
043    import java.awt.datatransfer.DataFlavor;
044    import java.awt.datatransfer.Transferable;
045    import java.awt.datatransfer.UnsupportedFlavorException;
046    import java.awt.dnd.DragGestureEvent;
047    import java.awt.dnd.DragGestureListener;
048    import java.awt.dnd.DragGestureRecognizer;
049    import java.awt.dnd.DragSource;
050    import java.awt.dnd.DragSourceContext;
051    import java.awt.dnd.DragSourceDragEvent;
052    import java.awt.dnd.DragSourceDropEvent;
053    import java.awt.dnd.DragSourceEvent;
054    import java.awt.dnd.DragSourceListener;
055    import java.awt.event.ActionEvent;
056    import java.awt.event.InputEvent;
057    import java.awt.event.MouseEvent;
058    import java.beans.BeanInfo;
059    import java.beans.IntrospectionException;
060    import java.beans.Introspector;
061    import java.beans.PropertyDescriptor;
062    import java.io.IOException;
063    import java.io.Serializable;
064    import java.lang.reflect.Method;
065    
066    public 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    }