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