001package jmri.util.iharder.dnd;
002
003
004import java.awt.Color;
005import java.awt.Component;
006import java.awt.Container;
007import java.awt.datatransfer.DataFlavor;
008import java.awt.datatransfer.Transferable;
009import java.awt.datatransfer.UnsupportedFlavorException;
010import java.awt.dnd.DnDConstants;
011import java.awt.dnd.DropTarget;
012import java.awt.dnd.DropTargetDragEvent;
013import java.awt.dnd.DropTargetDropEvent;
014import java.awt.dnd.DropTargetEvent;
015import java.awt.dnd.DropTargetListener;
016import java.awt.event.HierarchyEvent;
017import java.io.BufferedReader;
018import java.io.File;
019import java.io.IOException;
020import java.io.Reader;
021import java.util.List;
022import javax.swing.BorderFactory;
023import javax.swing.JComponent;
024import javax.swing.border.Border;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028
029/**
030 * This class makes it easy to drag and drop files from the operating system to
031 * a Java program. Any {@code Component} can be dropped onto, but only
032 * {@code JComponent}s will indicate the drop event with a changed border.
033 * <p>
034 * To use this class, construct a new {@code URIDrop} by passing it the target
035 * component and a {@code Listener} to receive notification when file(s) have
036 * been dropped. Here is an example:
037 * <p>
038 * <code>
039 *      JPanel myPanel = new JPanel();
040 *      new URIDrop( myPanel, new URIDrop.Listener()
041 *      {   public void filesDropped( java.io.File[] files )
042 *          {
043 *              // handle file drop
044 *              ...
045 *          }
046 *      });
047 * </code>
048 * <p>
049 * You can specify the border that will appear when files are being dragged by
050 * calling the constructor with a {@code border.Border}. Only
051 * {@code JComponent}s will show any indication with a border.
052 * <p>
053 * You can turn on some debugging features by passing a {@code PrintStream}
054 * object (such as {@code System.out}) into the full constructor. A {@code null}
055 * value will result in no extra debugging information being output.
056 *
057 * @author Robert Harder rharder@users.sf.net
058 * @author Nathan Blomquist
059 * @version 1.0.1
060 */
061public class URIDrop {
062
063    private transient Border normalBorder;
064    private transient DropTargetListener dropListener;
065
066    // Default border color
067    private static Color defaultBorderColor = new Color(0f, 0f, 1f, 0.25f);
068
069    /**
070     * Constructs a {@link URIDrop} with a default light-blue border and, if
071     * <var>c</var> is a {@link Container}, recursively sets all elements
072     * contained within as drop targets, though only the top level container
073     * will change borders.
074     *
075     * @param c        Component on which files will be dropped.
076     * @param listener Listens for {@code filesDropped}.
077     * @since 1.0
078     */
079    public URIDrop(
080            final Component c,
081            final Listener listener) {
082        this(null, // Logging stream
083                c, // Drop target
084                BorderFactory.createMatteBorder(2, 2, 2, 2, defaultBorderColor), // Drag border
085                true, // Recursive
086                listener);
087    }
088
089    /**
090     * Constructor with a default border and the option to recursively set drop
091     * targets. If your component is a {@code Container}, then each of its
092     * children components will also listen for drops, though only the parent
093     * will change borders.
094     *
095     * @param c         Component on which files will be dropped.
096     * @param recursive Recursively set children as drop targets.
097     * @param listener  Listens for {@code filesDropped}.
098     * @since 1.0
099     */
100    public URIDrop(
101            final Component c,
102            final boolean recursive,
103            final Listener listener) {
104        this(null, // Logging stream
105                c, // Drop target
106                BorderFactory.createMatteBorder(2, 2, 2, 2, defaultBorderColor), // Drag border
107                recursive, // Recursive
108                listener);
109    }
110
111    /**
112     * Constructor with a default border, debugging optionally turned on and the
113     * option to recursively set drop targets. If your component is a
114     * {@code Container}, then each of its children components will also listen
115     * for drops, though only the parent will change borders. With Debugging
116     * turned on, more status messages will be displayed to {@code out}. A
117     * common way to use this constructor is with {@code System.out} or
118     * {@code System.err}. A {@code null} value for the parameter {@code out}
119     * will result in no debugging output.
120     *
121     * @param out       PrintStream to record debugging info or null for no
122     *                  debugging.
123     * @param c         Component on which files will be dropped.
124     * @param recursive Recursively set children as drop targets.
125     * @param listener  Listens for {@code filesDropped}.
126     * @since 1.0
127     */
128    public URIDrop(
129            final java.io.PrintStream out,
130            final Component c,
131            final boolean recursive,
132            final Listener listener) {
133        this(out, // Logging stream
134                c, // Drop target
135                BorderFactory.createMatteBorder(2, 2, 2, 2, defaultBorderColor), // Drag border
136                recursive, // Recursive
137                listener);
138    }
139
140    /**
141     * Constructor with a specified border
142     *
143     * @param c          Component on which files will be dropped.
144     * @param dragBorder Border to use on {@code JComponent} when dragging
145     *                   occurs.
146     * @param listener   Listens for {@code filesDropped}.
147     * @since 1.0
148     */
149    public URIDrop(
150            final Component c,
151            final Border dragBorder,
152            final Listener listener) {
153        this(
154                null, // Logging stream
155                c, // Drop target
156                dragBorder, // Drag border
157                false, // Recursive
158                listener);
159    }
160
161    /**
162     * Constructor with a specified border and the option to recursively set
163     * drop targets. If your component is a {@code Container}, then each of its
164     * children components will also listen for drops, though only the parent
165     * will change borders.
166     *
167     * @param c          Component on which files will be dropped.
168     * @param dragBorder Border to use on {@code JComponent} when dragging
169     *                   occurs.
170     * @param recursive  Recursively set children as drop targets.
171     * @param listener   Listens for {@code filesDropped}.
172     * @since 1.0
173     */
174    public URIDrop(
175            final Component c,
176            final Border dragBorder,
177            final boolean recursive,
178            final Listener listener) {
179        this(
180                null,
181                c,
182                dragBorder,
183                recursive,
184                listener);
185    }
186
187    /**
188     * Constructor with a specified border and debugging optionally turned on.
189     * With Debugging turned on, more status messages will be displayed to
190     * {@code out}. A common way to use this constructor is with
191     * {@code System.out} or {@code System.err}. A {@code null} value for the
192     * parameter {@code out} will result in no debugging output.
193     *
194     * @param out        PrintStream to record debugging info or null for no
195     *                   debugging.
196     * @param c          Component on which files will be dropped.
197     * @param dragBorder Border to use on {@code JComponent} when dragging
198     *                   occurs.
199     * @param listener   Listens for {@code filesDropped}.
200     * @since 1.0
201     */
202    public URIDrop(
203            final java.io.PrintStream out,
204            final Component c,
205            final Border dragBorder,
206            final Listener listener) {
207        this(
208                out, // Logging stream
209                c, // Drop target
210                dragBorder, // Drag border
211                false, // Recursive
212                listener);
213    }
214
215    /**
216     * Full constructor with a specified border and debugging optionally turned
217     * on. With Debugging turned on, more status messages will be displayed to
218     * {@code out}. A common way to use this constructor is with
219     * {@code System.out} or {@code System.err}. A {@code null} value for the
220     * parameter {@code out} will result in no debugging output.
221     *
222     * @param out        PrintStream to record debugging info or null for no
223     *                   debugging.
224     * @param c          Component on which files will be dropped.
225     * @param dragBorder Border to use on {@code JComponent} when dragging
226     *                   occurs.
227     * @param recursive  Recursively set children as drop targets.
228     * @param listener   Listens for {@code filesDropped}.
229     * @since 1.0
230     */
231    public URIDrop(
232            final java.io.PrintStream out,
233            final Component c,
234            final Border dragBorder,
235            final boolean recursive,
236            final Listener listener) {
237
238        dropListener = new DropTargetListener() {
239            @Override
240            public void dragEnter(DropTargetDragEvent evt) {
241                log.debug("URIDrop: dragEnter event.");
242
243                // Is this an acceptable drag event?
244                if (isDragOk(evt)) {
245                    // If it's a Swing component, set its border
246                    if (c instanceof JComponent) {
247                        JComponent jc = (JComponent) c;
248                        normalBorder = jc.getBorder();
249                        log.debug("URIDrop: normal border saved.");
250                        jc.setBorder(dragBorder);
251                        log.debug("URIDrop: drag border set.");
252                    }
253
254                    // Acknowledge that it's okay to enter
255                    //evt.acceptDrag( DnDConstants.ACTION_COPY_OR_MOVE );
256                    evt.acceptDrag(DnDConstants.ACTION_COPY);
257                    log.debug("URIDrop: event accepted.");
258                } else {   // Reject the drag event
259                    evt.rejectDrag();
260                    log.debug("URIDrop: event rejected.");
261                }
262            }
263
264            @Override
265            public void dragOver(DropTargetDragEvent evt) {   // This is called continually as long as the mouse is
266                // over the drag target.
267            }
268
269            @SuppressWarnings("unchecked")
270            @Override
271            public void drop(DropTargetDropEvent evt) {
272                log.debug("URIDrop: drop event.");
273                try {   // Get whatever was dropped
274                    Transferable tr = evt.getTransferable();
275                    boolean handled = false;
276                    // Is it a file list?
277                    if (!handled && tr.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
278                        // Say we'll take it.
279                        evt.acceptDrop(DnDConstants.ACTION_COPY);
280                        log.debug("FileDrop: file list accepted.");
281                        // Get a useful list
282                        List<File> fileList = (List<File>) tr.getTransferData(DataFlavor.javaFileListFlavor);
283                        // Alert listener to drop.
284                        if (listener != null) {
285                            listener.URIsDropped(createURIArray(fileList));
286                        }
287                        // Mark that drop is completed.
288                        evt.getDropTargetContext().dropComplete(true);
289                        handled = true;
290                        log.debug("FileDrop: drop complete as files.");
291                    }
292                    // Is it a string?
293                    if (!handled && tr.isDataFlavorSupported(DataFlavor.stringFlavor)) {
294                        // Say we'll take it.
295                        evt.acceptDrop(DnDConstants.ACTION_COPY);
296                        log.debug("URIDrop: string accepted.");
297                        // Get a useful list
298                        String uristr = (String) tr.getTransferData(DataFlavor.stringFlavor);
299                        // Alert listener to drop.
300                        if (listener != null) {
301                            listener.URIsDropped(createURIArray(uristr));
302                        }
303                        // Mark that drop is completed.
304                        evt.getDropTargetContext().dropComplete(true);
305                        handled = true;
306                        log.debug("URIDrop: drop complete as URIs.");
307                    }
308                    // this section will check for a reader flavor.
309                    if (!handled) {
310                        DataFlavor[] flavors = tr.getTransferDataFlavors();
311                        for (DataFlavor flavor : flavors) {
312                            if (flavor.isRepresentationClassReader()) {
313                                // Say we'll take it.
314                                evt.acceptDrop(DnDConstants.ACTION_COPY);
315                                log.debug("URIDrop: reader accepted.");
316                                Reader reader = flavor.getReaderForText(tr);
317                                BufferedReader br = new BufferedReader(reader);
318                                if (listener != null) {
319                                    listener.URIsDropped(createURIArray(br));
320                                }
321                                // Mark that drop is completed.
322                                evt.getDropTargetContext().dropComplete(true);
323                                log.debug("URIDrop: drop complete as {}",flavor.getHumanPresentableName());
324                                handled = true;
325                                break;
326                            }
327                        }
328                    }
329                    if (!handled) {
330                        log.debug("URIDrop: not droppable.");
331                        evt.rejectDrop();
332                    }
333                } catch (java.io.IOException io) {
334                    log.error("URIDrop: IOException - abort:", io);
335                    evt.rejectDrop();
336                } catch (UnsupportedFlavorException ufe) {
337                    log.error("URIDrop: UnsupportedFlavorException - abort:", ufe);
338                    evt.rejectDrop();
339                } finally {
340                    // If it's a Swing component, reset its border
341                    if (c instanceof JComponent) {
342                        JComponent jc = (JComponent) c;
343                        jc.setBorder(normalBorder);
344                        log.debug("URIDrop: normal border restored.");
345                    }
346                }
347            }
348
349            @Override
350            public void dragExit(DropTargetEvent evt) {
351                log.debug("URIDrop: dragExit event.");
352                // If it's a Swing component, reset its border
353                if (c instanceof JComponent) {
354                    JComponent jc = (JComponent) c;
355                    jc.setBorder(normalBorder);
356                    log.debug("URIDrop: normal border restored.");
357                }
358
359            }
360
361            @Override
362            public void dropActionChanged(DropTargetDragEvent evt) {
363                log.debug("URIDrop: dropActionChanged event.");
364                // Is this an acceptable drag event?
365                if (isDragOk(evt)) {   //evt.acceptDrag( DnDConstants.ACTION_COPY_OR_MOVE );
366                    evt.acceptDrag(DnDConstants.ACTION_COPY);
367                    log.debug("URIDrop: event accepted.");
368                } else {
369                    evt.rejectDrag();
370                    log.debug("URIDrop: event rejected.");
371                }
372            }
373        };
374
375        // Make the component (and possibly children) drop targets
376        makeDropTarget(c, recursive);
377    }
378
379    private static String ZERO_CHAR_STRING = "" + (char) 0;
380
381    private static java.net.URI[] createURIArray(BufferedReader bReader) {
382        try {
383            java.util.List<java.net.URI> list = new java.util.ArrayList<>();
384            java.lang.String line;
385            while ((line = bReader.readLine()) != null) {
386                try {
387                    // kde seems to append a 0 char to the end of the reader
388                    if (ZERO_CHAR_STRING.equals(line)) {
389                        continue;
390                    }
391
392                    java.net.URI uri = new java.net.URI(line);
393                    list.add(uri);
394                } catch (java.net.URISyntaxException ex) {
395                    log.error("URIDrop: URISyntaxException");
396                    log.debug("URIDrop: line for URI : {}",line);
397                }
398            }
399
400            return list.toArray(new java.net.URI[list.size()]);
401        } catch (IOException ex) {
402            log.debug("URIDrop: IOException");
403        }
404        return new java.net.URI[0];
405    }
406
407    private static java.net.URI[] createURIArray(String str) {
408        java.util.List<java.net.URI> list = new java.util.ArrayList<>();
409        String lines[] = str.split("(\\r|\\n)");
410        for (String line : lines) {
411            // kde seems to append a 0 char to the end of the reader
412            if (ZERO_CHAR_STRING.equals(line)) {
413                continue;
414            }
415            try {
416                java.net.URI uri = new java.net.URI(line);
417                list.add(uri);
418            }catch (java.net.URISyntaxException ex) {
419                log.error("URIDrop: URISyntaxException");
420            }
421        }
422        return list.toArray(new java.net.URI[list.size()]);
423    }
424
425    private static java.net.URI[] createURIArray(List<File> fileList) {
426        java.util.List<java.net.URI> list = new java.util.ArrayList<>();
427        fileList.forEach((f) -> {
428            list.add(f.toURI());
429        });
430        return list.toArray(new java.net.URI[list.size()]);
431    }
432
433    private void makeDropTarget(final Component c, boolean recursive) {
434        // Make drop target
435        final DropTarget dt = new DropTarget();
436        try {
437            dt.addDropTargetListener(dropListener);
438        } catch (java.util.TooManyListenersException e) {
439            log.error("URIDrop: Drop will not work due to previous error. Do you have another listener attached?", e);
440        }
441
442        // Listen for hierarchy changes and remove the drop target when the parent gets cleared out.
443        c.addHierarchyListener((HierarchyEvent evt) -> {
444            log.debug("URIDrop: Hierarchy changed.");
445            Component parent = c.getParent();
446            if (parent == null) {
447                c.setDropTarget(null);
448                log.debug("URIDrop: Drop target cleared from component.");
449            } else {
450                new DropTarget(c, dropListener);
451                log.debug("URIDrop: Drop target added to component.");
452            }
453        });
454        if (c.getParent() != null) {
455            new DropTarget(c, dropListener);
456        }
457
458        if (recursive && (c instanceof Container)) {
459            // Get the container
460            Container cont = (Container) c;
461
462            // Get its components
463            Component[] comps = cont.getComponents();
464
465            // Set its components as listeners also
466            for (Component comp : comps) {
467                makeDropTarget(comp, recursive);
468            }
469        }
470    }
471
472    /**
473     * Determine if the dragged data is a file list.
474     */
475    private boolean isDragOk(final DropTargetDragEvent evt) {
476        boolean ok = false;
477
478        // Get data flavors being dragged
479        DataFlavor[] flavors = evt.getCurrentDataFlavors();
480
481        // See if any of the flavors are a file list
482        int i = 0;
483        while (!ok && i < flavors.length) {
484            // Is the flavor a file list?
485            final DataFlavor curFlavor = flavors[i];
486            if (curFlavor.equals(DataFlavor.javaFileListFlavor)
487                    || curFlavor.isRepresentationClassReader()) {
488                ok = true;
489            }
490            i++;
491        }
492
493        // If logging is enabled, show data flavors
494        if (log.isDebugEnabled()) {
495            if (flavors.length == 0) {
496                log.debug("URIDrop: no data flavors.");
497            }
498            for (i = 0; i < flavors.length; i++) {
499                log.debug("flavor {} {}", i, flavors[i].toString());
500            }
501        }
502
503        return ok;
504    }
505
506    /**
507     * Removes the drag-and-drop hooks from the component and optionally from
508     * the all children. You should call this if you add and remove components
509     * after you've set up the drag-and-drop. This will recursively unregister
510     * all components contained within
511     * <var>c</var> if <var>c</var> is a {@link Container}.
512     *
513     * @param c The component to unregister as a drop target
514     * @return true if any components were unregistered
515     * @since 1.0
516     */
517    public static boolean remove(Component c) {
518        return remove(c, true);
519    }
520
521    /**
522     * Removes the drag-and-drop hooks from the component and optionally from
523     * the all children. You should call this if you add and remove components
524     * after you've set up the drag-and-drop.
525     *
526     * @param c         The component to unregister
527     * @param recursive Recursively unregister components within a container
528     * @return true if any components were unregistered
529     * @since 1.0
530     */
531    public static boolean remove(Component c, boolean recursive) {
532        log.debug("URIDrop: Removing drag-and-drop hooks.");
533        c.setDropTarget(null);
534        if (recursive && (c instanceof Container)) {
535            Component[] comps = ((Container) c).getComponents();
536            for (Component comp : comps) {
537                remove(comp, recursive);
538            }
539            return true;
540        } else {
541            return false;
542        }
543    }
544
545    /* ********  I N N E R   I N T E R F A C E   L I S T E N E R  ******** */
546    /**
547     * Implement this inner interface to listen for when uris are dropped. For
548     * example your class declaration may begin like this:
549     * <pre><code>
550     *      public class MyClass implements URIsDrop.Listener
551     *      ...
552     *      public void URIsDropped( java.io.URI[] files )
553     *      {
554     *          ...
555     *      }
556     *      ...
557     * </code></pre>
558     *
559     * @since 1.0
560     */
561    public interface Listener {
562
563        /**
564         * This method is called when uris have been successfully dropped.
565         *
566         * @param uris An array of {@code URI}s that were dropped.
567         * @since 1.0
568         */
569        public abstract void URIsDropped(java.net.URI[] uris);
570    }
571
572    private final static Logger log = LoggerFactory.getLogger(URIDrop.class);
573}