1 /* 2 * Copyright (c) 1998, 2014, 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.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 transient protected 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 protected void determineOffset(JTree tree, Object value, 481 boolean isSelected, boolean expanded, 482 boolean leaf, int row) { 483 if(renderer != null) { 484 if(leaf) 485 editingIcon = renderer.getLeafIcon(); 486 else if(expanded) 487 editingIcon = renderer.getOpenIcon(); 488 else 489 editingIcon = renderer.getClosedIcon(); 490 if(editingIcon != null) 491 offset = renderer.getIconTextGap() + 492 editingIcon.getIconWidth(); 493 else 494 offset = renderer.getIconTextGap(); 495 } 496 else { 497 editingIcon = null; 498 offset = 0; 499 } 500 } 501 502 /** 503 * Invoked just before editing is to start. Will add the 504 * <code>editingComponent</code> to the 505 * <code>editingContainer</code>. 506 */ 507 protected void prepareForEditing() { 508 if (editingComponent != null) { 509 editingContainer.add(editingComponent); 510 } 511 } 512 513 /** 514 * Creates the container to manage placement of 515 * <code>editingComponent</code>. 516 * 517 * @return new Container object 518 */ 519 protected Container createContainer() { 520 return new EditorContainer(); 521 } 522 523 /** 524 * This is invoked if a <code>TreeCellEditor</code> 525 * is not supplied in the constructor. 526 * It returns a <code>TextField</code> editor. 527 * @return a new <code>TextField</code> editor 528 */ 529 protected TreeCellEditor createTreeCellEditor() { 530 Border aBorder = UIManager.getBorder("Tree.editorBorder"); 531 @SuppressWarnings("serial") // Safe: outer class is non-serializable 532 DefaultCellEditor editor = new DefaultCellEditor 533 (new DefaultTextField(aBorder)) { 534 public boolean shouldSelectCell(EventObject event) { 535 boolean retValue = super.shouldSelectCell(event); 536 return retValue; 537 } 538 }; 539 540 // One click to edit. 541 editor.setClickCountToStart(1); 542 return editor; 543 } 544 545 /** 546 * Cleans up any state after editing has completed. Removes the 547 * <code>editingComponent</code> the <code>editingContainer</code>. 548 */ 549 private void cleanupAfterEditing() { 550 if (editingComponent != null) { 551 editingContainer.remove(editingComponent); 552 } 553 editingComponent = null; 554 } 555 556 /** 557 * <code>TextField</code> used when no editor is supplied. 558 * This textfield locks into the border it is constructed with. 559 * It also prefers its parents font over its font. And if the 560 * renderer is not <code>null</code> and no font 561 * has been specified the preferred height is that of the renderer. 562 */ 563 @SuppressWarnings("serial") // Safe: outer class is non-serializable 564 public class DefaultTextField extends JTextField { 565 /** Border to use. */ 566 protected Border border; 567 568 /** 569 * Constructs a 570 * <code>DefaultTreeCellEditor.DefaultTextField</code> object. 571 * 572 * @param border a <code>Border</code> object 573 * @since 1.4 574 */ 575 public DefaultTextField(Border border) { 576 setBorder(border); 577 } 578 579 /** 580 * Sets the border of this component.<p> 581 * This is a bound property. 582 * 583 * @param border the border to be rendered for this component 584 * @see Border 585 * @see CompoundBorder 586 * @beaninfo 587 * bound: true 588 * preferred: true 589 * attribute: visualUpdate true 590 * description: The component's border. 591 */ 592 public void setBorder(Border border) { 593 super.setBorder(border); 594 this.border = border; 595 } 596 597 /** 598 * Overrides <code>JComponent.getBorder</code> to 599 * returns the current border. 600 */ 601 public Border getBorder() { 602 return border; 603 } 604 605 // implements java.awt.MenuContainer 606 public Font getFont() { 607 Font font = super.getFont(); 608 609 // Prefer the parent containers font if our font is a 610 // FontUIResource 611 if(font instanceof FontUIResource) { 612 Container parent = getParent(); 613 614 if(parent != null && parent.getFont() != null) 615 font = parent.getFont(); 616 } 617 return font; 618 } 619 620 /** 621 * Overrides <code>JTextField.getPreferredSize</code> to 622 * return the preferred size based on current font, if set, 623 * or else use renderer's font. 624 * @return a <code>Dimension</code> object containing 625 * the preferred size 626 */ 627 public Dimension getPreferredSize() { 628 Dimension size = super.getPreferredSize(); 629 630 // If not font has been set, prefer the renderers height. 631 if(renderer != null && 632 DefaultTreeCellEditor.this.getFont() == null) { 633 Dimension rSize = renderer.getPreferredSize(); 634 635 size.height = rSize.height; 636 } 637 return size; 638 } 639 } 640 641 642 /** 643 * Container responsible for placing the <code>editingComponent</code>. 644 */ 645 @SuppressWarnings("serial") // Safe: outer class is non-serializable 646 public class EditorContainer extends Container { 647 /** 648 * Constructs an <code>EditorContainer</code> object. 649 */ 650 public EditorContainer() { 651 setLayout(null); 652 } 653 654 // This should not be used. It will be removed when new API is 655 // allowed. 656 public void EditorContainer() { 657 setLayout(null); 658 } 659 660 /** 661 * Overrides <code>Container.paint</code> to paint the node's 662 * icon and use the selection color for the background. 663 */ 664 public void paint(Graphics g) { 665 int width = getWidth(); 666 int height = getHeight(); 667 668 // Then the icon. 669 if(editingIcon != null) { 670 int yLoc = calculateIconY(editingIcon); 671 672 if (getComponentOrientation().isLeftToRight()) { 673 editingIcon.paintIcon(this, g, 0, yLoc); 674 } else { 675 editingIcon.paintIcon( 676 this, g, width - editingIcon.getIconWidth(), 677 yLoc); 678 } 679 } 680 681 // Border selection color 682 Color background = getBorderSelectionColor(); 683 if(background != null) { 684 g.setColor(background); 685 g.drawRect(0, 0, width - 1, height - 1); 686 } 687 super.paint(g); 688 } 689 690 /** 691 * Lays out this <code>Container</code>. If editing, 692 * the editor will be placed at 693 * <code>offset</code> in the x direction and 0 for y. 694 */ 695 public void doLayout() { 696 if(editingComponent != null) { 697 int width = getWidth(); 698 int height = getHeight(); 699 if (getComponentOrientation().isLeftToRight()) { 700 editingComponent.setBounds( 701 offset, 0, width - offset, height); 702 } else { 703 editingComponent.setBounds( 704 0, 0, width - offset, height); 705 } 706 } 707 } 708 709 /** 710 * Calculate the y location for the icon. 711 */ 712 private int calculateIconY(Icon icon) { 713 // To make sure the icon position matches that of the 714 // renderer, use the same algorithm as JLabel 715 // (SwingUtilities.layoutCompoundLabel). 716 int iconHeight = icon.getIconHeight(); 717 int textHeight = editingComponent.getFontMetrics( 718 editingComponent.getFont()).getHeight(); 719 int textY = iconHeight / 2 - textHeight / 2; 720 int totalY = Math.min(0, textY); 721 int totalHeight = Math.max(iconHeight, textY + textHeight) - 722 totalY; 723 return getHeight() / 2 - (totalY + (totalHeight / 2)); 724 } 725 726 /** 727 * Returns the preferred size for the <code>Container</code>. 728 * This will be at least preferred size of the editor plus 729 * <code>offset</code>. 730 * @return a <code>Dimension</code> containing the preferred 731 * size for the <code>Container</code>; if 732 * <code>editingComponent</code> is <code>null</code> the 733 * <code>Dimension</code> returned is 0, 0 734 */ 735 public Dimension getPreferredSize() { 736 if(editingComponent != null) { 737 Dimension pSize = editingComponent.getPreferredSize(); 738 739 pSize.width += offset + 5; 740 741 Dimension rSize = (renderer != null) ? 742 renderer.getPreferredSize() : null; 743 744 if(rSize != null) 745 pSize.height = Math.max(pSize.height, rSize.height); 746 if(editingIcon != null) 747 pSize.height = Math.max(pSize.height, 748 editingIcon.getIconHeight()); 749 750 // Make sure width is at least 100. 751 pSize.width = Math.max(pSize.width, 100); 752 return pSize; 753 } 754 return new Dimension(0, 0); 755 } 756 } 757 }