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