1 /* 2 * Copyright (c) 1998, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package javax.swing.tree; 26 27 import javax.swing.*; 28 import javax.swing.border.*; 29 import javax.swing.event.*; 30 import javax.swing.plaf.FontUIResource; 31 import java.awt.*; 32 import java.awt.event.*; 33 import java.beans.BeanProperty; 34 import java.util.EventObject; 35 36 /** 37 * A <code>TreeCellEditor</code>. You need to supply an 38 * instance of <code>DefaultTreeCellRenderer</code> 39 * so that the icons can be obtained. You can optionally supply 40 * a <code>TreeCellEditor</code> that will be layed out according 41 * to the icon in the <code>DefaultTreeCellRenderer</code>. 42 * If you do not supply a <code>TreeCellEditor</code>, 43 * a <code>TextField</code> will be used. Editing is started 44 * on a triple mouse click, or after a click, pause, click and 45 * a delay of 1200 milliseconds. 46 *<p> 47 * <strong>Warning:</strong> 48 * Serialized objects of this class will not be compatible with 49 * future Swing releases. The current serialization support is 50 * appropriate for short term storage or RMI between applications running 51 * the same version of Swing. As of 1.4, support for long term storage 52 * of all JavaBeans™ 53 * has been added to the <code>java.beans</code> package. 54 * Please see {@link java.beans.XMLEncoder}. 55 * 56 * @see javax.swing.JTree 57 * 58 * @author Scott Violet 59 */ 60 public class DefaultTreeCellEditor implements ActionListener, TreeCellEditor, 61 TreeSelectionListener { 62 /** Editor handling the editing. */ 63 protected TreeCellEditor realEditor; 64 65 /** Renderer, used to get border and offsets from. */ 66 protected DefaultTreeCellRenderer renderer; 67 68 /** Editing container, will contain the <code>editorComponent</code>. */ 69 protected Container editingContainer; 70 71 /** 72 * Component used in editing, obtained from the 73 * <code>editingContainer</code>. 74 */ 75 protected transient Component editingComponent; 76 77 /** 78 * As of Java 2 platform v1.4 this field should no longer be used. If 79 * you wish to provide similar behavior you should directly override 80 * <code>isCellEditable</code>. 81 */ 82 protected boolean canEdit; 83 84 /** 85 * Used in editing. Indicates x position to place 86 * <code>editingComponent</code>. 87 */ 88 protected transient int offset; 89 90 /** <code>JTree</code> instance listening too. */ 91 protected transient JTree tree; 92 93 /** Last path that was selected. */ 94 protected transient TreePath lastPath; 95 96 /** Used before starting the editing session. */ 97 protected transient Timer timer; 98 99 /** 100 * Row that was last passed into 101 * <code>getTreeCellEditorComponent</code>. 102 */ 103 protected transient int lastRow; 104 105 /** True if the border selection color should be drawn. */ 106 protected Color borderSelectionColor; 107 108 /** Icon to use when editing. */ 109 protected transient Icon editingIcon; 110 111 /** 112 * Font to paint with, <code>null</code> indicates 113 * font of renderer is to be used. 114 */ 115 protected Font font; 116 117 118 /** 119 * Constructs a <code>DefaultTreeCellEditor</code> 120 * object for a JTree using the specified renderer and 121 * a default editor. (Use this constructor for normal editing.) 122 * 123 * @param tree a <code>JTree</code> object 124 * @param renderer a <code>DefaultTreeCellRenderer</code> object 125 */ 126 public DefaultTreeCellEditor(JTree tree, 127 DefaultTreeCellRenderer renderer) { 128 this(tree, renderer, null); 129 } 130 131 /** 132 * Constructs a <code>DefaultTreeCellEditor</code> 133 * object for a <code>JTree</code> using the 134 * specified renderer and the specified editor. (Use this constructor 135 * for specialized editing.) 136 * 137 * @param tree a <code>JTree</code> object 138 * @param renderer a <code>DefaultTreeCellRenderer</code> object 139 * @param editor a <code>TreeCellEditor</code> object 140 */ 141 public DefaultTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer, 142 TreeCellEditor editor) { 143 this.renderer = renderer; 144 realEditor = editor; 145 if(realEditor == null) 146 realEditor = createTreeCellEditor(); 147 editingContainer = createContainer(); 148 setTree(tree); 149 setBorderSelectionColor(UIManager.getColor 150 ("Tree.editorBorderSelectionColor")); 151 } 152 153 /** 154 * Sets the color to use for the border. 155 * @param newColor the new border color 156 */ 157 public void setBorderSelectionColor(Color newColor) { 158 borderSelectionColor = newColor; 159 } 160 161 /** 162 * Returns the color the border is drawn. 163 * @return the border selection color 164 */ 165 public Color getBorderSelectionColor() { 166 return borderSelectionColor; 167 } 168 169 /** 170 * Sets the font to edit with. <code>null</code> indicates 171 * the renderers font should be used. This will NOT 172 * override any font you have set in the editor 173 * the receiver was instantiated with. If <code>null</code> 174 * for an editor was passed in a default editor will be 175 * created that will pick up this font. 176 * 177 * @param font the editing <code>Font</code> 178 * @see #getFont 179 */ 180 public void setFont(Font font) { 181 this.font = font; 182 } 183 184 /** 185 * Gets the font used for editing. 186 * 187 * @return the editing <code>Font</code> 188 * @see #setFont 189 */ 190 public Font getFont() { 191 return font; 192 } 193 194 // 195 // TreeCellEditor 196 // 197 198 /** 199 * Configures the editor. Passed onto the <code>realEditor</code>. 200 */ 201 public Component getTreeCellEditorComponent(JTree tree, Object value, 202 boolean isSelected, 203 boolean expanded, 204 boolean leaf, int row) { 205 setTree(tree); 206 lastRow = row; 207 determineOffset(tree, value, isSelected, expanded, leaf, row); 208 209 if (editingComponent != null) { 210 editingContainer.remove(editingComponent); 211 } 212 editingComponent = realEditor.getTreeCellEditorComponent(tree, value, 213 isSelected, expanded,leaf, row); 214 215 216 // this is kept for backwards compatibility but isn't really needed 217 // with the current BasicTreeUI implementation. 218 TreePath newPath = tree.getPathForRow(row); 219 220 canEdit = (lastPath != null && newPath != null && 221 lastPath.equals(newPath)); 222 223 Font font = getFont(); 224 225 if(font == null) { 226 if(renderer != null) 227 font = renderer.getFont(); 228 if(font == null) 229 font = tree.getFont(); 230 } 231 editingContainer.setFont(font); 232 prepareForEditing(); 233 return editingContainer; 234 } 235 236 /** 237 * Returns the value currently being edited. 238 * @return the value currently being edited 239 */ 240 public Object getCellEditorValue() { 241 return realEditor.getCellEditorValue(); 242 } 243 244 /** 245 * If the <code>realEditor</code> returns true to this 246 * message, <code>prepareForEditing</code> 247 * is messaged and true is returned. 248 */ 249 public boolean isCellEditable(EventObject event) { 250 boolean retValue = false; 251 boolean editable = false; 252 253 if (event != null) { 254 if (event.getSource() instanceof JTree) { 255 setTree((JTree)event.getSource()); 256 if (event instanceof MouseEvent) { 257 TreePath path = tree.getPathForLocation( 258 ((MouseEvent)event).getX(), 259 ((MouseEvent)event).getY()); 260 editable = (lastPath != null && path != null && 261 lastPath.equals(path)); 262 if (path!=null) { 263 lastRow = tree.getRowForPath(path); 264 Object value = path.getLastPathComponent(); 265 boolean isSelected = tree.isRowSelected(lastRow); 266 boolean expanded = tree.isExpanded(path); 267 TreeModel treeModel = tree.getModel(); 268 boolean leaf = treeModel.isLeaf(value); 269 determineOffset(tree, value, isSelected, 270 expanded, leaf, lastRow); 271 } 272 } 273 } 274 } 275 if(!realEditor.isCellEditable(event)) 276 return false; 277 if(canEditImmediately(event)) 278 retValue = true; 279 else if(editable && shouldStartEditingTimer(event)) { 280 startEditingTimer(); 281 } 282 else if(timer != null && timer.isRunning()) 283 timer.stop(); 284 if(retValue) 285 prepareForEditing(); 286 return retValue; 287 } 288 289 /** 290 * Messages the <code>realEditor</code> for the return value. 291 */ 292 public boolean shouldSelectCell(EventObject event) { 293 return realEditor.shouldSelectCell(event); 294 } 295 296 /** 297 * If the <code>realEditor</code> will allow editing to stop, 298 * the <code>realEditor</code> is removed and true is returned, 299 * otherwise false is returned. 300 */ 301 public boolean stopCellEditing() { 302 if(realEditor.stopCellEditing()) { 303 cleanupAfterEditing(); 304 return true; 305 } 306 return false; 307 } 308 309 /** 310 * Messages <code>cancelCellEditing</code> to the 311 * <code>realEditor</code> and removes it from this instance. 312 */ 313 public void cancelCellEditing() { 314 realEditor.cancelCellEditing(); 315 cleanupAfterEditing(); 316 } 317 318 /** 319 * Adds the <code>CellEditorListener</code>. 320 * @param l the listener to be added 321 */ 322 public void addCellEditorListener(CellEditorListener l) { 323 realEditor.addCellEditorListener(l); 324 } 325 326 /** 327 * Removes the previously added <code>CellEditorListener</code>. 328 * @param l the listener to be removed 329 */ 330 public void removeCellEditorListener(CellEditorListener l) { 331 realEditor.removeCellEditorListener(l); 332 } 333 334 /** 335 * Returns an array of all the <code>CellEditorListener</code>s added 336 * to this DefaultTreeCellEditor with addCellEditorListener(). 337 * 338 * @return all of the <code>CellEditorListener</code>s added or an empty 339 * array if no listeners have been added 340 * @since 1.4 341 */ 342 public CellEditorListener[] getCellEditorListeners() { 343 return ((DefaultCellEditor)realEditor).getCellEditorListeners(); 344 } 345 346 // 347 // TreeSelectionListener 348 // 349 350 /** 351 * Resets <code>lastPath</code>. 352 */ 353 public void valueChanged(TreeSelectionEvent e) { 354 if(tree != null) { 355 if(tree.getSelectionCount() == 1) 356 lastPath = tree.getSelectionPath(); 357 else 358 lastPath = null; 359 } 360 if(timer != null) { 361 timer.stop(); 362 } 363 } 364 365 // 366 // ActionListener (for Timer). 367 // 368 369 /** 370 * Messaged when the timer fires, this will start the editing 371 * session. 372 */ 373 public void actionPerformed(ActionEvent e) { 374 if(tree != null && lastPath != null) { 375 tree.startEditingAtPath(lastPath); 376 } 377 } 378 379 // 380 // Local methods 381 // 382 383 /** 384 * Sets the tree currently editing for. This is needed to add 385 * a selection listener. 386 * @param newTree the new tree to be edited 387 */ 388 protected void setTree(JTree newTree) { 389 if(tree != newTree) { 390 if(tree != null) 391 tree.removeTreeSelectionListener(this); 392 tree = newTree; 393 if(tree != null) 394 tree.addTreeSelectionListener(this); 395 if(timer != null) { 396 timer.stop(); 397 } 398 } 399 } 400 401 /** 402 * Returns true if <code>event</code> is a <code>MouseEvent</code> 403 * and the click count is 1. 404 * 405 * @param event the event being studied 406 * @return whether {@code event} should starts the editing timer 407 */ 408 protected boolean shouldStartEditingTimer(EventObject event) { 409 if((event instanceof MouseEvent) && 410 SwingUtilities.isLeftMouseButton((MouseEvent)event)) { 411 MouseEvent me = (MouseEvent)event; 412 413 return (me.getClickCount() == 1 && 414 inHitRegion(me.getX(), me.getY())); 415 } 416 return false; 417 } 418 419 /** 420 * Starts the editing timer. 421 */ 422 protected void startEditingTimer() { 423 if(timer == null) { 424 timer = new Timer(1200, this); 425 timer.setRepeats(false); 426 } 427 timer.start(); 428 } 429 430 /** 431 * Returns true if <code>event</code> is <code>null</code>, 432 * or it is a <code>MouseEvent</code> with a click count > 2 433 * and <code>inHitRegion</code> returns true. 434 * 435 * @param event the event being studied 436 * @return whether editing can be started for the given {@code event} 437 */ 438 protected boolean canEditImmediately(EventObject event) { 439 if((event instanceof MouseEvent) && 440 SwingUtilities.isLeftMouseButton((MouseEvent)event)) { 441 MouseEvent me = (MouseEvent)event; 442 443 return ((me.getClickCount() > 2) && 444 inHitRegion(me.getX(), me.getY())); 445 } 446 return (event == null); 447 } 448 449 /** 450 * Returns true if the passed in location is a valid mouse location 451 * to start editing from. This is implemented to return false if 452 * <code>x</code> is <= the width of the icon and icon gap displayed 453 * by the renderer. In other words this returns true if the user 454 * clicks over the text part displayed by the renderer, and false 455 * otherwise. 456 * @param x the x-coordinate of the point 457 * @param y the y-coordinate of the point 458 * @return true if the passed in location is a valid mouse location 459 */ 460 protected boolean inHitRegion(int x, int y) { 461 if(lastRow != -1 && tree != null) { 462 Rectangle bounds = tree.getRowBounds(lastRow); 463 ComponentOrientation treeOrientation = tree.getComponentOrientation(); 464 465 if ( treeOrientation.isLeftToRight() ) { 466 if (bounds != null && x <= (bounds.x + offset) && 467 offset < (bounds.width - 5)) { 468 return false; 469 } 470 } else if ( bounds != null && 471 ( x >= (bounds.x+bounds.width-offset+5) || 472 x <= (bounds.x + 5) ) && 473 offset < (bounds.width - 5) ) { 474 return false; 475 } 476 } 477 return true; 478 } 479 480 /** 481 * Determine the offset. 482 * @param tree a <code>JTree</code> object 483 * @param value a value 484 * @param isSelected selection status 485 * @param expanded expansion status 486 * @param leaf leaf status 487 * @param row current row 488 */ 489 protected void determineOffset(JTree tree, Object value, 490 boolean isSelected, boolean expanded, 491 boolean leaf, int row) { 492 if(renderer != null) { 493 if(leaf) 494 editingIcon = renderer.getLeafIcon(); 495 else if(expanded) 496 editingIcon = renderer.getOpenIcon(); 497 else 498 editingIcon = renderer.getClosedIcon(); 499 if(editingIcon != null) 500 offset = renderer.getIconTextGap() + 501 editingIcon.getIconWidth(); 502 else 503 offset = renderer.getIconTextGap(); 504 } 505 else { 506 editingIcon = null; 507 offset = 0; 508 } 509 } 510 511 /** 512 * Invoked just before editing is to start. Will add the 513 * <code>editingComponent</code> to the 514 * <code>editingContainer</code>. 515 */ 516 protected void prepareForEditing() { 517 if (editingComponent != null) { 518 editingContainer.add(editingComponent); 519 } 520 } 521 522 /** 523 * Creates the container to manage placement of 524 * <code>editingComponent</code>. 525 * 526 * @return new Container object 527 */ 528 protected Container createContainer() { 529 return new EditorContainer(); 530 } 531 532 /** 533 * This is invoked if a <code>TreeCellEditor</code> 534 * is not supplied in the constructor. 535 * It returns a <code>TextField</code> editor. 536 * @return a new <code>TextField</code> editor 537 */ 538 protected TreeCellEditor createTreeCellEditor() { 539 Border aBorder = UIManager.getBorder("Tree.editorBorder"); 540 @SuppressWarnings("serial") // Safe: outer class is non-serializable 541 DefaultCellEditor editor = new DefaultCellEditor 542 (new DefaultTextField(aBorder)) { 543 public boolean shouldSelectCell(EventObject event) { 544 boolean retValue = super.shouldSelectCell(event); 545 return retValue; 546 } 547 }; 548 549 // One click to edit. 550 editor.setClickCountToStart(1); 551 return editor; 552 } 553 554 /** 555 * Cleans up any state after editing has completed. Removes the 556 * <code>editingComponent</code> the <code>editingContainer</code>. 557 */ 558 private void cleanupAfterEditing() { 559 if (editingComponent != null) { 560 editingContainer.remove(editingComponent); 561 } 562 editingComponent = null; 563 } 564 565 /** 566 * <code>TextField</code> used when no editor is supplied. 567 * This textfield locks into the border it is constructed with. 568 * It also prefers its parents font over its font. And if the 569 * renderer is not <code>null</code> and no font 570 * has been specified the preferred height is that of the renderer. 571 */ 572 @SuppressWarnings("serial") // Safe: outer class is non-serializable 573 public class DefaultTextField extends JTextField { 574 /** Border to use. */ 575 protected Border border; 576 577 /** 578 * Constructs a 579 * <code>DefaultTreeCellEditor.DefaultTextField</code> object. 580 * 581 * @param border a <code>Border</code> object 582 * @since 1.4 583 */ 584 public DefaultTextField(Border border) { 585 setBorder(border); 586 } 587 588 /** 589 * Sets the border of this component.<p> 590 * This is a bound property. 591 * 592 * @param border the border to be rendered for this component 593 * @see Border 594 * @see CompoundBorder 595 */ 596 @BeanProperty(preferred = true, visualUpdate = true, description 597 = "The component's border.") 598 public void setBorder(Border border) { 599 super.setBorder(border); 600 this.border = border; 601 } 602 603 /** 604 * Overrides <code>JComponent.getBorder</code> to 605 * returns the current border. 606 */ 607 public Border getBorder() { 608 return border; 609 } 610 611 // implements java.awt.MenuContainer 612 public Font getFont() { 613 Font font = super.getFont(); 614 615 // Prefer the parent containers font if our font is a 616 // FontUIResource 617 if(font instanceof FontUIResource) { 618 Container parent = getParent(); 619 620 if(parent != null && parent.getFont() != null) 621 font = parent.getFont(); 622 } 623 return font; 624 } 625 626 /** 627 * Overrides <code>JTextField.getPreferredSize</code> to 628 * return the preferred size based on current font, if set, 629 * or else use renderer's font. 630 * @return a <code>Dimension</code> object containing 631 * the preferred size 632 */ 633 public Dimension getPreferredSize() { 634 Dimension size = super.getPreferredSize(); 635 636 // If not font has been set, prefer the renderers height. 637 if(renderer != null && 638 DefaultTreeCellEditor.this.getFont() == null) { 639 Dimension rSize = renderer.getPreferredSize(); 640 641 size.height = rSize.height; 642 } 643 return size; 644 } 645 } 646 647 648 /** 649 * Container responsible for placing the <code>editingComponent</code>. 650 */ 651 @SuppressWarnings("serial") // Safe: outer class is non-serializable 652 public class EditorContainer extends Container { 653 /** 654 * Constructs an <code>EditorContainer</code> object. 655 */ 656 public EditorContainer() { 657 setLayout(null); 658 } 659 660 // This should not be used. It will be removed when new API is 661 // allowed. 662 /** 663 * Do not use. 664 */ 665 public void EditorContainer() { 666 setLayout(null); 667 } 668 669 /** 670 * Overrides <code>Container.paint</code> to paint the node's 671 * icon and use the selection color for the background. 672 */ 673 public void paint(Graphics g) { 674 int width = getWidth(); 675 int height = getHeight(); 676 677 // Then the icon. 678 if(editingIcon != null) { 679 int yLoc = calculateIconY(editingIcon); 680 681 if (getComponentOrientation().isLeftToRight()) { 682 editingIcon.paintIcon(this, g, 0, yLoc); 683 } else { 684 editingIcon.paintIcon( 685 this, g, width - editingIcon.getIconWidth(), 686 yLoc); 687 } 688 } 689 690 // Border selection color 691 Color background = getBorderSelectionColor(); 692 if(background != null) { 693 g.setColor(background); 694 g.drawRect(0, 0, width - 1, height - 1); 695 } 696 super.paint(g); 697 } 698 699 /** 700 * Lays out this <code>Container</code>. If editing, 701 * the editor will be placed at 702 * <code>offset</code> in the x direction and 0 for y. 703 */ 704 public void doLayout() { 705 if(editingComponent != null) { 706 int width = getWidth(); 707 int height = getHeight(); 708 if (getComponentOrientation().isLeftToRight()) { 709 editingComponent.setBounds( 710 offset, 0, width - offset, height); 711 } else { 712 editingComponent.setBounds( 713 0, 0, width - offset, height); 714 } 715 } 716 } 717 718 /** 719 * Calculate the y location for the icon. 720 */ 721 private int calculateIconY(Icon icon) { 722 // To make sure the icon position matches that of the 723 // renderer, use the same algorithm as JLabel 724 // (SwingUtilities.layoutCompoundLabel). 725 int iconHeight = icon.getIconHeight(); 726 int textHeight = editingComponent.getFontMetrics( 727 editingComponent.getFont()).getHeight(); 728 int textY = iconHeight / 2 - textHeight / 2; 729 int totalY = Math.min(0, textY); 730 int totalHeight = Math.max(iconHeight, textY + textHeight) - 731 totalY; 732 return getHeight() / 2 - (totalY + (totalHeight / 2)); 733 } 734 735 /** 736 * Returns the preferred size for the <code>Container</code>. 737 * This will be at least preferred size of the editor plus 738 * <code>offset</code>. 739 * @return a <code>Dimension</code> containing the preferred 740 * size for the <code>Container</code>; if 741 * <code>editingComponent</code> is <code>null</code> the 742 * <code>Dimension</code> returned is 0, 0 743 */ 744 public Dimension getPreferredSize() { 745 if(editingComponent != null) { 746 Dimension pSize = editingComponent.getPreferredSize(); 747 748 pSize.width += offset + 5; 749 750 Dimension rSize = (renderer != null) ? 751 renderer.getPreferredSize() : null; 752 753 if(rSize != null) 754 pSize.height = Math.max(pSize.height, rSize.height); 755 if(editingIcon != null) 756 pSize.height = Math.max(pSize.height, 757 editingIcon.getIconHeight()); 758 759 // Make sure width is at least 100. 760 pSize.width = Math.max(pSize.width, 100); 761 return pSize; 762 } 763 return new Dimension(0, 0); 764 } 765 } 766 }