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