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