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