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