001package jmri.util.iharder.dnd; 002 003 004import java.awt.Color; 005import java.awt.Component; 006import java.awt.Container; 007import java.awt.datatransfer.DataFlavor; 008import java.awt.datatransfer.Transferable; 009import java.awt.datatransfer.UnsupportedFlavorException; 010import java.awt.dnd.DnDConstants; 011import java.awt.dnd.DropTarget; 012import java.awt.dnd.DropTargetDragEvent; 013import java.awt.dnd.DropTargetDropEvent; 014import java.awt.dnd.DropTargetEvent; 015import java.awt.dnd.DropTargetListener; 016import java.awt.event.HierarchyEvent; 017import java.awt.image.BufferedImage; 018import java.io.*; 019import java.util.List; 020import javax.swing.BorderFactory; 021import javax.swing.JComponent; 022import javax.swing.border.Border; 023import org.slf4j.Logger; 024import org.slf4j.LoggerFactory; 025 026 027/** 028 * This class makes it easy to drag and drop files from the operating system to 029 * a Java program. Any {@code Component} can be dropped onto, but only 030 * {@code JComponent}s will indicate the drop event with a changed border. 031 * <p> 032 * To use this class, construct a new {@code URIDrop} by passing it the target 033 * component and a {@code Listener} to receive notification when file(s) have 034 * been dropped. Here is an example: 035 * <p> 036 * <code> 037 * JPanel myPanel = new JPanel(); 038 * new URIDrop( myPanel, new URIDrop.Listener() 039 * { public void filesDropped( java.io.File[] files ) 040 * { 041 * // handle file drop 042 * ... 043 * } 044 * }); 045 * </code> 046 * <p> 047 * You can specify the border that will appear when files are being dragged by 048 * calling the constructor with a {@code border.Border}. Only 049 * {@code JComponent}s will show any indication with a border. 050 * <p> 051 * You can turn on some debugging features by passing a {@code PrintStream} 052 * object (such as {@code System.out}) into the full constructor. A {@code null} 053 * value will result in no extra debugging information being output. 054 * 055 * @author Robert Harder rharder@users.sf.net 056 * @author Nathan Blomquist 057 * @version 1.0.1 058 */ 059public class URIDrop { 060 061 private transient Border normalBorder; 062 private transient DropTargetListener dropListener; 063 064 // Default border color 065 private static Color defaultBorderColor = new Color(0f, 0f, 1f, 0.25f); 066 067 /** 068 * Constructs a {@link URIDrop} with a default light-blue border and, if 069 * <var>c</var> is a {@link Container}, recursively sets all elements 070 * contained within as drop targets, though only the top level container 071 * will change borders. 072 * 073 * @param c Component on which files will be dropped. 074 * @param listener Listens for {@code filesDropped}. 075 * @since 1.0 076 */ 077 public URIDrop( 078 final Component c, 079 final Listener listener) { 080 this(null, // Logging stream 081 c, // Drop target 082 BorderFactory.createMatteBorder(2, 2, 2, 2, defaultBorderColor), // Drag border 083 true, // Recursive 084 listener); 085 } 086 087 /** 088 * Constructor with a default border and the option to recursively set drop 089 * targets. If your component is a {@code Container}, then each of its 090 * children components will also listen for drops, though only the parent 091 * will change borders. 092 * 093 * @param c Component on which files will be dropped. 094 * @param recursive Recursively set children as drop targets. 095 * @param listener Listens for {@code filesDropped}. 096 * @since 1.0 097 */ 098 public URIDrop( 099 final Component c, 100 final boolean recursive, 101 final Listener listener) { 102 this(null, // Logging stream 103 c, // Drop target 104 BorderFactory.createMatteBorder(2, 2, 2, 2, defaultBorderColor), // Drag border 105 recursive, // Recursive 106 listener); 107 } 108 109 /** 110 * Constructor with a default border, debugging optionally turned on and the 111 * option to recursively set drop targets. If your component is a 112 * {@code Container}, then each of its children components will also listen 113 * for drops, though only the parent will change borders. With Debugging 114 * turned on, more status messages will be displayed to {@code out}. A 115 * common way to use this constructor is with {@code System.out} or 116 * {@code System.err}. A {@code null} value for the parameter {@code out} 117 * will result in no debugging output. 118 * 119 * @param out PrintStream to record debugging info or null for no 120 * debugging. 121 * @param c Component on which files will be dropped. 122 * @param recursive Recursively set children as drop targets. 123 * @param listener Listens for {@code filesDropped}. 124 * @since 1.0 125 */ 126 public URIDrop( 127 final java.io.PrintStream out, 128 final Component c, 129 final boolean recursive, 130 final Listener listener) { 131 this(out, // Logging stream 132 c, // Drop target 133 BorderFactory.createMatteBorder(2, 2, 2, 2, defaultBorderColor), // Drag border 134 recursive, // Recursive 135 listener); 136 } 137 138 /** 139 * Constructor with a specified border 140 * 141 * @param c Component on which files will be dropped. 142 * @param dragBorder Border to use on {@code JComponent} when dragging 143 * occurs. 144 * @param listener Listens for {@code filesDropped}. 145 * @since 1.0 146 */ 147 public URIDrop( 148 final Component c, 149 final Border dragBorder, 150 final Listener listener) { 151 this( 152 null, // Logging stream 153 c, // Drop target 154 dragBorder, // Drag border 155 false, // Recursive 156 listener); 157 } 158 159 /** 160 * Constructor with a specified border and the option to recursively set 161 * drop targets. If your component is a {@code Container}, then each of its 162 * children components will also listen for drops, though only the parent 163 * will change borders. 164 * 165 * @param c Component on which files will be dropped. 166 * @param dragBorder Border to use on {@code JComponent} when dragging 167 * occurs. 168 * @param recursive Recursively set children as drop targets. 169 * @param listener Listens for {@code filesDropped}. 170 * @since 1.0 171 */ 172 public URIDrop( 173 final Component c, 174 final Border dragBorder, 175 final boolean recursive, 176 final Listener listener) { 177 this( 178 null, 179 c, 180 dragBorder, 181 recursive, 182 listener); 183 } 184 185 /** 186 * Constructor with a specified border and debugging optionally turned on. 187 * With Debugging turned on, more status messages will be displayed to 188 * {@code out}. A common way to use this constructor is with 189 * {@code System.out} or {@code System.err}. A {@code null} value for the 190 * parameter {@code out} will result in no debugging output. 191 * 192 * @param out PrintStream to record debugging info or null for no 193 * debugging. 194 * @param c Component on which files will be dropped. 195 * @param dragBorder Border to use on {@code JComponent} when dragging 196 * occurs. 197 * @param listener Listens for {@code filesDropped}. 198 * @since 1.0 199 */ 200 public URIDrop( 201 final java.io.PrintStream out, 202 final Component c, 203 final Border dragBorder, 204 final Listener listener) { 205 this( 206 out, // Logging stream 207 c, // Drop target 208 dragBorder, // Drag border 209 false, // Recursive 210 listener); 211 } 212 213 /** 214 * Full constructor with a specified border and debugging optionally turned 215 * on. With Debugging turned on, more status messages will be displayed to 216 * {@code out}. A common way to use this constructor is with 217 * {@code System.out} or {@code System.err}. A {@code null} value for the 218 * parameter {@code out} will result in no debugging output. 219 * 220 * @param out PrintStream to record debugging info or null for no 221 * debugging. 222 * @param c Component on which files will be dropped. 223 * @param dragBorder Border to use on {@code JComponent} when dragging 224 * occurs. 225 * @param recursive Recursively set children as drop targets. 226 * @param listener Listens for {@code filesDropped}. 227 * @since 1.0 228 */ 229 public URIDrop( 230 final java.io.PrintStream out, 231 final Component c, 232 final Border dragBorder, 233 final boolean recursive, 234 final Listener listener) { 235 236 dropListener = new DropTargetListener() { 237 @Override 238 public void dragEnter(DropTargetDragEvent evt) { 239 log.debug("URIDrop: dragEnter event."); 240 241 // Is this an acceptable drag event? 242 if (isDragOk(evt)) { 243 // If it's a Swing component, set its border 244 if (c instanceof JComponent) { 245 JComponent jc = (JComponent) c; 246 normalBorder = jc.getBorder(); 247 log.debug("URIDrop: normal border saved."); 248 jc.setBorder(dragBorder); 249 log.debug("URIDrop: drag border set."); 250 } 251 252 // Acknowledge that it's okay to enter 253 //evt.acceptDrag( DnDConstants.ACTION_COPY_OR_MOVE ); 254 evt.acceptDrag(DnDConstants.ACTION_COPY); 255 log.debug("URIDrop: event accepted."); 256 } else { // Reject the drag event 257 evt.rejectDrag(); 258 log.debug("URIDrop: event rejected."); 259 } 260 } 261 262 @Override 263 public void dragOver(DropTargetDragEvent evt) { // This is called continually as long as the mouse is 264 // over the drag target. 265 } 266 267 @SuppressWarnings("unchecked") 268 @Override 269 public void drop(DropTargetDropEvent evt) { 270 log.debug("URIDrop: drop event."); 271 try { // Get whatever was dropped 272 Transferable tr = evt.getTransferable(); 273 boolean handled = false; 274 // Is it a raw image? 275 if (!handled && tr.isDataFlavorSupported(DataFlavor.imageFlavor) && listener != null && listener instanceof ListenerExt) { 276 // Say we'll take it. 277 evt.acceptDrop(DnDConstants.ACTION_COPY); 278 log.debug("HTMLDrop: raw image accepted."); 279 BufferedImage img = (BufferedImage) tr.getTransferData(DataFlavor.imageFlavor); 280 ((ListenerExt)listener).imageDropped(img); 281 // Mark that drop is completed. 282 evt.getDropTargetContext().dropComplete(true); 283 handled = true; 284 log.debug("ImageDrop: drop complete as image."); 285 } 286 // Is it a file path list ? 287 if (!handled && tr.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { 288 // Say we'll take it. 289 evt.acceptDrop(DnDConstants.ACTION_COPY); 290 log.debug("FileDrop: file list accepted."); 291 // Get a useful list 292 List<File> fileList = (List<File>) tr.getTransferData(DataFlavor.javaFileListFlavor); 293 // Alert listener to drop. 294 if (listener != null) { 295 listener.URIsDropped(createURIArray(fileList)); 296 } 297 // Mark that drop is completed. 298 evt.getDropTargetContext().dropComplete(true); 299 handled = true; 300 log.debug("FileDrop: drop complete as files."); 301 } 302 // Is it a string? 303 if (!handled && tr.isDataFlavorSupported(DataFlavor.stringFlavor)) { 304 // Say we'll take it. 305 evt.acceptDrop(DnDConstants.ACTION_COPY); 306 log.debug("URIDrop: string accepted."); 307 // Get a useful list 308 String uristr = (String) tr.getTransferData(DataFlavor.stringFlavor); 309 // Alert listener to drop. 310 if (listener != null) { 311 listener.URIsDropped(createURIArray(uristr)); 312 } 313 // Mark that drop is completed. 314 evt.getDropTargetContext().dropComplete(true); 315 handled = true; 316 log.debug("URIDrop: drop complete as URIs."); 317 } 318 // this section will check for a reader flavor. 319 if (!handled) { 320 DataFlavor[] flavors = tr.getTransferDataFlavors(); 321 for (DataFlavor flavor : flavors) { 322 if (flavor.isRepresentationClassReader()) { 323 // Say we'll take it. 324 evt.acceptDrop(DnDConstants.ACTION_COPY); 325 log.debug("URIDrop: reader accepted."); 326 Reader reader = flavor.getReaderForText(tr); 327 BufferedReader br = new BufferedReader(reader); 328 if (listener != null) { 329 listener.URIsDropped(createURIArray(br)); 330 } 331 // Mark that drop is completed. 332 evt.getDropTargetContext().dropComplete(true); 333 log.debug("URIDrop: drop complete as {}",flavor.getHumanPresentableName()); 334 handled = true; 335 break; 336 } 337 } 338 } 339 if (!handled) { 340 log.debug("URIDrop: not droppable."); 341 evt.rejectDrop(); 342 } 343 } catch (java.io.IOException io) { 344 log.error("URIDrop: IOException - abort:", io); 345 evt.rejectDrop(); 346 } catch (UnsupportedFlavorException ufe) { 347 log.error("URIDrop: UnsupportedFlavorException - abort:", ufe); 348 evt.rejectDrop(); 349 } finally { 350 // If it's a Swing component, reset its border 351 if (c instanceof JComponent) { 352 JComponent jc = (JComponent) c; 353 jc.setBorder(normalBorder); 354 log.debug("URIDrop: normal border restored."); 355 } 356 } 357 } 358 359 @Override 360 public void dragExit(DropTargetEvent evt) { 361 log.debug("URIDrop: dragExit event."); 362 // If it's a Swing component, reset its border 363 if (c instanceof JComponent) { 364 JComponent jc = (JComponent) c; 365 jc.setBorder(normalBorder); 366 log.debug("URIDrop: normal border restored."); 367 } 368 369 } 370 371 @Override 372 public void dropActionChanged(DropTargetDragEvent evt) { 373 log.debug("URIDrop: dropActionChanged event."); 374 // Is this an acceptable drag event? 375 if (isDragOk(evt)) { //evt.acceptDrag( DnDConstants.ACTION_COPY_OR_MOVE ); 376 evt.acceptDrag(DnDConstants.ACTION_COPY); 377 log.debug("URIDrop: event accepted."); 378 } else { 379 evt.rejectDrag(); 380 log.debug("URIDrop: event rejected."); 381 } 382 } 383 }; 384 385 // Make the component (and possibly children) drop targets 386 makeDropTarget(c, recursive); 387 } 388 389 private static String ZERO_CHAR_STRING = "" + (char) 0; 390 391 private static java.net.URI[] createURIArray(BufferedReader bReader) { 392 try { 393 java.util.List<java.net.URI> list = new java.util.ArrayList<>(); 394 java.lang.String line; 395 while ((line = bReader.readLine()) != null) { 396 try { 397 // kde seems to append a 0 char to the end of the reader 398 if (ZERO_CHAR_STRING.equals(line)) { 399 continue; 400 } 401 402 java.net.URI uri = new java.net.URI(line); 403 list.add(uri); 404 } catch (java.net.URISyntaxException ex) { 405 log.error("URIDrop: URISyntaxException"); 406 log.debug("URIDrop: line for URI : {}",line); 407 } 408 } 409 410 return list.toArray(new java.net.URI[list.size()]); 411 } catch (IOException ex) { 412 log.debug("URIDrop: IOException"); 413 } 414 return new java.net.URI[0]; 415 } 416 417 private static java.net.URI[] createURIArray(String str) { 418 java.util.List<java.net.URI> list = new java.util.ArrayList<>(); 419 String lines[] = str.split("(\\r|\\n)"); 420 for (String line : lines) { 421 // kde seems to append a 0 char to the end of the reader 422 if (ZERO_CHAR_STRING.equals(line)) { 423 continue; 424 } 425 try { 426 java.net.URI uri = new java.net.URI(line); 427 list.add(uri); 428 }catch (java.net.URISyntaxException ex) { 429 log.error("URIDrop: URISyntaxException"); 430 } 431 } 432 return list.toArray(new java.net.URI[list.size()]); 433 } 434 435 private static java.net.URI[] createURIArray(List<File> fileList) { 436 java.util.List<java.net.URI> list = new java.util.ArrayList<>(); 437 fileList.forEach((f) -> { 438 list.add(f.toURI()); 439 }); 440 return list.toArray(new java.net.URI[list.size()]); 441 } 442 443 private void makeDropTarget(final Component c, boolean recursive) { 444 // Make drop target 445 final DropTarget dt = new DropTarget(); 446 try { 447 dt.addDropTargetListener(dropListener); 448 } catch (java.util.TooManyListenersException e) { 449 log.error("URIDrop: Drop will not work due to previous error. Do you have another listener attached?", e); 450 } 451 452 // Listen for hierarchy changes and remove the drop target when the parent gets cleared out. 453 c.addHierarchyListener((HierarchyEvent evt) -> { 454 log.debug("URIDrop: Hierarchy changed."); 455 Component parent = c.getParent(); 456 if (parent == null) { 457 c.setDropTarget(null); 458 log.debug("URIDrop: Drop target cleared from component."); 459 } else { 460 new DropTarget(c, dropListener); 461 log.debug("URIDrop: Drop target added to component."); 462 } 463 }); 464 if (c.getParent() != null) { 465 new DropTarget(c, dropListener); 466 } 467 468 if (recursive && (c instanceof Container)) { 469 // Get the container 470 Container cont = (Container) c; 471 472 // Get its components 473 Component[] comps = cont.getComponents(); 474 475 // Set its components as listeners also 476 for (Component comp : comps) { 477 makeDropTarget(comp, recursive); 478 } 479 } 480 } 481 482 /** 483 * Determine if the dragged data is a file list. 484 */ 485 private boolean isDragOk(final DropTargetDragEvent evt) { 486 boolean ok = false; 487 488 // Get data flavors being dragged 489 DataFlavor[] flavors = evt.getCurrentDataFlavors(); 490 491 // See if any of the flavors are a file list 492 int i = 0; 493 while (!ok && i < flavors.length) { 494 // Is the flavor a file list? 495 final DataFlavor curFlavor = flavors[i]; 496 if (curFlavor.equals(DataFlavor.javaFileListFlavor) 497 || curFlavor.isRepresentationClassReader()) { 498 ok = true; 499 } 500 i++; 501 } 502 503 // If logging is enabled, show data flavors 504 if (log.isDebugEnabled()) { 505 if (flavors.length == 0) { 506 log.debug("URIDrop: no data flavors."); 507 } 508 for (i = 0; i < flavors.length; i++) { 509 log.debug("flavor {} {}", i, flavors[i].toString()); 510 } 511 } 512 513 return ok; 514 } 515 516 /** 517 * Removes the drag-and-drop hooks from the component and optionally from 518 * the all children. You should call this if you add and remove components 519 * after you've set up the drag-and-drop. This will recursively unregister 520 * all components contained within 521 * <var>c</var> if <var>c</var> is a {@link Container}. 522 * 523 * @param c The component to unregister as a drop target 524 * @return true if any components were unregistered 525 * @since 1.0 526 */ 527 public static boolean remove(Component c) { 528 return remove(c, true); 529 } 530 531 /** 532 * Removes the drag-and-drop hooks from the component and optionally from 533 * the all children. You should call this if you add and remove components 534 * after you've set up the drag-and-drop. 535 * 536 * @param c The component to unregister 537 * @param recursive Recursively unregister components within a container 538 * @return true if any components were unregistered 539 * @since 1.0 540 */ 541 public static boolean remove(Component c, boolean recursive) { 542 log.debug("URIDrop: Removing drag-and-drop hooks."); 543 c.setDropTarget(null); 544 if (recursive && (c instanceof Container)) { 545 Component[] comps = ((Container) c).getComponents(); 546 for (Component comp : comps) { 547 remove(comp, recursive); 548 } 549 return true; 550 } else { 551 return false; 552 } 553 } 554 555 /* ******** I N N E R I N T E R F A C E L I S T E N E R ******** */ 556 /** 557 * Implement this inner interface to listen for when uris are dropped. For 558 * example your class declaration may begin like this: 559 * <pre><code> 560 * public class MyClass implements URIsDrop.Listener 561 * ... 562 * public void URIsDropped( java.io.URI[] files ) 563 * { 564 * ... 565 * } 566 * ... 567 * </code></pre> 568 * 569 * @since 1.0 570 */ 571 public interface Listener { 572 573 /** 574 * This method is called when uris have been successfully dropped. 575 * 576 * @param uris An array of {@code URI}s that were dropped. 577 * @since 1.0 578 */ 579 public abstract void URIsDropped(java.net.URI[] uris); 580 } 581 582 public interface ListenerExt extends Listener{ 583 /** 584 * This method is called when an image has been successfully dropped. 585 * 586 * @param image The BufferedImage that has been dropped 587 * @since 1.0 588 */ 589 public void imageDropped(BufferedImage image); 590 } 591 592 593 private final static Logger log = LoggerFactory.getLogger(URIDrop.class); 594}