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