001/* code from: http://iharder.sourceforge.net/current/java/filedrop/
002  (public domain) with only very small additions */
003package org.openstreetmap.josm.gui;
004
005import java.awt.Color;
006import java.awt.Component;
007import java.awt.Container;
008import java.awt.datatransfer.DataFlavor;
009import java.awt.datatransfer.Transferable;
010import java.awt.datatransfer.UnsupportedFlavorException;
011import java.awt.dnd.DnDConstants;
012import java.awt.dnd.DropTarget;
013import java.awt.dnd.DropTargetDragEvent;
014import java.awt.dnd.DropTargetDropEvent;
015import java.awt.dnd.DropTargetEvent;
016import java.awt.dnd.DropTargetListener;
017import java.awt.dnd.InvalidDnDOperationException;
018import java.awt.event.HierarchyEvent;
019import java.awt.event.HierarchyListener;
020import java.io.BufferedReader;
021import java.io.File;
022import java.io.IOException;
023import java.io.Reader;
024import java.net.URI;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.List;
028import java.util.TooManyListenersException;
029
030import javax.swing.BorderFactory;
031import javax.swing.JComponent;
032import javax.swing.border.Border;
033
034import org.openstreetmap.josm.Main;
035import org.openstreetmap.josm.actions.OpenFileAction;
036import org.openstreetmap.josm.gui.FileDrop.TransferableObject;
037
038// CHECKSTYLE.OFF: HideUtilityClassConstructor
039
040/**
041 * This class makes it easy to drag and drop files from the operating
042 * system to a Java program. Any {@link java.awt.Component} can be
043 * dropped onto, but only {@link javax.swing.JComponent}s will indicate
044 * the drop event with a changed border.
045 * <p>
046 * To use this class, construct a new <tt>FileDrop</tt> by passing
047 * it the target component and a <tt>Listener</tt> to receive notification
048 * when file(s) have been dropped. Here is an example:
049 * <p>
050 * <code>
051 *      JPanel myPanel = new JPanel();
052 *      new FileDrop( myPanel, new FileDrop.Listener()
053 *      {   public void filesDropped( java.io.File[] files )
054 *          {
055 *              // handle file drop
056 *              ...
057 *          }   // end filesDropped
058 *      }); // end FileDrop.Listener
059 * </code>
060 * <p>
061 * You can specify the border that will appear when files are being dragged by
062 * calling the constructor with a {@link javax.swing.border.Border}. Only
063 * <tt>JComponent</tt>s will show any indication with a border.
064 * <p>
065 * You can turn on some debugging features by passing a <tt>PrintStream</tt>
066 * object (such as <tt>System.out</tt>) into the full constructor. A <tt>null</tt>
067 * value will result in no extra debugging information being output.
068 *
069 * <p>I'm releasing this code into the Public Domain. Enjoy.
070 * </p>
071 * <p><em>Original author: Robert Harder, rharder@usa.net</em></p>
072 * <p>2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.</p>
073 *
074 * @author  Robert Harder
075 * @author  rharder@users.sf.net
076 * @version 1.0.1
077 * @since 1231
078 */
079public class FileDrop {
080
081    // CHECKSTYLE.ON: HideUtilityClassConstructor
082
083    private Border normalBorder;
084    private DropTargetListener dropListener;
085
086    /** Discover if the running JVM is modern enough to have drag and drop. */
087    private static Boolean supportsDnD;
088
089    // Default border color
090    private static Color defaultBorderColor = new Color(0f, 0f, 1f, 0.25f);
091
092    /**
093     * Constructor for JOSM file drop
094     * @param c The drop target
095     */
096    public FileDrop(final Component c) {
097        this(
098                c,     // Drop target
099                BorderFactory.createMatteBorder(2, 2, 2, 2, defaultBorderColor), // Drag border
100                true, // Recursive
101                new FileDrop.Listener() {
102                    @Override
103                    public void filesDropped(File[] files) {
104                        // start asynchronous loading of files
105                        OpenFileAction.OpenFileTask task = new OpenFileAction.OpenFileTask(Arrays.asList(files), null);
106                        task.setRecordHistory(true);
107                        Main.worker.submit(task);
108                    }
109                }
110        );
111    }
112
113    /**
114     * Full constructor with a specified border and debugging optionally turned on.
115     * With Debugging turned on, more status messages will be displayed to
116     * <tt>out</tt>. A common way to use this constructor is with
117     * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for
118     * the parameter <tt>out</tt> will result in no debugging output.
119     *
120     * @param c Component on which files will be dropped.
121     * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs.
122     * @param recursive Recursively set children as drop targets.
123     * @param listener Listens for <tt>filesDropped</tt>.
124     */
125    public FileDrop(
126            final Component c,
127            final Border dragBorder,
128            final boolean recursive,
129            final Listener listener) {
130
131        if (supportsDnD()) {
132            // Make a drop listener
133            dropListener = new DropListener(listener, dragBorder, c);
134
135            // Make the component (and possibly children) drop targets
136            makeDropTarget(c, recursive);
137        } else {
138            Main.info("FileDrop: Drag and drop is not supported with this JVM");
139        }
140    }
141
142    private static synchronized boolean supportsDnD() {
143        if (supportsDnD == null) {
144            boolean support = false;
145            try {
146                Class.forName("java.awt.dnd.DnDConstants");
147                support = true;
148            } catch (Exception e) {
149                support = false;
150            }
151            supportsDnD = support;
152        }
153        return supportsDnD.booleanValue();
154    }
155
156    // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.
157    private static final String ZERO_CHAR_STRING = Character.toString((char) 0);
158
159    private static File[] createFileArray(BufferedReader bReader) {
160        try {
161            List<File> list = new ArrayList<>();
162            String line = null;
163            while ((line = bReader.readLine()) != null) {
164                try {
165                    // kde seems to append a 0 char to the end of the reader
166                    if (ZERO_CHAR_STRING.equals(line)) {
167                        continue;
168                    }
169
170                    File file = new File(new URI(line));
171                    list.add(file);
172                } catch (Exception ex) {
173                    Main.warn("Error with " + line + ": " + ex.getMessage());
174                }
175            }
176
177            return list.toArray(new File[list.size()]);
178        } catch (IOException ex) {
179            Main.warn("FileDrop: IOException");
180        }
181        return new File[0];
182    }
183    // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.
184
185    private void makeDropTarget(final Component c, boolean recursive) {
186        // Make drop target
187        final DropTarget dt = new DropTarget();
188        try {
189            dt.addDropTargetListener(dropListener);
190        } catch (TooManyListenersException e) {
191            Main.error(e);
192            Main.warn("FileDrop: Drop will not work due to previous error. Do you have another listener attached?");
193        }
194
195        // Listen for hierarchy changes and remove the drop target when the parent gets cleared out.
196        c.addHierarchyListener(new HierarchyListener() {
197            @Override
198            public void hierarchyChanged(HierarchyEvent evt) {
199                Main.trace("FileDrop: Hierarchy changed.");
200                Component parent = c.getParent();
201                if (parent == null) {
202                    c.setDropTarget(null);
203                    Main.trace("FileDrop: Drop target cleared from component.");
204                } else {
205                    new DropTarget(c, dropListener);
206                    Main.trace("FileDrop: Drop target added to component.");
207                }
208            }
209        });
210        if (c.getParent() != null) {
211            new DropTarget(c, dropListener);
212        }
213
214        if (recursive && (c instanceof Container)) {
215            // Get the container
216            Container cont = (Container) c;
217
218            // Get it's components
219            Component[] comps = cont.getComponents();
220
221            // Set it's components as listeners also
222            for (Component comp : comps) {
223                makeDropTarget(comp, recursive);
224            }
225        }
226    }
227
228    /**
229     * Determines if the dragged data is a file list.
230     * @param evt the drag event
231     * @return {@code true} if the dragged data is a file list
232     */
233    private static boolean isDragOk(final DropTargetDragEvent evt) {
234        boolean ok = false;
235
236        // Get data flavors being dragged
237        DataFlavor[] flavors = evt.getCurrentDataFlavors();
238
239        // See if any of the flavors are a file list
240        int i = 0;
241        while (!ok && i < flavors.length) {
242            // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.
243            // Is the flavor a file list?
244            final DataFlavor curFlavor = flavors[i];
245            if (curFlavor.equals(DataFlavor.javaFileListFlavor) ||
246                    curFlavor.isRepresentationClassReader()) {
247                ok = true;
248            }
249            // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.
250            i++;
251        }
252
253        // show data flavors
254        if (flavors.length == 0) {
255            Main.trace("FileDrop: no data flavors.");
256        }
257        for (i = 0; i < flavors.length; i++) {
258            Main.trace(flavors[i].toString());
259        }
260
261        return ok;
262    }
263
264    /**
265     * Removes the drag-and-drop hooks from the component and optionally
266     * from the all children. You should call this if you add and remove
267     * components after you've set up the drag-and-drop.
268     * This will recursively unregister all components contained within
269     * <var>c</var> if <var>c</var> is a {@link java.awt.Container}.
270     *
271     * @param c The component to unregister as a drop target
272     * @return {@code true} if at least one item has been removed, {@code false} otherwise
273     */
274    public static boolean remove(Component c) {
275        return remove(c, true);
276    }
277
278    /**
279     * Removes the drag-and-drop hooks from the component and optionally
280     * from the all children. You should call this if you add and remove
281     * components after you've set up the drag-and-drop.
282     *
283     * @param c The component to unregister
284     * @param recursive Recursively unregister components within a container
285     * @return {@code true} if at least one item has been removed, {@code false} otherwise
286     */
287    public static boolean remove(Component c, boolean recursive) {
288        // Make sure we support dnd.
289        if (supportsDnD()) {
290            Main.trace("FileDrop: Removing drag-and-drop hooks.");
291            c.setDropTarget(null);
292            if (recursive && (c instanceof Container)) {
293                for (Component comp : ((Container) c).getComponents()) {
294                    remove(comp, recursive);
295                }
296                return true;
297            } else
298                return false;
299        } else
300            return false;
301    }
302
303    /* ********  I N N E R   I N T E R F A C E   L I S T E N E R  ******** */
304
305    private final class DropListener implements DropTargetListener {
306        private final Listener listener;
307        private final Border dragBorder;
308        private final Component c;
309
310        private DropListener(Listener listener, Border dragBorder, Component c) {
311            this.listener = listener;
312            this.dragBorder = dragBorder;
313            this.c = c;
314        }
315
316        @Override
317        public void dragEnter(DropTargetDragEvent evt) {
318            Main.trace("FileDrop: dragEnter event.");
319
320            // Is this an acceptable drag event?
321            if (isDragOk(evt)) {
322                // If it's a Swing component, set its border
323                if (c instanceof JComponent) {
324                   JComponent jc = (JComponent) c;
325                    normalBorder = jc.getBorder();
326                    Main.trace("FileDrop: normal border saved.");
327                    jc.setBorder(dragBorder);
328                    Main.trace("FileDrop: drag border set.");
329                }
330
331                // Acknowledge that it's okay to enter
332                evt.acceptDrag(DnDConstants.ACTION_COPY);
333                Main.trace("FileDrop: event accepted.");
334            } else {
335                // Reject the drag event
336                evt.rejectDrag();
337                Main.trace("FileDrop: event rejected.");
338            }
339        }
340
341        @Override
342        public void dragOver(DropTargetDragEvent evt) {
343            // This is called continually as long as the mouse is over the drag target.
344        }
345
346        @Override
347        public void drop(DropTargetDropEvent evt) {
348           Main.trace("FileDrop: drop event.");
349            try {
350                // Get whatever was dropped
351                Transferable tr = evt.getTransferable();
352
353                // Is it a file list?
354                if (tr.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
355
356                    // Say we'll take it.
357                    evt.acceptDrop(DnDConstants.ACTION_COPY);
358                    Main.trace("FileDrop: file list accepted.");
359
360                    // Get a useful list
361                    List<?> fileList = (List<?>) tr.getTransferData(DataFlavor.javaFileListFlavor);
362
363                    // Convert list to array
364                    final File[] files = fileList.toArray(new File[fileList.size()]);
365
366                    // Alert listener to drop.
367                    if (listener != null) {
368                        listener.filesDropped(files);
369                    }
370
371                    // Mark that drop is completed.
372                    evt.getDropTargetContext().dropComplete(true);
373                    Main.trace("FileDrop: drop complete.");
374                } else {
375                    // this section will check for a reader flavor.
376                    // Thanks, Nathan!
377                    // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.
378                    DataFlavor[] flavors = tr.getTransferDataFlavors();
379                    boolean handled = false;
380                    for (DataFlavor flavor : flavors) {
381                        if (flavor.isRepresentationClassReader()) {
382                            // Say we'll take it.
383                            evt.acceptDrop(DnDConstants.ACTION_COPY);
384                            Main.trace("FileDrop: reader accepted.");
385
386                            Reader reader = flavor.getReaderForText(tr);
387
388                            BufferedReader br = new BufferedReader(reader);
389
390                            if (listener != null) {
391                                listener.filesDropped(createFileArray(br));
392                            }
393
394                            // Mark that drop is completed.
395                            evt.getDropTargetContext().dropComplete(true);
396                            Main.trace("FileDrop: drop complete.");
397                            handled = true;
398                            break;
399                        }
400                    }
401                    if (!handled) {
402                        Main.trace("FileDrop: not a file list or reader - abort.");
403                        evt.rejectDrop();
404                    }
405                    // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.
406                }
407            } catch (IOException | UnsupportedFlavorException e) {
408                Main.warn("FileDrop: "+e.getClass().getSimpleName()+" - abort:");
409                Main.error(e);
410                try {
411                    evt.rejectDrop();
412                } catch (InvalidDnDOperationException ex) {
413                    // Catch InvalidDnDOperationException to fix #11259
414                    Main.error(ex);
415                }
416            } finally {
417                // If it's a Swing component, reset its border
418                if (c instanceof JComponent) {
419                   JComponent jc = (JComponent) c;
420                    jc.setBorder(normalBorder);
421                    Main.debug("FileDrop: normal border restored.");
422                }
423            }
424        }
425
426        @Override
427        public void dragExit(DropTargetEvent evt) {
428            Main.debug("FileDrop: dragExit event.");
429            // If it's a Swing component, reset its border
430            if (c instanceof JComponent) {
431                JComponent jc = (JComponent) c;
432                jc.setBorder(normalBorder);
433                Main.debug("FileDrop: normal border restored.");
434            }
435        }
436
437        @Override
438        public void dropActionChanged(DropTargetDragEvent evt) {
439            Main.debug("FileDrop: dropActionChanged event.");
440            // Is this an acceptable drag event?
441            if (isDragOk(evt)) {
442                evt.acceptDrag(DnDConstants.ACTION_COPY);
443                Main.debug("FileDrop: event accepted.");
444            } else {
445                evt.rejectDrag();
446                Main.debug("FileDrop: event rejected.");
447            }
448        }
449    }
450
451    /**
452     * Implement this inner interface to listen for when files are dropped. For example
453     * your class declaration may begin like this:
454     * <code>
455     *      public class MyClass implements FileDrop.Listener
456     *      ...
457     *      public void filesDropped( java.io.File[] files )
458     *      {
459     *          ...
460     *      }   // end filesDropped
461     *      ...
462     * </code>
463     */
464    public interface Listener {
465
466        /**
467         * This method is called when files have been successfully dropped.
468         *
469         * @param files An array of <tt>File</tt>s that were dropped.
470         */
471        void filesDropped(File[] files);
472    }
473
474    /* ********  I N N E R   C L A S S  ******** */
475
476    /**
477     * At last an easy way to encapsulate your custom objects for dragging and dropping
478     * in your Java programs!
479     * When you need to create a {@link java.awt.datatransfer.Transferable} object,
480     * use this class to wrap your object.
481     * For example:
482     * <pre><code>
483     *      ...
484     *      MyCoolClass myObj = new MyCoolClass();
485     *      Transferable xfer = new TransferableObject( myObj );
486     *      ...
487     * </code></pre>
488     * Or if you need to know when the data was actually dropped, like when you're
489     * moving data out of a list, say, you can use the {@link TransferableObject.Fetcher}
490     * inner class to return your object Just in Time.
491     * For example:
492     * <pre><code>
493     *      ...
494     *      final MyCoolClass myObj = new MyCoolClass();
495     *
496     *      TransferableObject.Fetcher fetcher = new TransferableObject.Fetcher()
497     *      {   public Object getObject() { return myObj; }
498     *      }; // end fetcher
499     *
500     *      Transferable xfer = new TransferableObject( fetcher );
501     *      ...
502     * </code></pre>
503     *
504     * The {@link java.awt.datatransfer.DataFlavor} associated with
505     * {@link TransferableObject} has the representation class
506     * <tt>net.iharder.dnd.TransferableObject.class</tt> and MIME type
507     * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.
508     * This data flavor is accessible via the static
509     * {@link #DATA_FLAVOR} property.
510     *
511     *
512     * <p>I'm releasing this code into the Public Domain. Enjoy.</p>
513     *
514     * @author  Robert Harder
515     * @author  rob@iharder.net
516     * @version 1.2
517     */
518    public static class TransferableObject implements Transferable {
519
520        /**
521         * The MIME type for {@link #DATA_FLAVOR} is
522         * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.
523         */
524        public static final String MIME_TYPE = "application/x-net.iharder.dnd.TransferableObject";
525
526        /**
527         * The default {@link java.awt.datatransfer.DataFlavor} for
528         * {@link TransferableObject} has the representation class
529         * <tt>net.iharder.dnd.TransferableObject.class</tt>
530         * and the MIME type
531         * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.
532         */
533        public static final DataFlavor DATA_FLAVOR =
534            new DataFlavor(TransferableObject.class, MIME_TYPE);
535
536        private Fetcher fetcher;
537        private Object data;
538
539        private DataFlavor customFlavor;
540
541        /**
542         * Creates a new {@link TransferableObject} that wraps <var>data</var>.
543         * Along with the {@link #DATA_FLAVOR} associated with this class,
544         * this creates a custom data flavor with a representation class
545         * determined from <code>data.getClass()</code> and the MIME type
546         * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.
547         *
548         * @param data The data to transfer
549         */
550        public TransferableObject(Object data) {
551            this.data = data;
552            this.customFlavor = new DataFlavor(data.getClass(), MIME_TYPE);
553        }
554
555        /**
556         * Creates a new {@link TransferableObject} that will return the
557         * object that is returned by <var>fetcher</var>.
558         * No custom data flavor is set other than the default
559         * {@link #DATA_FLAVOR}.
560         *
561         * @param fetcher The {@link Fetcher} that will return the data object
562         * @see Fetcher
563         */
564        public TransferableObject(Fetcher fetcher) {
565            this.fetcher = fetcher;
566        }
567
568        /**
569         * Creates a new {@link TransferableObject} that will return the
570         * object that is returned by <var>fetcher</var>.
571         * Along with the {@link #DATA_FLAVOR} associated with this class,
572         * this creates a custom data flavor with a representation class <var>dataClass</var>
573         * and the MIME type
574         * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.
575         *
576         * @param dataClass The {@link java.lang.Class} to use in the custom data flavor
577         * @param fetcher The {@link Fetcher} that will return the data object
578         * @see Fetcher
579         */
580        public TransferableObject(Class<?> dataClass, Fetcher fetcher) {
581            this.fetcher = fetcher;
582            this.customFlavor = new DataFlavor(dataClass, MIME_TYPE);
583        }
584
585        /**
586         * Returns the custom {@link java.awt.datatransfer.DataFlavor} associated
587         * with the encapsulated object or <tt>null</tt> if the {@link Fetcher}
588         * constructor was used without passing a {@link java.lang.Class}.
589         *
590         * @return The custom data flavor for the encapsulated object
591         */
592        public DataFlavor getCustomDataFlavor() {
593            return customFlavor;
594        }
595
596        /* ********  T R A N S F E R A B L E   M E T H O D S  ******** */
597
598        /**
599         * Returns a two- or three-element array containing first
600         * the custom data flavor, if one was created in the constructors,
601         * second the default {@link #DATA_FLAVOR} associated with
602         * {@link TransferableObject}, and third the
603         * {@link java.awt.datatransfer.DataFlavor#stringFlavor}.
604         *
605         * @return An array of supported data flavors
606         */
607        @Override
608        public DataFlavor[] getTransferDataFlavors() {
609            if (customFlavor != null)
610                return new DataFlavor[] {
611                    customFlavor,
612                    DATA_FLAVOR,
613                    DataFlavor.stringFlavor};
614            else
615                return new DataFlavor[] {
616                    DATA_FLAVOR,
617                    DataFlavor.stringFlavor};
618        }
619
620        /**
621         * Returns the data encapsulated in this {@link TransferableObject}.
622         * If the {@link Fetcher} constructor was used, then this is when
623         * the {@link Fetcher#getObject getObject()} method will be called.
624         * If the requested data flavor is not supported, then the
625         * {@link Fetcher#getObject getObject()} method will not be called.
626         *
627         * @param flavor The data flavor for the data to return
628         * @return The dropped data
629         */
630        @Override
631        public Object getTransferData(DataFlavor flavor)
632        throws UnsupportedFlavorException, IOException {
633            // Native object
634            if (flavor.equals(DATA_FLAVOR))
635                return fetcher == null ? data : fetcher.getObject();
636
637            // String
638            if (flavor.equals(DataFlavor.stringFlavor))
639                return fetcher == null ? data.toString() : fetcher.getObject().toString();
640
641            // We can't do anything else
642            throw new UnsupportedFlavorException(flavor);
643        }
644
645        /**
646         * Returns <tt>true</tt> if <var>flavor</var> is one of the supported
647         * flavors. Flavors are supported using the <code>equals(...)</code> method.
648         *
649         * @param flavor The data flavor to check
650         * @return Whether or not the flavor is supported
651         */
652        @Override
653        public boolean isDataFlavorSupported(DataFlavor flavor) {
654            // Native object
655            if (flavor.equals(DATA_FLAVOR))
656                return true;
657
658            // String
659            if (flavor.equals(DataFlavor.stringFlavor))
660                return true;
661
662            // We can't do anything else
663            return false;
664        }
665
666        /* ********  I N N E R   I N T E R F A C E   F E T C H E R  ******** */
667
668        /**
669         * Instead of passing your data directly to the {@link TransferableObject}
670         * constructor, you may want to know exactly when your data was received
671         * in case you need to remove it from its source (or do anyting else to it).
672         * When the {@link #getTransferData getTransferData(...)} method is called
673         * on the {@link TransferableObject}, the {@link Fetcher}'s
674         * {@link #getObject getObject()} method will be called.
675         *
676         * @author Robert Harder
677         */
678        public interface Fetcher {
679            /**
680             * Return the object being encapsulated in the
681             * {@link TransferableObject}.
682             *
683             * @return The dropped object
684             */
685            Object getObject();
686        }
687    }
688}