/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is NetBeans. The Initial Developer of the Original
* Code is Sun Microsystems, Inc. Portions Copyright 1997-2004 Sun
* Microsystems, Inc. All Rights Reserved.
*/
package org.openide.explorer.propertysheet;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.KeyboardFocusManager;
import java.awt.LayoutManager;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.beans.FeatureDescriptor;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyEditor;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import org.openide.nodes.Node;
import org.openide.util.NbBundle;
import javax.swing.*;
import javax.swing.event.ChangeListener;
import org.netbeans.modules.openide.explorer.TTVEnvBridge;
/**
PropertyPanel is a generic GUI component for displaying and editing a JavaBeans™
* property or any compatible getter/setter pair for which there is a property editor
* available, in accordance with the JavaBeans specification. It makes it possible to
* instantiate an appropriate GUI component for a property and provides the plumbing
* between user interation with the gui component and calls to the getter/setter pair
* to update the value.
*
*
The simplest way to use PropertyPanel is by driving it from an instance of
* PropertySupport.Reflection. To do that, simply pass the name of the
* property and an object with a getter/setter pair matching that property to the
* PropertySupport.Reflection's constructor, and pass the resulting instance of
* PropertySupport.Reflection to the PropertyPanel constructor.
*
*
A more efficient approach is to implement Node.Property or pass an existing Node.Property
* object to the PropertyPanel's constructor or PropertyPanel.setProperty - thus
* bypassing the use of reflection to locate the getter and setter.
*
*
A note on uses of Node.Property and PropertyModel: PropertyPanel was
* originally designed to work with instances of PropertyModel, and has since been
* rewritten to be driven by instances of Node.Property. The main reason for this
* is simplification - there is considerable overlap between PropertyModel and
* Node.Property; particularly, DefaultPropertyModel and PropertySupport.Reflection
* effectively are two ways of doing exactly the same thing.
*
*
Use of PropertyModel is still supported, but discouraged except under special
* circumstances. The one significant difference between Node.Property
* and PropertyModel is that PropertyModel permits listening for changes.
*
It is generally accepted that GUI components whose contents unexpectedly change
* due to events beyond their control does not tend to lead to quality, usable user
* interfaces. However, there are cases where a UI will, for example, contain several
* components and modification to one should immediately be reflected in the other.
* For such a case, use of PropertyModel is still supported. For other cases,
* it makes more sense to use BeanNode and for the designer of the UI
* to make a design choice as to how to handle (if at all) unexpected changes happening to
* properties it is displaying. If all you need to do is display or edit a
* property, use one of the constructors that takes a Node.Property object or
* use setProperty. PropertyModel will be deprecated at some point
* in the future, when a suitable replacement more consistent with
* Node.Property is created.
*
* PropertyModel and displays an editor component for it.
* @author Jaroslav Tulach, Petr Hamernik, Jan Jancura, David Strupl, Tim Boudreau
*/
public class PropertyPanel extends JComponent implements javax.accessibility.Accessible {
/** Constant defining a preference for rendering the value.
* Value should be displayed in read-only mode.
*/
public static final int PREF_READ_ONLY = 0x0001;
/** Constant defining a preference for rendering the value.
* Value should be displayed in custom editor.
*/
public static final int PREF_CUSTOM_EDITOR = 0x0002;
/** Constant defining a preference for rendering the value.
* Value should be displayed in editor only.
*/
public static final int PREF_INPUT_STATE = 0x0004;
/** Constant defining a preference for a borderless UI suitable for
* use in a table */
public static final int PREF_TABLEUI = 0x0008;
/** Name of the 'preferences' property. */
public static final String PROP_PREFERENCES = "preferences"; // NOI18N
/** Name of the 'model' property. */
public static final String PROP_MODEL = "model"; // NOI18N
/** Name of the read-only property 'propertyEditor'.
* @deprecated - the property editor is re-fetched from the underlying
* property object as needed. It is up to the property object to
* cache or not cache the property editor. This property will no longer
* be fired. */
public static final String PROP_PROPERTY_EDITOR = "propertyEditor"; // NOI18N
/** Name of property 'state' that describes the state of the embeded PropertyEditor.
* @see PropertyEnv#getState
* @since 2.20 */
public static final String PROP_STATE = PropertyEnv.PROP_STATE;
/** Holds value of property preferences. */
private int preferences;
/** Holds value of property model. */
private PropertyModel model;
/**
* If this is true the changes made in the property editor
* are immediately propagated to the value of the property
* (to the property model). */
private boolean changeImmediate = true;
/** The inner component, either a custom property editor, an
* InplaceEditor's component or null, depending on the mode and state */
Component inner=null;
/** Listener that will listen for changes in model, editor, env */
private Listener listener=null;
/** The property which will drive the PropertyPanel */
private Node.Property prop;
/** Flag to avoid an endless loop when setProperty is called by setModel */
private boolean settingModel=false;
/** Creates new PropertyPanel backed by a dummy property */
public PropertyPanel () {
this (ModelProperty.toProperty(null), 0, null);
}
/**
* Creates new PropertyPanel with DefaultPropertyModel
* @param preferences the preferences that affect how this propertypanel
* will operate
* @param bean The instance of bean
* @param propertyName The name of the property to be displayed
*/
public PropertyPanel (
Object bean,
String propertyName,
int preferences
) {
//XXX inefficient, get DefaultPropertyModel out of the loop
this (
ModelProperty.toProperty(new DefaultPropertyModel (bean,
propertyName)), preferences,
//XXX can probably subst null for below
new DefaultPropertyModel(bean, propertyName));
}
/** Creates a new PropertyPanel. While not quite deprecated, do not
* use this constructor if your intention is to display a Node.Property
* object; use the constructor that takes a Node.Property object directly
* instead.
* @param model The model to display
* @see org.openide.explorer.propertysheet.PropertyModel
*/
public PropertyPanel (PropertyModel model, int preferences) {
this(null, preferences, model);
}
/**
* Create a new property panel for the specified property with the
* specified preferences.
* @param p
* @param preferences
*/
public PropertyPanel(Node.Property p, int preferences) {
this(p, preferences, null);
}
/**
* Create a new property panel for displaying/editing the specified
* property
* @param p A Property object for this node to represent
* @see org.openide.nodes.Node.Property
*/
public PropertyPanel(Node.Property p) {
this(p, 0, null);
}
/** Create a property panel that displays a property belonging to several
* nodes. This is useful e.g. for TreeTableView. The property panel will
* display the standard "different values" condition for cases
* where the value of various properties does not match.
*
* Note that this method assumes that none of the nodes will have two
* different property sets each containing a property with the requested
* name. The behavior of this constructor is undefined in this case,
* and will likely result in a ClassCastException.
*
* This constructor is fail-fast, and will preemptively check that the
* properties to be proxied indeed exist and are of the same type.
*
* @param nodes The nodes that will supply properties
* @param propertyName The name of the property to look for
* @throws ClassCastException if the named property for one of the nodes
* has a different value type than for others
* @throws NullPointerException if even one of the nodes does not have
* a property of the given name.
*/
PropertyPanel (Node[] nodes, String propertyName) throws ClassCastException, NullPointerException {
//Protected for now - TreeTableView can use it
this (nodes.length == 1 ?
ModelProperty.findProperty(nodes[0], propertyName)
: ModelProperty.toProperty(nodes, propertyName));
}
/** The constructor all the other constructors call */
private PropertyPanel(Node.Property p, int preferences, PropertyModel mdl) {
if (p == null) {
prop = ModelProperty.toProperty(mdl);
} else {
prop = p;
}
this.preferences = preferences;
initializing = true;
setModel(mdl);
initializing = false;
setOpaque(true);
//for debugging, allow CTRL-. to dump the state to stderr
getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_PERIOD, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "dump");
getActionMap().put("dump", new AbstractAction() { //NOI18N
public void actionPerformed(ActionEvent ae) {
System.err.println(""); //NOI18N
System.err.println(PropertyPanel.this);
System.err.println(""); //NOI18N
}
});
//#44226 - Unpretty, but this allows the TreeTableView to invoke a custom editor dialog when
//necessary - with the TTV rewrite, all cell editor infrastructure will be moved to
//org.netbeans.modules.openide.explorer, and they will simply share editor classes. Since that
//involves an API change (some package private methods of PropertyEnv need to be accessible to
//the editor classes), this will have to wait for after 4.0 - Tim
getActionMap().put("invokeCustomEditor", new CustomEditorProxyAction()); //NOI18N
}
/**
* Held in action map to allow TreeTableView to invoke the custom editor over read-only cells
*/
private class CustomEditorProxyAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
Action wrapped = getWrapped();
if (wrapped != null) {
wrapped.actionPerformed (e);
} else {
Toolkit.getDefaultToolkit().beep();
}
}
private Action getWrapped() {
Node.Property p = getProperty();
EditablePropertyDisplayer pd =
getPropertyDisplayer() instanceof EditablePropertyDisplayer ?
(EditablePropertyDisplayer) getPropertyDisplayer() :
new EditablePropertyDisplayer(p);
return pd.getCustomEditorAction();
}
public boolean isEnabled() {
Action wrapped = getWrapped();
if (wrapped != null) {
return wrapped.isEnabled();
} else {
return getProperty() != null;
}
}
}
private boolean initializing = false;
public void setBackground(Color c) {
if (inner != null) {
inner.setBackground(c);
}
super.setBackground(c);
}
public void setForeground(Color c) {
if (inner != null) {
inner.setForeground(c);
}
super.setForeground(c);
}
/** Returns an appropriate property displayer instance depending on the
* preferences. For non-editable modes, will use a lightweight, near-stateless
* RendererPropertyDisplayer component */
private PropertyDisplayer findPropertyDisplayer() {
PropertyDisplayer result;
Node.Property prop = getProperty();
if ((preferences & PREF_CUSTOM_EDITOR) == 0 && ((preferences &
PREF_READ_ONLY) != 0 || !isEnabled())) {
//Always use a renderer if we're inline and non-editable
return getRendererComponent(prop);
}
switch (preferences) {
case 9 :
case 1 : //PREF_READ_ONLY
result = getRendererComponent (prop);
break;
case 10 :
case 2 : //PREF_CUSTOM_EDITOR
result = new CustomEditorDisplayer(prop, model);
break;
case 11:
case 3 : //PREF_CUSTOM_EDITOR & PREF_READ_ONLY
result = new CustomEditorDisplayer(prop, model);
//XXX remember to set enabled on the components
break;
case 12:
case 4 : //PREF_INPUT_STATE
result = new EditablePropertyDisplayer(prop, model);
break;
case 13:
case 5 : //PREF_INPUT_STATE & PREF_READ_ONLY
result = getRendererComponent (prop);
break;
case 14:
case 6 : //PREF_INPUT_STATE & PREF_CUSTOM_EDIITOR
result = new CustomEditorDisplayer(prop, model);
//Only difference with this combination is it should display
//an error dialog on commit if the entered value is bad
break;
case 15:
case 7 : //PREF_INPUT_STATE & PREF_CUSTOM_EDITOR & PREF_READ_ONLY
result = new CustomEditorDisplayer(prop, model);
break;
case 0 :
case 8 :
default :
result = new EditablePropertyDisplayer(prop, model);
break;
}
if (result instanceof PropertyDisplayer_Inline) {
PropertyDisplayer_Inline inline = (PropertyDisplayer_Inline) result;
boolean tableUI = (preferences & PREF_TABLEUI) != 0 ||
Boolean.TRUE.equals(getClientProperty("flat")); //NOI18N
inline.setTableUI(tableUI); //NOI18N
if (inline.isTableUI()) {
inline.setUseLabels(!tableUI);
}
}
boolean isTableUI = (preferences & PREF_TABLEUI) != 0;
if (result instanceof CustomEditorDisplayer) {
((PropertyDisplayer_Editable) result).setUpdatePolicy(changeImmediate ? PropertyDisplayer.UPDATE_ON_FOCUS_LOST : PropertyDisplayer.UPDATE_ON_EXPLICIT_REQUEST);
} else if (result instanceof PropertyDisplayer_Editable) {
((PropertyDisplayer_Editable) result).setUpdatePolicy(isTableUI ? PropertyDisplayer.UPDATE_ON_CONFIRMATION : PropertyDisplayer.UPDATE_ON_FOCUS_LOST);
}
if ((preferences & PREF_READ_ONLY) != 0 && result instanceof CustomEditorDisplayer) {
((CustomEditorDisplayer) result).setEnabled(false);
} else if (result instanceof PropertyDisplayer_Editable) {
if (!isEnabled()) {
((PropertyDisplayer_Editable)result).setEnabled(isEnabled());
}
}
return result;
}
/** Convenience method to allow reuse of mutable renderer components -
* will either create a renderer or reuse the current displayer if there
* is one and it's a renderer */
private RendererPropertyDisplayer getRendererComponent(Node.Property prop) {
RendererPropertyDisplayer result;
if (inner instanceof RendererPropertyDisplayer) {
//re-use the one we already have if possible
((RendererPropertyDisplayer) inner).setProperty(prop);
result = (RendererPropertyDisplayer) inner;
} else {
result = new RendererPropertyDisplayer(prop);
}
return result;
}
private PropertyDisplayer displayer=null;
/** Fetch the PropertyDisplayer which will do the actual work of displaying
* the property. It will be created if necessary */
private PropertyDisplayer getPropertyDisplayer() {
if (displayer == null) {
setDisplayer (findPropertyDisplayer());
}
return displayer;
}
/** Installs the component we will embed to display the property */
private void installDisplayerComponent() {
//Fetch or instantiate the component we will embed to display the
//property. Depending on the prefs, it may be a RendererPropertyDisplayer,
//an EditablePropertyDisplayer or a CustomPropertyDisplayer.
PropertyDisplayer displayer = getPropertyDisplayer();
//Find who has focus now, so if we have focus, focus won't end up set
//to null
Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
boolean hadFocus = focusOwner == this || isAncestorOf(focusOwner);
if (hadFocus) {
//If we had focus, clear the global focus owner for now, so that
//when the existing component is removed, it does not cause
//focus to get briefly set to a random component
KeyboardFocusManager.getCurrentKeyboardFocusManager().clearGlobalFocusOwner();
}
//Fetch the new inner component (the custom editor or Inplace editor)
Component newInner = displayer.getComponent();
//Set the enabled state appropriately. For implementations of
//PropertyDisplayer_Editable, this will already be handled; for render-
//only cases, it should be handled explicitly
if (!(displayer instanceof PropertyDisplayer_Editable)) {
//only for renderers
newInner.setEnabled(isEnabled() && getProperty().canWrite());
}
newInner.setForeground(getForeground());
newInner.setBackground(getBackground());
//Make sure the inner component has really changed
if (newInner != inner) {
synchronized (getTreeLock()) {
//remove the odl component
if (inner != null) {
remove (inner);
}
//and add the new one (if any)
if (newInner != null) {
add (newInner);
//invalidate its layout so it will be re-laid out
newInner.invalidate();
inner = newInner;
}
}
}
//Force a re-layout immediately if visible
if (isShowing() && !(getParent() instanceof javax.swing.CellRendererPane)) {
validate();
}
//Restore focus if necessary
if (hadFocus && isEnabled() && (preferences & PREF_READ_ONLY) == 0) {
requestFocus();
}
//Simply adding a component to a container can sometimes cause it to be
//given focus even though it's not focusable. If this has happened,
//find the next component in the focus cycle root and force focus to that.
//Mainly a problem with JFileChooser, but we also have a few property
//editors that force focus on addNotify which should be fixed
if (!isEnabled() || (preferences & PREF_READ_ONLY) != 0) {
Component focus =
KeyboardFocusManager.getCurrentKeyboardFocusManager().
getFocusOwner();
if (focus == inner || (
(inner instanceof Container) &&
((Container) inner).isAncestorOf(focus))) {
this.transferFocusUpCycle();
}
}
}
public void doLayout() {
layout();
}
public void layout() {
if (inner != null) {
inner.setBounds(0,0, getWidth(), getHeight());
}
}
public Dimension getMinimumSize() {
return getPreferredSize();
}
public Dimension getPreferredSize() {
Dimension result;
if (!isDisplayable() && (preferences & PREF_CUSTOM_EDITOR) == 0) {
//XXX use rendererfactory to make this more efficient and just
//configure a shared renderer instacne with the property & fetch size
result = getRendererComponent(
getProperty()).getComponent().getPreferredSize();
} else if (inner != null) {
result = inner.getPreferredSize();
} else {
result = PropUtils.getMinimumPanelSize();
}
return result;
}
/** Sets the property displayer we are using to display the property,
* detaching listeners, etc */
private void setDisplayer (PropertyDisplayer nue) {
if (displayer != null) {
detachFromDisplayer(displayer);
}
displayer = nue;
if (nue != null) {
attachToDisplayer(displayer);
}
}
/** Attach any necessary listeners to the property displayer to be used */
private void attachToDisplayer (PropertyDisplayer displayer) {
if (displayer instanceof PropertyDisplayer_Inline) {
updateDisplayerFromClientProps();
}
if (displayer instanceof CustomEditorDisplayer) {
((CustomEditorDisplayer) displayer).setRemoteEnvListener(getListener());
((CustomEditorDisplayer) displayer).setRemoteEnvVetoListener(getListener());
}
if (displayer instanceof EditablePropertyDisplayer) {
((EditablePropertyDisplayer) displayer).setRemoteEnvListener(getListener());
((EditablePropertyDisplayer) displayer).setRemoteEnvVetoListener(getListener());
PropertyEnv env =
((EditablePropertyDisplayer) displayer).getPropertyEnv();
if (env != null) {
env.setFeatureDescriptor(getProperty());
}
}
}
/** Remove any listeners and dispose any state relating to a displayer
* we are no longer interested in */
private void detachFromDisplayer (PropertyDisplayer displayer) {
if (displayer instanceof CustomEditorDisplayer) {
((CustomEditorDisplayer) displayer).setRemoteEnvVetoListener(null);
}
if (displayer instanceof EditablePropertyDisplayer) {
((EditablePropertyDisplayer) displayer).setRemoteEnvVetoListener(null);
}
if (displayer instanceof PropertyDisplayer_Editable) {
((PropertyDisplayer_Editable) displayer).dispose();
}
}
/** Overridden to catch changes in those client properties that are
* relevant to PropertyPanel */
protected void firePropertyChange(String nm, Object old, Object nue) {
if (("flat".equals(nm) || "radioButtonMax".equals(nm) ||
"suppressCustomEditor".equals(nm) || "useLabels".equals(nm))
&& displayer != null &&
displayer instanceof PropertyDisplayer_Inline) { //NOI18N
updateDisplayerFromClientProp(nm, nue);
}
super.firePropertyChange(nm, old, nue);
}
/** Update the current property displayer based on previously set client
* properties */
private void updateDisplayerFromClientProp (String nm, Object val) {
PropertyDisplayer displayer = getPropertyDisplayer();
if (displayer instanceof PropertyDisplayer_Inline) {
PropertyDisplayer_Inline inline = (PropertyDisplayer_Inline) displayer;
if ("flat".equals(nm)) { //NOI18N
inline.setTableUI(Boolean.TRUE.equals(val));
if (Boolean.TRUE.equals(val)) {
inline.setUseLabels(false);
} else if (Boolean.FALSE.equals(val) &&
getClientProperty("useLabels") == null) { //NOI18N
inline.setUseLabels(true);
}
} else if ("radioButtonMax".equals(nm)) { //NOI18N
int max = val instanceof Integer ? ((Integer) val).intValue() :
0;
inline.setRadioButtonMax(max);
} else if ("suppressCustomEditor".equals(nm)) { //NOI18N
inline.setShowCustomEditorButton(!Boolean.TRUE.equals(val));
} else if ("useLabels".equals(nm)) { //NOI18N
inline.setUseLabels(Boolean.TRUE.equals(val));
}
}
}
/** Overridden to return false in cases that the preferences specify a
* read-only state */
public boolean isFocusable() {
return super.isFocusable() && isEnabled() && (preferences & PREF_READ_ONLY) == 0;
}
/** Overridden to do
* nothing in a read only state, since some custom property editors (File
* chooser) are capable of receiving focus even if they are disabled,
* effectively making focus disappear */
public void requestFocus() {
//Do this because even if everything is disabled, JFileChooser's UI
//*does* supply some focusable components
if (!isEnabled() || (preferences & PREF_READ_ONLY) != 0) {
return;
} else if (inner != null && inner.isEnabled()) {
super.requestFocus();
inner.requestFocus();
}
}
/** In the case that some client properties may have been set before a
* PropertyRenderer was added, set up its values accordingly. */
private void updateDisplayerFromClientProps() {
String[] props = new String[] {"flat", "radioButtonMax",
"suppressCustomEditor", "useLabels"}; //NOI18N
for (int i=0; i < props.length; i++) {
Object o = getClientProperty(props[i]);
if (o != null) {
updateDisplayerFromClientProp(props[i], o);
}
}
}
protected void processFocusEvent (FocusEvent fe) {
super.processFocusEvent(fe);
if (fe.getID() == fe.FOCUS_GAINED) {
if (inner != null && inner.isEnabled() && inner.isFocusTraversable()) {
inner.requestFocus();
}
}
}
/** Lazily create the listener for listening to the property editor, env
* and model */
private Listener getListener() {
if (listener == null) {
listener = new Listener();
}
return listener;
}
/** Overridden to install the inner component that will display the property*/
public void addNotify() {
attachToModel();
if (displayer != null) {
attachToDisplayer(displayer);
}
if (inner == null) {
installDisplayerComponent();
}
super.addNotify();
}
/** Overridden to dispose the component that actually displays the property
* and any state information associated with it */
public void removeNotify() {
super.removeNotify();
detachFromModel();
if (displayer != null && (!(displayer instanceof RendererPropertyDisplayer))) {
detachFromDisplayer(displayer);
displayer = null;
}
if (!(inner instanceof RendererPropertyDisplayer)) {
//Renderers hold no references the property panel doesn't, so avoid
//creating a new one for performance reasons in TTV - PropertyPanel
//will be repeatedly added to and removed from a CellRendererPane
remove(inner);
inner = null;
}
}
/*
public Dimension getPreferredSize() {
Dimension result;
if (!isDisplayable() && (preferences & PREF_CUSTOM_EDITOR) == 0) {
//XXX use rendererfactory to make this more efficient and just
//configure a shared renderer instacne with the property & fetch size
result = getRendererComponent(
getProperty()).getComponent().getPreferredSize();
} else if (inner != null) {
result = inner.getPreferredSize();
} else {
result = super.getPreferredSize();
}
return result;
}
public Dimension getMinimumSize() {
return getPreferredSize();
}
*/
/** Returns the preferences set for this property panel. The preferences
* determine such things as read-only mode and whether an inline or custom
* editor is displayed
* @return The preferences
*/
public int getPreferences () {
return preferences;
}
/** Setter for visual preferences in displaying
* of the value of the property.
* @param preferences PREF_XXXX constants
*/
public void setPreferences (int preferences) {
if (preferences != this.preferences) {
int oldPreferences = this.preferences;
this.preferences = preferences;
hardReset();
firePropertyChange(PROP_PREFERENCES, oldPreferences,preferences);
}
}
/** Get the property model associated with this property panel. Note that
* while the PropertyModel usages of PropertyPanel are not yet deprecated,
* the preferred and more efficient use of PropertyPanel is directly with
* a Node.Property instance rather than a PropertyModel.