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