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