001/* 002 * $Source: v:/cvsroot/open/projects/WebARTS/ca/bc/webarts/widgets/dnd/FileDrop.java,v $ 003 * $Name: $ 004 * $Revision: 1.1 $ 005 * $Date: 2005-04-10 11:53:16 -0700 (Sun, 10 Apr 2005) $ 006 * $Locker: $ 007 */ 008/* 009 * Copyright (C) 2001 WebARTS Design, North Vancouver Canada 010 * http://www..webarts.bc.ca 011 * 012 * This program is free software; you can redistribute it and/or modify 013 * it under the terms of the GNU General Public License as published by 014 * the Free Software Foundation; either version 2 of the License, or 015 * (at your option) any later version. 016 * 017 * This program is distributed in the hope that it will be useful, 018 * but WITHOUT ANY WARRANTY; without even the implied warranty of 019 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 020 * GNU General Public License for more details. 021 * 022 * You should have received a copy of the GNU General Public License 023 * along with this program; if not, write to the Free Software 024 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 025 */ 026package ca.bc.webarts.widgets.dnd; 027 028/** 029 * This class makes it easy to drag and drop files from the operating 030 * system to a Java program. Any <tt>java.awt.Component</tt> can be 031 * dropped onto, but only <tt>javax.swing.JComponent</tt>s will indicate 032 * the drop event with a changed border. 033 * <p/> 034 * To use this class, construct a new <tt>FileDrop</tt> by passing 035 * it the target component and a <tt>Listener</tt> to receive notification 036 * when file(s) have been dropped. Here is an example: 037 * <p/> 038 * <code><pre> 039 * JPanel myPanel = new JPanel(); 040 * new FileDrop( myPanel, new FileDrop.Listener() 041 * { public void filesDropped( java.io.File[] files ) 042 * { 043 * // handle file drop 044 * ... 045 * } // end filesDropped 046 * }); // end FileDrop.Listener 047 * </pre></code> 048 * <p/> 049 * You can specify the border that will appear when files are being dragged by 050 * calling the constructor with a <tt>javax.swing.border.Border</tt>. Only 051 * <tt>JComponent</tt>s will show any indication with a border. 052 * <p/> 053 * You can turn on some debugging features by passing a <tt>PrintStream</tt> 054 * object (such as <tt>System.out</tt>) into the full constructor. A <tt>null</tt> 055 * value will result in no extra debugging information being output. 056 * <p/> 057 * 058 * <p>I'm releasing this code into the Public Domain. Enjoy. 059 * </p> 060 * <p><em>Original author: Robert Harder, rharder@usa.net</em></p> 061 * 062 * @author Robert Harder 063 * @author rharder@usa.net 064 * @version 1.0 065 */ 066public class FileDrop 067{ 068 private transient javax.swing.border.Border normalBorder; 069 private transient java.awt.dnd.DropTargetListener dropListener; 070 071 072 /** Discover if the running JVM is modern enough to have drag and drop. */ 073 private static Boolean supportsDnD; 074 075 // Default border color 076 private static java.awt.Color defaultBorderColor = new java.awt.Color( 0f, 0f, 1f, 0.25f ); 077 078 /** 079 * Constructs a {@link FileDrop} with a default light-blue border 080 * and, if <var>c</var> is a {@link java.awt.Container}, recursively 081 * sets all elements contained within as drop targets, though only 082 * the top level container will change borders. 083 * 084 * @param c Component on which files will be dropped. 085 * @param listener Listens for <tt>filesDropped</tt>. 086 * @since 1.0 087 */ 088 public FileDrop( 089 final java.awt.Component c, 090 final Listener listener ) 091 { this( null, // Logging stream 092 c, // Drop target 093 javax.swing.BorderFactory.createMatteBorder( 2, 2, 2, 2, defaultBorderColor ), // Drag border 094 true, // Recursive 095 listener ); 096 } // end constructor 097 098 099 100 101 /** 102 * Constructor with a default border and the option to recursively set drop targets. 103 * If your component is a <tt>java.awt.Container</tt>, then each of its children 104 * components will also listen for drops, though only the parent will change borders. 105 * 106 * @param c Component on which files will be dropped. 107 * @param recursive Recursively set children as drop targets. 108 * @param listener Listens for <tt>filesDropped</tt>. 109 * @since 1.0 110 */ 111 public FileDrop( 112 final java.awt.Component c, 113 final boolean recursive, 114 final Listener listener ) 115 { this( null, // Logging stream 116 c, // Drop target 117 javax.swing.BorderFactory.createMatteBorder( 2, 2, 2, 2, defaultBorderColor ), // Drag border 118 recursive, // Recursive 119 listener ); 120 } // end constructor 121 122 123 /** 124 * Constructor with a default border and debugging optionally turned on. 125 * With Debugging turned on, more status messages will be displayed to 126 * <tt>out</tt>. A common way to use this constructor is with 127 * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for 128 * the parameter <tt>out</tt> will result in no debugging output. 129 * 130 * @param out PrintStream to record debugging info or null for no debugging. 131 * @param out 132 * @param c Component on which files will be dropped. 133 * @param listener Listens for <tt>filesDropped</tt>. 134 * @since 1.0 135 */ 136 public FileDrop( 137 final java.io.PrintStream out, 138 final java.awt.Component c, 139 final Listener listener ) 140 { this( out, // Logging stream 141 c, // Drop target 142 javax.swing.BorderFactory.createMatteBorder( 2, 2, 2, 2, defaultBorderColor ), 143 false, // Recursive 144 listener ); 145 } // end constructor 146 147 148 149 /** 150 * Constructor with a default border, debugging optionally turned on 151 * and the option to recursively set drop targets. 152 * If your component is a <tt>java.awt.Container</tt>, then each of its children 153 * components will also listen for drops, though only the parent will change borders. 154 * With Debugging turned on, more status messages will be displayed to 155 * <tt>out</tt>. A common way to use this constructor is with 156 * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for 157 * the parameter <tt>out</tt> will result in no debugging output. 158 * 159 * @param out PrintStream to record debugging info or null for no debugging. 160 * @param out 161 * @param c Component on which files will be dropped. 162 * @param recursive Recursively set children as drop targets. 163 * @param listener Listens for <tt>filesDropped</tt>. 164 * @since 1.0 165 */ 166 public FileDrop( 167 final java.io.PrintStream out, 168 final java.awt.Component c, 169 final boolean recursive, 170 final Listener listener) 171 { this( out, // Logging stream 172 c, // Drop target 173 javax.swing.BorderFactory.createMatteBorder( 2, 2, 2, 2, defaultBorderColor ), // Drag border 174 recursive, // Recursive 175 listener ); 176 } // end constructor 177 178 179 180 181 /** 182 * Constructor with a specified border 183 * 184 * @param c Component on which files will be dropped. 185 * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs. 186 * @param listener Listens for <tt>filesDropped</tt>. 187 * @since 1.0 188 */ 189 public FileDrop( 190 final java.awt.Component c, 191 final javax.swing.border.Border dragBorder, 192 final Listener listener) 193 { this( 194 null, // Logging stream 195 c, // Drop target 196 dragBorder, // Drag border 197 false, // Recursive 198 listener ); 199 } // end constructor 200 201 202 203 204 /** 205 * Constructor with a specified border and the option to recursively set drop targets. 206 * If your component is a <tt>java.awt.Container</tt>, then each of its children 207 * components will also listen for drops, though only the parent will change borders. 208 * 209 * @param c Component on which files will be dropped. 210 * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs. 211 * @param recursive Recursively set children as drop targets. 212 * @param listener Listens for <tt>filesDropped</tt>. 213 * @since 1.0 214 */ 215 public FileDrop( 216 final java.awt.Component c, 217 final javax.swing.border.Border dragBorder, 218 final boolean recursive, 219 final Listener listener) 220 { this( 221 null, 222 c, 223 dragBorder, 224 recursive, 225 listener ); 226 } // end constructor 227 228 229 230 /** 231 * Constructor with a specified border and debugging optionally turned on. 232 * With Debugging turned on, more status messages will be displayed to 233 * <tt>out</tt>. A common way to use this constructor is with 234 * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for 235 * the parameter <tt>out</tt> will result in no debugging output. 236 * 237 * @param out PrintStream to record debugging info or null for no debugging. 238 * @param c Component on which files will be dropped. 239 * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs. 240 * @param listener Listens for <tt>filesDropped</tt>. 241 * @since 1.0 242 */ 243 public FileDrop( 244 final java.io.PrintStream out, 245 final java.awt.Component c, 246 final javax.swing.border.Border dragBorder, 247 final Listener listener) 248 { this( 249 out, // Logging stream 250 c, // Drop target 251 dragBorder, // Drag border 252 false, // Recursive 253 listener ); 254 } // end constructor 255 256 257 258 259 260 /** 261 * Full constructor with a specified border and debugging optionally turned on. 262 * With Debugging turned on, more status messages will be displayed to 263 * <tt>out</tt>. A common way to use this constructor is with 264 * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for 265 * the parameter <tt>out</tt> will result in no debugging output. 266 * 267 * @param out PrintStream to record debugging info or null for no debugging. 268 * @param c Component on which files will be dropped. 269 * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs. 270 * @param recursive Recursively set children as drop targets. 271 * @param listener Listens for <tt>filesDropped</tt>. 272 * @since 1.0 273 */ 274 public FileDrop( 275 final java.io.PrintStream out, 276 final java.awt.Component c, 277 final javax.swing.border.Border dragBorder, 278 final boolean recursive, 279 final Listener listener) 280 { 281 282 if( supportsDnD() ) 283 { // Make a drop listener 284 dropListener = new java.awt.dnd.DropTargetListener() 285 { 286 public void dragEnter( java.awt.dnd.DropTargetDragEvent evt ) 287 { 288 log( out, "FileDrop: dragEnter event." ); 289 290 // Is this an acceptable drag event? 291 if( isDraggedFileList( out, evt ) ) 292 { 293 // If it's a Swing component, set its border 294 if( c instanceof javax.swing.JComponent ) 295 { javax.swing.JComponent jc = (javax.swing.JComponent) c; 296 normalBorder = jc.getBorder(); 297 log( out, "FileDrop: normal border saved." ); 298 jc.setBorder( dragBorder ); 299 log( out, "FileDrop: drag border set." ); 300 } // end if: JComponent 301 302 // Acknowledge that it's okay to enter 303 //evt.acceptDrag( java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE ); 304 evt.acceptDrag( java.awt.dnd.DnDConstants.ACTION_COPY ); 305 log( out, "FileDrop: event accepted." ); 306 } // end if: drag ok 307 else 308 { // Reject the drag event 309 evt.rejectDrag(); 310 log( out, "FileDrop: event rejected." ); 311 } // end else: drag not ok 312 } // end dragEnter 313 314 public void dragOver( java.awt.dnd.DropTargetDragEvent evt ) 315 { // This is called continually as long as the mouse is 316 // over the drag target. 317 } // end dragOver 318 319 public void drop( java.awt.dnd.DropTargetDropEvent evt ) 320 { log( out, "FileDrop: drop event." ); 321 try 322 { // Get whatever was dropped 323 java.awt.datatransfer.Transferable tr = evt.getTransferable(); 324 325 // Is it a file list? 326 //if (tr.isDataFlavorSupported (java.awt.datatransfer.DataFlavor.javaFileListFlavor)) 327 if( isDroppedFileList( out, evt ) ) 328 { 329 // Say we'll take it. 330 //evt.acceptDrop ( java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE ); 331 evt.acceptDrop ( java.awt.dnd.DnDConstants.ACTION_COPY ); 332 log( out, "FileDrop: file list accepted." ); 333 334 // Get a useful list 335 java.util.List fileList = (java.util.List) 336 tr.getTransferData(java.awt.datatransfer.DataFlavor.javaFileListFlavor); 337 //tr.getTransferData(java.awt.datatransfer.DataFlavor.stringFlavor ); 338 java.util.Iterator iterator = fileList.iterator(); 339 340 /* 341 String [] filenamesArray = new String [ fileList.size() ]; 342 fileList.toArray( filenamesArray ); 343 log( out, "FileDrop: dropped filenames:" ); 344 for (int iii = 0; iii < filenamesArray.length; iii++) 345 { 346 log( out, " " + filenamesArray[iii]); 347 } 348 */ 349 // Convert list to array 350 java.io.File[] filesTemp = new java.io.File[ fileList.size() ]; 351 fileList.toArray( filesTemp ); 352 final java.io.File[] files = filesTemp; 353 354 // Alert listener to drop. 355 if( listener != null ) 356 listener.filesDropped( files ); 357 358 // Mark that drop is completed. 359 evt.getDropTargetContext().dropComplete(true); 360 log( out, "FileDrop: drop complete." ); 361 } // end if: file list 362 else 363 { log( out, "FileDrop: not a file list - abort." ); 364 evt.rejectDrop(); 365 } // end else: not a file list 366 } // end try 367 catch ( java.io.IOException io) 368 { log( out, "FileDrop: IOException - abort:" ); 369 io.printStackTrace( out ); 370 evt.rejectDrop(); 371 } // end catch IOException 372 catch (java.awt.datatransfer.UnsupportedFlavorException ufe) 373 { log( out, "FileDrop: UnsupportedFlavorException - abort:" ); 374 ufe.printStackTrace( out ); 375 evt.rejectDrop(); 376 } // end catch: UnsupportedFlavorException 377 finally 378 { 379 // If it's a Swing component, reset its border 380 if( c instanceof javax.swing.JComponent ) 381 { javax.swing.JComponent jc = (javax.swing.JComponent) c; 382 jc.setBorder( normalBorder ); 383 log( out, "FileDrop: normal border restored." ); 384 } // end if: JComponent 385 } // end finally 386 } // end drop 387 388 public void dragExit( java.awt.dnd.DropTargetEvent evt ) 389 { log( out, "FileDrop: dragExit event." ); 390 // If it's a Swing component, reset its border 391 if( c instanceof javax.swing.JComponent ) 392 { javax.swing.JComponent jc = (javax.swing.JComponent) c; 393 jc.setBorder( normalBorder ); 394 log( out, "FileDrop: normal border restored." ); 395 } // end if: JComponent 396 } // end dragExit 397 398 public void dropActionChanged( java.awt.dnd.DropTargetDragEvent evt ) 399 { log( out, "FileDrop: dropActionChanged event." ); 400 // Is this an acceptable drag event? 401 if( isDraggedFileList( out, evt ) ) 402 { //evt.acceptDrag( java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE ); 403 evt.acceptDrag( java.awt.dnd.DnDConstants.ACTION_COPY ); 404 log( out, "FileDrop: event accepted." ); 405 } // end if: drag ok 406 else 407 { evt.rejectDrag(); 408 log( out, "FileDrop: event rejected." ); 409 } // end else: drag not ok 410 } // end dropActionChanged 411 }; // end DropTargetListener 412 413 // Make the component (and possibly children) drop targets 414 makeDropTarget( out, c, recursive ); 415 } // end if: supports dnd 416 else 417 { log( out, "FileDrop: Drag and drop is not supported with this JVM" ); 418 } // end else: does not support DnD 419 } // end constructor 420 421 422 private static boolean supportsDnD() 423 { // Static Boolean 424 if( supportsDnD == null ) 425 { 426 boolean support = false; 427 try 428 { Class arbitraryDndClass = Class.forName( "java.awt.dnd.DnDConstants" ); 429 support = true; 430 } // end try 431 catch( Exception e ) 432 { support = false; 433 } // end catch 434 supportsDnD = new Boolean( support ); 435 } // end if: first time through 436 return supportsDnD.booleanValue(); 437 } // end supportsDnD 438 439 440 441 442 443 private void makeDropTarget( final java.io.PrintStream out, final java.awt.Component c, boolean recursive ) 444 { 445 // Make drop target 446 final java.awt.dnd.DropTarget dt = new java.awt.dnd.DropTarget(); 447 try 448 { dt.addDropTargetListener( dropListener ); 449 } // end try 450 catch( java.util.TooManyListenersException e ) 451 { e.printStackTrace(); 452 log(out, "FileDrop: Drop will not work due to previous error. Do you have another listener attached?" ); 453 } // end catch 454 455 // Listen for hierarchy changes and remove the drop target when the parent gets cleared out. 456 c.addHierarchyListener( new java.awt.event.HierarchyListener() 457 { public void hierarchyChanged( java.awt.event.HierarchyEvent evt ) 458 { log( out, "FileDrop: Hierarchy changed." ); 459 java.awt.Component parent = c.getParent(); 460 if( parent == null ) 461 { c.setDropTarget( null ); 462 log( out, "FileDrop: Drop target cleared from component." ); 463 } // end if: null parent 464 else 465 { new java.awt.dnd.DropTarget(c, dropListener); 466 log( out, "FileDrop: Drop target added to component." ); 467 } // end else: parent not null 468 } // end hierarchyChanged 469 }); // end hierarchy listener 470 if( c.getParent() != null ) 471 new java.awt.dnd.DropTarget(c, dropListener); 472 473 if( recursive && (c instanceof java.awt.Container ) ) 474 { 475 // Get the container 476 java.awt.Container cont = (java.awt.Container) c; 477 478 // Get it's components 479 java.awt.Component[] comps = cont.getComponents(); 480 481 // Set it's components as listeners also 482 for( int i = 0; i < comps.length; i++ ) 483 makeDropTarget( out, comps[i], recursive ); 484 } // end if: recursively set components as listener 485 } // end dropListener 486 487 488 489 /** Determine if the dropped data is a file list. */ 490 private boolean isDroppedFileList( final java.io.PrintStream out, final java.awt.dnd.DropTargetDropEvent evt ) 491 { 492 boolean ok = false; 493 494 // Get data flavors being dragged 495 java.awt.datatransfer.DataFlavor[] flavors = evt.getCurrentDataFlavors(); 496 //log( out, "FileDrop: Number of flavors="+flavors.length ); 497 // See if any of the flavors are a file list 498 int i = 0; 499 while( !ok && i < flavors.length ) 500 { // Is the flavor a file list? 501 //if( flavors[i].equals( java.awt.datatransfer.DataFlavor.javaFileListFlavor ) ) 502 if( flavors[i].getMimeType().startsWith("text/uri-list")) 503 { 504 ok = true; 505 //log( out, "FileDrop: Acceptable data flavors. "+ flavors[i].getMimeType() ); 506 } 507 //log( out, "FileDrop: Found data flavors. "+ flavors[i].getMimeType() ); 508 i++; 509 } // end while: through flavors 510 511 // If logging is enabled, show data flavors 512 if( out != null ) 513 { if( flavors.length == 0 ) 514 log( out, "FileDrop: no data flavors." ); 515 for( i = 0; i < flavors.length; i++ ) 516 log( out, flavors[i].toString() ); 517 } // end if: logging enabled 518 519 return ok; 520 } // end isDragOk 521 522 523 /** Determine if the dragged data is a file list. */ 524 private boolean isDraggedFileList( final java.io.PrintStream out, final java.awt.dnd.DropTargetDragEvent evt ) 525 { 526 boolean ok = false; 527 528 // Get data flavors being dragged 529 java.awt.datatransfer.DataFlavor[] flavors = evt.getCurrentDataFlavors(); 530 //log( out, "FileDrop: Number of flavors="+flavors.length ); 531 // See if any of the flavors are a file list 532 int i = 0; 533 while( !ok && i < flavors.length ) 534 { // Is the flavor a file list? 535 //if( flavors[i].equals( java.awt.datatransfer.DataFlavor.javaFileListFlavor ) ) 536 if( flavors[i].getMimeType().startsWith("text/uri-list")) 537 { 538 ok = true; 539 //log( out, "FileDrop: Acceptable data flavors. "+ flavors[i].getMimeType() ); 540 } 541 //log( out, "FileDrop: Found data flavors. "+ flavors[i].getMimeType() ); 542 i++; 543 } // end while: through flavors 544 545 // If logging is enabled, show data flavors 546 if( out != null ) 547 { if( flavors.length == 0 ) 548 log( out, "FileDrop: no data flavors." ); 549 for( i = 0; i < flavors.length; i++ ) 550 log( out, flavors[i].toString() ); 551 } // end if: logging enabled 552 553 return ok; 554 } // end isDragOk 555 556 557 /** Outputs <tt>message</tt> to <tt>out</tt> if it's not null. */ 558 private static void log( java.io.PrintStream out, String message ) 559 { // Log message if requested 560 if( out != null ) 561 out.println( message ); 562 } // end log 563 564 565 566 567 /** 568 * Removes the drag-and-drop hooks from the component and optionally 569 * from the all children. You should call this if you add and remove 570 * components after you've set up the drag-and-drop. 571 * This will recursively unregister all components contained within 572 * <var>c</var> if <var>c</var> is a {@link java.awt.Container}. 573 * 574 * @param c The component to unregister as a drop target 575 * @since 1.0 576 */ 577 public static boolean remove( java.awt.Component c) 578 { return remove( null, c, true ); 579 } // end remove 580 581 582 583 /** 584 * Removes the drag-and-drop hooks from the component and optionally 585 * from the all children. You should call this if you add and remove 586 * components after you've set up the drag-and-drop. 587 * 588 * @param out Optional {@link java.io.PrintStream} for logging drag and drop messages 589 * @param c The component to unregister 590 * @param recursive Recursively unregister components within a container 591 * @since 1.0 592 */ 593 public static boolean remove( java.io.PrintStream out, java.awt.Component c, boolean recursive ) 594 { // Make sure we support dnd. 595 if( supportsDnD() ) 596 { log( out, "FileDrop: Removing drag-and-drop hooks." ); 597 c.setDropTarget( null ); 598 if( recursive && ( c instanceof java.awt.Container ) ) 599 { java.awt.Component[] comps = ((java.awt.Container)c).getComponents(); 600 for( int i = 0; i < comps.length; i++ ) 601 remove( out, comps[i], recursive ); 602 return true; 603 } // end if: recursive 604 else return false; 605 } // end if: supports DnD 606 else return false; 607 } // end remove 608 609 610 611 612 613 614 /** Runs a sample program that shows dropped files */ 615 public static void main( String[] args ) 616 { 617 javax.swing.JFrame frame = new javax.swing.JFrame( "FileDrop" ); 618 //javax.swing.border.TitledBorder dragBorder = new javax.swing.border.TitledBorder( "Drop 'em" ); 619 final javax.swing.JTextArea text = new javax.swing.JTextArea(); 620 frame.getContentPane().add( 621 new javax.swing.JScrollPane( text ), 622 java.awt.BorderLayout.CENTER ); 623 624 new FileDrop( System.out, text, /*dragBorder,*/ new FileDrop.Listener() 625 { public void filesDropped( java.io.File[] files ) 626 { for( int i = 0; i < files.length; i++ ) 627 { try 628 { text.append( files[i].getCanonicalPath() + "\n" ); 629 } // end try 630 catch( java.io.IOException e ) {} 631 } // end for: through each dropped file 632 } // end filesDropped 633 }); // end FileDrop.Listener 634 635 frame.setBounds( 100, 100, 300, 400 ); 636 frame.setDefaultCloseOperation( frame.EXIT_ON_CLOSE ); 637 frame.show(); 638 } // end main 639 640 641 642 643 644/* ******** I N N E R I N T E R F A C E L I S T E N E R ******** */ 645 646 647 /** 648 * Implement this inner interface to listen for when files are dropped. For example 649 * your class declaration may begin like this: 650 * <code><pre> 651 * public class MyClass implements FileDrop.Listener 652 * ... 653 * public void filesDropped( java.io.File[] files ) 654 * { 655 * ... 656 * } // end filesDropped 657 * ... 658 * </pre></code> 659 * 660 * @since 1.0 661 */ 662 public interface Listener 663 { 664 /** 665 * This method is called when files have been successfully dropped. 666 * 667 * @param files An array of <tt>File</tt>s that were dropped. 668 * @since 1.0 669 */ 670 public abstract void filesDropped( java.io.File[] files ); 671 } // end inner-interface Listener 672 673} // end class FileDrop 674