source: josm/trunk/src/org/openstreetmap/josm/gui/FileDrop.java@ 8510

Last change on this file since 8510 was 8510, checked in by Don-vip, 9 years ago

checkstyle: enable relevant whitespace checks and fix them

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