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