/*
* Copyright (c) 1998, 2013, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package javax.swing.tree;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.plaf.
FontUIResource;
import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.io.*;
import java.util.
EventObject;
import java.util.
Vector;
/**
* A <code>TreeCellEditor</code>. You need to supply an
* instance of <code>DefaultTreeCellRenderer</code>
* so that the icons can be obtained. You can optionally supply
* a <code>TreeCellEditor</code> that will be layed out according
* to the icon in the <code>DefaultTreeCellRenderer</code>.
* If you do not supply a <code>TreeCellEditor</code>,
* a <code>TextField</code> will be used. Editing is started
* on a triple mouse click, or after a click, pause, click and
* a delay of 1200 milliseconds.
*<p>
* <strong>Warning:</strong>
* Serialized objects of this class will not be compatible with
* future Swing releases. The current serialization support is
* appropriate for short term storage or RMI between applications running
* the same version of Swing. As of 1.4, support for long term storage
* of all JavaBeans™
* has been added to the <code>java.beans</code> package.
* Please see {@link java.beans.XMLEncoder}.
*
* @see javax.swing.JTree
*
* @author Scott Violet
*/
public class
DefaultTreeCellEditor implements
ActionListener,
TreeCellEditor,
TreeSelectionListener {
/** Editor handling the editing. */
protected
TreeCellEditor realEditor;
/** Renderer, used to get border and offsets from. */
protected
DefaultTreeCellRenderer renderer;
/** Editing container, will contain the <code>editorComponent</code>. */
protected
Container editingContainer;
/**
* Component used in editing, obtained from the
* <code>editingContainer</code>.
*/
transient protected
Component editingComponent;
/**
* As of Java 2 platform v1.4 this field should no longer be used. If
* you wish to provide similar behavior you should directly override
* <code>isCellEditable</code>.
*/
protected boolean
canEdit;
/**
* Used in editing. Indicates x position to place
* <code>editingComponent</code>.
*/
protected transient int
offset;
/** <code>JTree</code> instance listening too. */
protected transient
JTree tree;
/** Last path that was selected. */
protected transient
TreePath lastPath;
/** Used before starting the editing session. */
protected transient
Timer timer;
/**
* Row that was last passed into
* <code>getTreeCellEditorComponent</code>.
*/
protected transient int
lastRow;
/** True if the border selection color should be drawn. */
protected
Color borderSelectionColor;
/** Icon to use when editing. */
protected transient
Icon editingIcon;
/**
* Font to paint with, <code>null</code> indicates
* font of renderer is to be used.
*/
protected
Font font;
/**
* Constructs a <code>DefaultTreeCellEditor</code>
* object for a JTree using the specified renderer and
* a default editor. (Use this constructor for normal editing.)
*
* @param tree a <code>JTree</code> object
* @param renderer a <code>DefaultTreeCellRenderer</code> object
*/
public
DefaultTreeCellEditor(
JTree tree,
DefaultTreeCellRenderer renderer) {
this(
tree,
renderer, null);
}
/**
* Constructs a <code>DefaultTreeCellEditor</code>
* object for a <code>JTree</code> using the
* specified renderer and the specified editor. (Use this constructor
* for specialized editing.)
*
* @param tree a <code>JTree</code> object
* @param renderer a <code>DefaultTreeCellRenderer</code> object
* @param editor a <code>TreeCellEditor</code> object
*/
public
DefaultTreeCellEditor(
JTree tree,
DefaultTreeCellRenderer renderer,
TreeCellEditor editor) {
this.
renderer =
renderer;
realEditor =
editor;
if(
realEditor == null)
realEditor =
createTreeCellEditor();
editingContainer =
createContainer();
setTree(
tree);
setBorderSelectionColor(
UIManager.
getColor
("Tree.editorBorderSelectionColor"));
}
/**
* Sets the color to use for the border.
* @param newColor the new border color
*/
public void
setBorderSelectionColor(
Color newColor) {
borderSelectionColor =
newColor;
}
/**
* Returns the color the border is drawn.
* @return the border selection color
*/
public
Color getBorderSelectionColor() {
return
borderSelectionColor;
}
/**
* Sets the font to edit with. <code>null</code> indicates
* the renderers font should be used. This will NOT
* override any font you have set in the editor
* the receiver was instantiated with. If <code>null</code>
* for an editor was passed in a default editor will be
* created that will pick up this font.
*
* @param font the editing <code>Font</code>
* @see #getFont
*/
public void
setFont(
Font font) {
this.
font =
font;
}
/**
* Gets the font used for editing.
*
* @return the editing <code>Font</code>
* @see #setFont
*/
public
Font getFont() {
return
font;
}
//
// TreeCellEditor
//
/**
* Configures the editor. Passed onto the <code>realEditor</code>.
*/
public
Component getTreeCellEditorComponent(
JTree tree,
Object value,
boolean
isSelected,
boolean
expanded,
boolean
leaf, int
row) {
setTree(
tree);
lastRow =
row;
determineOffset(
tree,
value,
isSelected,
expanded,
leaf,
row);
if (
editingComponent != null) {
editingContainer.
remove(
editingComponent);
}
editingComponent =
realEditor.
getTreeCellEditorComponent(
tree,
value,
isSelected,
expanded,
leaf,
row);
// this is kept for backwards compatibility but isn't really needed
// with the current BasicTreeUI implementation.
TreePath newPath =
tree.
getPathForRow(
row);
canEdit = (
lastPath != null &&
newPath != null &&
lastPath.
equals(
newPath));
Font font =
getFont();
if(
font == null) {
if(
renderer != null)
font =
renderer.
getFont();
if(
font == null)
font =
tree.
getFont();
}
editingContainer.
setFont(
font);
prepareForEditing();
return
editingContainer;
}
/**
* Returns the value currently being edited.
* @return the value currently being edited
*/
public
Object getCellEditorValue() {
return
realEditor.
getCellEditorValue();
}
/**
* If the <code>realEditor</code> returns true to this
* message, <code>prepareForEditing</code>
* is messaged and true is returned.
*/
public boolean
isCellEditable(
EventObject event) {
boolean
retValue = false;
boolean
editable = false;
if (
event != null) {
if (
event.
getSource() instanceof
JTree) {
setTree((
JTree)
event.
getSource());
if (
event instanceof
MouseEvent) {
TreePath path =
tree.
getPathForLocation(
((
MouseEvent)
event).
getX(),
((
MouseEvent)
event).
getY());
editable = (
lastPath != null &&
path != null &&
lastPath.
equals(
path));
if (
path!=null) {
lastRow =
tree.
getRowForPath(
path);
Object value =
path.
getLastPathComponent();
boolean
isSelected =
tree.
isRowSelected(
lastRow);
boolean
expanded =
tree.
isExpanded(
path);
TreeModel treeModel =
tree.
getModel();
boolean
leaf =
treeModel.
isLeaf(
value);
determineOffset(
tree,
value,
isSelected,
expanded,
leaf,
lastRow);
}
}
}
}
if(!
realEditor.
isCellEditable(
event))
return false;
if(
canEditImmediately(
event))
retValue = true;
else if(
editable &&
shouldStartEditingTimer(
event)) {
startEditingTimer();
}
else if(
timer != null &&
timer.
isRunning())
timer.
stop();
if(
retValue)
prepareForEditing();
return
retValue;
}
/**
* Messages the <code>realEditor</code> for the return value.
*/
public boolean
shouldSelectCell(
EventObject event) {
return
realEditor.
shouldSelectCell(
event);
}
/**
* If the <code>realEditor</code> will allow editing to stop,
* the <code>realEditor</code> is removed and true is returned,
* otherwise false is returned.
*/
public boolean
stopCellEditing() {
if(
realEditor.
stopCellEditing()) {
cleanupAfterEditing();
return true;
}
return false;
}
/**
* Messages <code>cancelCellEditing</code> to the
* <code>realEditor</code> and removes it from this instance.
*/
public void
cancelCellEditing() {
realEditor.
cancelCellEditing();
cleanupAfterEditing();
}
/**
* Adds the <code>CellEditorListener</code>.
* @param l the listener to be added
*/
public void
addCellEditorListener(
CellEditorListener l) {
realEditor.
addCellEditorListener(
l);
}
/**
* Removes the previously added <code>CellEditorListener</code>.
* @param l the listener to be removed
*/
public void
removeCellEditorListener(
CellEditorListener l) {
realEditor.
removeCellEditorListener(
l);
}
/**
* Returns an array of all the <code>CellEditorListener</code>s added
* to this DefaultTreeCellEditor with addCellEditorListener().
*
* @return all of the <code>CellEditorListener</code>s added or an empty
* array if no listeners have been added
* @since 1.4
*/
public
CellEditorListener[]
getCellEditorListeners() {
return ((
DefaultCellEditor)
realEditor).
getCellEditorListeners();
}
//
// TreeSelectionListener
//
/**
* Resets <code>lastPath</code>.
*/
public void
valueChanged(
TreeSelectionEvent e) {
if(
tree != null) {
if(
tree.
getSelectionCount() == 1)
lastPath =
tree.
getSelectionPath();
else
lastPath = null;
}
if(
timer != null) {
timer.
stop();
}
}
//
// ActionListener (for Timer).
//
/**
* Messaged when the timer fires, this will start the editing
* session.
*/
public void
actionPerformed(
ActionEvent e) {
if(
tree != null &&
lastPath != null) {
tree.
startEditingAtPath(
lastPath);
}
}
//
// Local methods
//
/**
* Sets the tree currently editing for. This is needed to add
* a selection listener.
* @param newTree the new tree to be edited
*/
protected void
setTree(
JTree newTree) {
if(
tree !=
newTree) {
if(
tree != null)
tree.
removeTreeSelectionListener(this);
tree =
newTree;
if(
tree != null)
tree.
addTreeSelectionListener(this);
if(
timer != null) {
timer.
stop();
}
}
}
/**
* Returns true if <code>event</code> is a <code>MouseEvent</code>
* and the click count is 1.
* @param event the event being studied
*/
protected boolean
shouldStartEditingTimer(
EventObject event) {
if((
event instanceof
MouseEvent) &&
SwingUtilities.
isLeftMouseButton((
MouseEvent)
event)) {
MouseEvent me = (
MouseEvent)
event;
return (
me.
getClickCount() == 1 &&
inHitRegion(
me.
getX(),
me.
getY()));
}
return false;
}
/**
* Starts the editing timer.
*/
protected void
startEditingTimer() {
if(
timer == null) {
timer = new
Timer(1200, this);
timer.
setRepeats(false);
}
timer.
start();
}
/**
* Returns true if <code>event</code> is <code>null</code>,
* or it is a <code>MouseEvent</code> with a click count > 2
* and <code>inHitRegion</code> returns true.
* @param event the event being studied
*/
protected boolean
canEditImmediately(
EventObject event) {
if((
event instanceof
MouseEvent) &&
SwingUtilities.
isLeftMouseButton((
MouseEvent)
event)) {
MouseEvent me = (
MouseEvent)
event;
return ((
me.
getClickCount() > 2) &&
inHitRegion(
me.
getX(),
me.
getY()));
}
return (
event == null);
}
/**
* Returns true if the passed in location is a valid mouse location
* to start editing from. This is implemented to return false if
* <code>x</code> is <= the width of the icon and icon gap displayed
* by the renderer. In other words this returns true if the user
* clicks over the text part displayed by the renderer, and false
* otherwise.
* @param x the x-coordinate of the point
* @param y the y-coordinate of the point
* @return true if the passed in location is a valid mouse location
*/
protected boolean
inHitRegion(int
x, int
y) {
if(
lastRow != -1 &&
tree != null) {
Rectangle bounds =
tree.
getRowBounds(
lastRow);
ComponentOrientation treeOrientation =
tree.
getComponentOrientation();
if (
treeOrientation.
isLeftToRight() ) {
if (
bounds != null &&
x <= (
bounds.
x +
offset) &&
offset < (
bounds.
width - 5)) {
return false;
}
} else if (
bounds != null &&
(
x >= (
bounds.
x+
bounds.
width-
offset+5) ||
x <= (
bounds.
x + 5) ) &&
offset < (
bounds.
width - 5) ) {
return false;
}
}
return true;
}
protected void
determineOffset(
JTree tree,
Object value,
boolean
isSelected, boolean
expanded,
boolean
leaf, int
row) {
if(
renderer != null) {
if(
leaf)
editingIcon =
renderer.
getLeafIcon();
else if(
expanded)
editingIcon =
renderer.
getOpenIcon();
else
editingIcon =
renderer.
getClosedIcon();
if(
editingIcon != null)
offset =
renderer.
getIconTextGap() +
editingIcon.
getIconWidth();
else
offset =
renderer.
getIconTextGap();
}
else {
editingIcon = null;
offset = 0;
}
}
/**
* Invoked just before editing is to start. Will add the
* <code>editingComponent</code> to the
* <code>editingContainer</code>.
*/
protected void
prepareForEditing() {
if (
editingComponent != null) {
editingContainer.
add(
editingComponent);
}
}
/**
* Creates the container to manage placement of
* <code>editingComponent</code>.
*/
protected
Container createContainer() {
return new
EditorContainer();
}
/**
* This is invoked if a <code>TreeCellEditor</code>
* is not supplied in the constructor.
* It returns a <code>TextField</code> editor.
* @return a new <code>TextField</code> editor
*/
protected
TreeCellEditor createTreeCellEditor() {
Border aBorder =
UIManager.
getBorder("Tree.editorBorder");
DefaultCellEditor editor = new
DefaultCellEditor
(new
DefaultTextField(
aBorder)) {
public boolean
shouldSelectCell(
EventObject event) {
boolean
retValue = super.shouldSelectCell(
event);
return
retValue;
}
};
// One click to edit.
editor.
setClickCountToStart(1);
return
editor;
}
/**
* Cleans up any state after editing has completed. Removes the
* <code>editingComponent</code> the <code>editingContainer</code>.
*/
private void
cleanupAfterEditing() {
if (
editingComponent != null) {
editingContainer.
remove(
editingComponent);
}
editingComponent = null;
}
// Serialization support.
private void
writeObject(
ObjectOutputStream s) throws
IOException {
Vector<
Object>
values = new
Vector<
Object>();
s.
defaultWriteObject();
// Save the realEditor, if its Serializable.
if(
realEditor != null &&
realEditor instanceof
Serializable) {
values.
addElement("realEditor");
values.
addElement(
realEditor);
}
s.
writeObject(
values);
}
private void
readObject(
ObjectInputStream s)
throws
IOException,
ClassNotFoundException {
s.
defaultReadObject();
Vector values = (
Vector)
s.
readObject();
int
indexCounter = 0;
int
maxCounter =
values.
size();
if(
indexCounter <
maxCounter &&
values.
elementAt(
indexCounter).
equals("realEditor")) {
realEditor = (
TreeCellEditor)
values.
elementAt(++
indexCounter);
indexCounter++;
}
}
/**
* <code>TextField</code> used when no editor is supplied.
* This textfield locks into the border it is constructed with.
* It also prefers its parents font over its font. And if the
* renderer is not <code>null</code> and no font
* has been specified the preferred height is that of the renderer.
*/
public class
DefaultTextField extends
JTextField {
/** Border to use. */
protected
Border border;
/**
* Constructs a
* <code>DefaultTreeCellEditor.DefaultTextField</code> object.
*
* @param border a <code>Border</code> object
* @since 1.4
*/
public
DefaultTextField(
Border border) {
setBorder(
border);
}
/**
* Sets the border of this component.<p>
* This is a bound property.
*
* @param border the border to be rendered for this component
* @see Border
* @see CompoundBorder
* @beaninfo
* bound: true
* preferred: true
* attribute: visualUpdate true
* description: The component's border.
*/
public void
setBorder(
Border border) {
super.setBorder(
border);
this.
border =
border;
}
/**
* Overrides <code>JComponent.getBorder</code> to
* returns the current border.
*/
public
Border getBorder() {
return
border;
}
// implements java.awt.MenuContainer
public
Font getFont() {
Font font = super.getFont();
// Prefer the parent containers font if our font is a
// FontUIResource
if(
font instanceof
FontUIResource) {
Container parent =
getParent();
if(
parent != null &&
parent.
getFont() != null)
font =
parent.
getFont();
}
return
font;
}
/**
* Overrides <code>JTextField.getPreferredSize</code> to
* return the preferred size based on current font, if set,
* or else use renderer's font.
* @return a <code>Dimension</code> object containing
* the preferred size
*/
public
Dimension getPreferredSize() {
Dimension size = super.getPreferredSize();
// If not font has been set, prefer the renderers height.
if(
renderer != null &&
DefaultTreeCellEditor.this.
getFont() == null) {
Dimension rSize =
renderer.
getPreferredSize();
size.
height =
rSize.
height;
}
return
size;
}
}
/**
* Container responsible for placing the <code>editingComponent</code>.
*/
public class
EditorContainer extends
Container {
/**
* Constructs an <code>EditorContainer</code> object.
*/
public
EditorContainer() {
setLayout(null);
}
// This should not be used. It will be removed when new API is
// allowed.
public void
EditorContainer() {
setLayout(null);
}
/**
* Overrides <code>Container.paint</code> to paint the node's
* icon and use the selection color for the background.
*/
public void
paint(
Graphics g) {
int
width =
getWidth();
int
height =
getHeight();
// Then the icon.
if(
editingIcon != null) {
int
yLoc =
calculateIconY(
editingIcon);
if (
getComponentOrientation().
isLeftToRight()) {
editingIcon.
paintIcon(this,
g, 0,
yLoc);
} else {
editingIcon.
paintIcon(
this,
g,
width -
editingIcon.
getIconWidth(),
yLoc);
}
}
// Border selection color
Color background =
getBorderSelectionColor();
if(
background != null) {
g.
setColor(
background);
g.
drawRect(0, 0,
width - 1,
height - 1);
}
super.paint(
g);
}
/**
* Lays out this <code>Container</code>. If editing,
* the editor will be placed at
* <code>offset</code> in the x direction and 0 for y.
*/
public void
doLayout() {
if(
editingComponent != null) {
int
width =
getWidth();
int
height =
getHeight();
if (
getComponentOrientation().
isLeftToRight()) {
editingComponent.
setBounds(
offset, 0,
width -
offset,
height);
} else {
editingComponent.
setBounds(
0, 0,
width -
offset,
height);
}
}
}
/**
* Calculate the y location for the icon.
*/
private int
calculateIconY(
Icon icon) {
// To make sure the icon position matches that of the
// renderer, use the same algorithm as JLabel
// (SwingUtilities.layoutCompoundLabel).
int
iconHeight =
icon.
getIconHeight();
int
textHeight =
editingComponent.
getFontMetrics(
editingComponent.
getFont()).
getHeight();
int
textY =
iconHeight / 2 -
textHeight / 2;
int
totalY =
Math.
min(0,
textY);
int
totalHeight =
Math.
max(
iconHeight,
textY +
textHeight) -
totalY;
return
getHeight() / 2 - (
totalY + (
totalHeight / 2));
}
/**
* Returns the preferred size for the <code>Container</code>.
* This will be at least preferred size of the editor plus
* <code>offset</code>.
* @return a <code>Dimension</code> containing the preferred
* size for the <code>Container</code>; if
* <code>editingComponent</code> is <code>null</code> the
* <code>Dimension</code> returned is 0, 0
*/
public
Dimension getPreferredSize() {
if(
editingComponent != null) {
Dimension pSize =
editingComponent.
getPreferredSize();
pSize.
width +=
offset + 5;
Dimension rSize = (
renderer != null) ?
renderer.
getPreferredSize() : null;
if(
rSize != null)
pSize.
height =
Math.
max(
pSize.
height,
rSize.
height);
if(
editingIcon != null)
pSize.
height =
Math.
max(
pSize.
height,
editingIcon.
getIconHeight());
// Make sure width is at least 100.
pSize.
width =
Math.
max(
pSize.
width, 100);
return
pSize;
}
return new
Dimension(0, 0);
}
}
}