/*
* 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-2003 Sun
* Microsystems, Inc. All Rights Reserved.
*/
package org.openide.nodes;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.awt.Image;
import java.awt.datatransfer.Transferable;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.*;
import javax.swing.Action;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import org.openide.util.HelpCtx;
import org.openide.util.Utilities;
import org.openide.util.Lookup;
import org.openide.util.datatransfer.*;
import org.openide.util.actions.SystemAction;
/** A basic implementation of a node.
*
*
It simplifies creation of the display name, based on a message
* format and the system name. It also simplifies working with icons:
* one need only specify the base name and all icons will be loaded
* when needed. Other common requirements are handled as well.
*
* @author Jaroslav Tulach */
public class AbstractNode extends Node {
/** messages to create a resource identification for each type of
* icon from the base name for the icon.
*/
private static final String[] icons = {
// color 16x16
".gif", // NOI18N
// color 32x32
"32.gif", // NOI18N
// mono 16x16
".gif", // NOI18N
// mono 32x32
"32.gif", // NOI18N
// opened color 16x16
"Open.gif", // NOI18N
// opened color 32x32
"Open32.gif", // NOI18N
// opened mono 16x16
"Open.gif", // NOI18N
// opened mono 32x32
"Open32.gif" // NOI18N
};
/** To index normal icon from previous array use
* + ICON_BASE.
*/
private static final int ICON_BASE = -1;
/** for indexing opened icons */
private static final int OPENED_ICON_BASE = 3;
/** empty array of paste types */
private static final PasteType[] NO_PASTE_TYPES = {};
/** empty array of new types */
private static final NewType[] NO_NEW_TYPES = {};
/** Message format to use for creation of the display name.
* It permits conversion of text from
* {@link #getName} to the one sent to {@link #setDisplayName}. The format can take
* one parameter, {0}, which will be filled by a value from getName().
*
*
The default format just uses the simple name; subclasses may
* change it, though it will not take effect until the next {@link #setName} call.
*
*
Can be set to null. Then there is no connection between the
* name and display name; they may be independently modified. */
protected MessageFormat displayFormat;
/** Preferred action */
private Action preferredAction;
/** default icon base for all nodes */
private static final String DEFAULT_ICON_BASE = "org/openide/resources/defaultNode"; // NOI18N
private static final String DEFAULT_ICON = DEFAULT_ICON_BASE + ".gif"; // NOI18N
/** Resource base for icons (without suffix denoting right icon) */
private String iconBase = DEFAULT_ICON_BASE;
/** array of cookies for this node */
private Object lookup;
/** set of properties to use */
private Sheet sheet;
/** Actions for the node. They are used only for the pop-up menus
* of this node.
*/
protected SystemAction[] systemActions;
/** Listener for changes in the sheet and the cookie set. */
private final class SheetAndCookieListener implements PropertyChangeListener, ChangeListener {
SheetAndCookieListener() {}
public void propertyChange (PropertyChangeEvent ev) {
AbstractNode.this.firePropertySetsChange (null, null);
}
public void stateChanged (ChangeEvent ev) {
AbstractNode.this.fireCookieChange ();
}
}
private SheetAndCookieListener sheetCookieL = null;
/** Create a new abstract node with a given child set.
* @param children the children to use for this node
*/
public AbstractNode(Children children) {
this (children, null);
}
/** Create a new abstract node with a given child set and associated
* lookup. If you use this constructor, please do not call methods
* getCookieSet and setCookieSet they will
* throw an exception.
*
* More info on the correct usage of constructor with Lookup can be found
* in the {@link Node#Node(org.openide.nodes.Children, org.openide.util.Lookup)}
* javadoc.
*
* @param children the children to use for this node
* @param lookup the lookup to provide content of {@link #getLookup}
* and also {@link #getCookie}
* @since 3.11
*/
public AbstractNode (Children children, Lookup lookup) {
super (children, lookup);
// Setting the name to non-null value for the node
// to return "reasonable" name and displayName
// not using this.setName since the descendants
// can override it and might assume that it is
// not called from constructor (see e.g. DataNode)
super.setName(""); // NOI18N
}
/** Clone the node. If the object implements {@link Cloneable},
* that is used; otherwise a {@link FilterNode filter node}
* is created.
*
* @return copy of this node
*/
public Node cloneNode () {
try {
if (this instanceof Cloneable) {
return (Node)clone ();
}
} catch (CloneNotSupportedException ex) {
}
return new FilterNode (this);
}
/** Set the system name. Fires a property change event.
* Also may change the display name according to {@link #displayFormat}.
*
* @param s the new name
*/
public void setName (String s) {
super.setName (s);
MessageFormat mf = displayFormat;
if (mf != null) {
setDisplayName (mf.format (new Object[] { s }));
} else {
// additional hack, because if no display name is set, then it
// is taken from the getName, that means calling setName can
// also change display name
// fix of 10665
fireDisplayNameChange (null, null);
}
}
/** Change the icon.
* One need only specify the base resource name;
* the real name of the icon is obtained by the applying icon message
* formats.
*
*
For example, for the base resource/MyIcon, the
* following images may be used according to the icon state and
* {@link java.beans.BeanInfo#getIcon presentation type}:
*
*
resource/MyIcon.gif
resource/MyIconOpen.gif
*
resource/MyIcon32.gif
resource/MyIconOpen32.gif
*
*
* This method may be used to dynamically switch between different sets
* of icons for different configurations. If the set is changed,
* an icon property change event is fired.
*
* @param base base resouce name (no initial slash)
*/
public void setIconBase (String base) {
this.iconBase = base;
fireIconChange ();
fireOpenedIconChange ();
}
/** Find an icon for this node. Uses an {@link #setIconBase icon set}.
*
* @param type constants from {@link java.beans.BeanInfo}
*
* @return icon to use to represent the bean
*/
public Image getIcon (int type) {
return findIcon (type, ICON_BASE);
}
/** Finds an icon for this node when opened. This icon should represent the node
* only when it is opened (when it can have children).
*
* @param type as in {@link #getIcon}
* @return icon to use to represent the bean when opened
*/
public Image getOpenedIcon (int type) {
return findIcon (type, OPENED_ICON_BASE);
}
public HelpCtx getHelpCtx () {
return HelpCtx.DEFAULT_HELP;
}
/** Tries to find the right icon for the iconbase.
* @param type type of icon (from BeanInfo constants)
* @param ib base where to scan in the array
*/
private Image findIcon (int type, int ib) {
String res = iconBase + icons[type + ib];
Image im = Utilities.loadImage (res);
if (im != null) return im;
// try the first icon
res = iconBase + icons[java.beans.BeanInfo.ICON_COLOR_16x16 + ib];
im = Utilities.loadImage (res);
if (im != null) return im;
if (ib == OPENED_ICON_BASE) {
// try closed icon also
return findIcon (type, ICON_BASE);
}
// if still not found return default icon
return getDefaultIcon ();
}
Image getDefaultIcon () {
Image i = Utilities.loadImage(DEFAULT_ICON);
if (i == null) throw new MissingResourceException("No default icon", "", DEFAULT_ICON); // NOI18N
return i;
}
/** Can this node be renamed?
* @return false
*/
public boolean canRename () {
return false;
}
/** Can this node be destroyed?
* @return false
*/
public boolean canDestroy () {
return false;
}
/** Set the set of properties.
* A listener is attached to the provided sheet
* and any change of the sheet is propagated to the node by
* firing a {@link #PROP_PROPERTY_SETS} change event.
*
* @param s the sheet to use
*/
protected final synchronized void setSheet (Sheet s) {
setSheetImpl(s);
firePropertySetsChange (null, null);
}
private synchronized void setSheetImpl(Sheet s) {
if (sheetCookieL == null) {
sheetCookieL = new SheetAndCookieListener ();
}
if (sheet != null) {
sheet.removePropertyChangeListener (sheetCookieL);
}
s.addPropertyChangeListener (sheetCookieL);
sheet = s;
}
/** Initialize a default
* property sheet; commonly overridden. If {@link #getSheet}
* is called and there is not yet a sheet,
* this method is called to allow a subclass
* to specify its properties.
*
* Warning: Do not call getSheet in this method.
*
* The default implementation returns an empty sheet.
*
* @return the sheet with initialized values (never null)
*/
protected Sheet createSheet () {
return new Sheet ();
}
/** Get the current property sheet. If the sheet has been
* previously set by a call to {@link #setSheet}, that sheet
* is returned. Otherwise {@link #createSheet} is called.
*
* @return the sheet (never null)
*/
protected final synchronized Sheet getSheet () {
if (sheet != null)
return sheet;
setSheetImpl(createSheet());
return sheet;
}
/** Get a list of property sets.
*
* @return the property sets for this node
* @see #getSheet
*/
public PropertySet[] getPropertySets () {
Sheet s = getSheet ();
return s.toArray ();
}
boolean propertySetsAreKnown() {
return (sheet != null);
}
/** Copy this node to the clipboard.
*
* @return {@link org.openide.util.datatransfer.ExTransferable.Single} with one copy flavor
* @throws IOException if it could not copy
* @see NodeTransfer
*/
public Transferable clipboardCopy () throws IOException {
return NodeTransfer.transferable (this, NodeTransfer.CLIPBOARD_COPY);
}
/** Cut this node to the clipboard.
*
* @return {@link org.openide.util.datatransfer.ExTransferable.Single} with one cut flavor
* @throws IOException if it could not cut
* @see NodeTransfer
*/
public Transferable clipboardCut () throws IOException {
return NodeTransfer.transferable (this, NodeTransfer.CLIPBOARD_CUT);
}
/**
* This implementation only calls clipboardCopy supposing that
* copy to clipboard and copy by d'n'd are similar.
*
* @return transferable to represent this node during a drag
* @exception IOException when the
* cut cannot be performed
*/
public Transferable drag () throws IOException {
return clipboardCopy ();
}
/** Can this node be copied?
* @return true
*/
public boolean canCopy () {
return true;
}
/** Can this node be cut?
* @return false
*/
public boolean canCut () {
return false;
}
/** Accumulate the paste types that this node can handle
* for a given transferable.
*
* The default implementation simply tests whether the transferable supports
* intelligent pasting via {@link NodeTransfer#findPaste}, and if so, it obtains the paste types
* from the {@link NodeTransfer.Paste transfer data} and inserts them into the set.
*
Subclass implementations should typically call super (first or last) so that they
* add to, rather than replace, a superclass's available paste types; especially as the
* default implementation in AbstractNode is generally desirable to retain.
*
* @param t a transferable containing clipboard data
* @param s a list of {@link PasteType}s that will have added to it all types
* valid for this node (ordered as they will be presented to the user)
*/
protected void createPasteTypes (Transferable t, List s) {
NodeTransfer.Paste p = NodeTransfer.findPaste (t);
if (p != null) {
// adds all its types into the set
s.addAll (Arrays.asList (p.types (this)));
}
}
/** Determine which paste operations are allowed when a given transferable is in the clipboard.
* Subclasses should override {@link #createPasteTypes}.
*
* @param t the transferable in the clipboard
* @return array of operations that are allowed
*/
public final PasteType[] getPasteTypes (Transferable t) {
List s = new LinkedList ();
createPasteTypes (t, s);
return (PasteType[])s.toArray (NO_PASTE_TYPES);
}
/** Default implementation that tries to delegate the implementation
* to the createPasteTypes method. Simply calls the method and
* tries to take the first provided argument. Ignores the action
* argument and index.
*
* @param t the transferable
* @param action the drag'n'drop action to do DnDConstants.ACTION_MOVE, ACTION_COPY, ACTION_LINK
* @param index index between children the drop occured at or -1 if not specified
* @return null if the transferable cannot be accepted or the paste type
* to execute when the drop occures
*/
public PasteType getDropType (Transferable t, int action, int index) {
java.util.List s = new LinkedList ();
createPasteTypes (t, s);
return s.isEmpty () ? null : (PasteType)s.get (0);
}
/* List new types that can be created in this node.
* @return new types
*/
public NewType[] getNewTypes () {
return NO_NEW_TYPES;
}
private static final WeakHashMap overridesGetDefaultAction = new WeakHashMap (27);
/** Checks whether subclass overrides a method
*/
private boolean overridesAMethod (String name, Class[] arguments) {
// we are subclass of AbstractNode
try {
java.lang.reflect.Method m = getClass ().getMethod (name, arguments);
if (m.getDeclaringClass () != AbstractNode.class) {
// ok somebody overriden the method
return true;
}
} catch (NoSuchMethodException ex) {
org.openide.ErrorManager.getDefault().notify(ex);
}
return false;
}
/** Gets preferred action.
* By default, null.
* @return preferred action
* @see Node#getPreferredAction
* @since 3.29
*/
public Action getPreferredAction() {
boolean delegate = false;
Class c = getClass ();
if (c != AbstractNode.class) {
synchronized (overridesGetDefaultAction) {
Object in = overridesGetDefaultAction.get (c);
if (in == this) {
// catched in a loop of overriding getDefaultAction and
// calling super.getDefaultAction
// pretend that we do not override
overridesGetDefaultAction.put (c, Boolean.FALSE);
return preferredAction;
}
Boolean b;
if (in == null) {
b = overridesAMethod ("getDefaultAction", new Class[0] ) ? Boolean.TRUE : Boolean.FALSE; // NOI18N
if (b.booleanValue()) {
// check whether it is safe to call the getDefaultAction
overridesGetDefaultAction.put (c, this);
getDefaultAction ();
if (overridesGetDefaultAction.get (c) == this) {
// value unchanged, we have not been cought in a loop
overridesGetDefaultAction.put (c, b);
}
} else {
overridesGetDefaultAction.put (c, b);
}
} else {
b = (Boolean)in;
}
delegate = b.booleanValue();
}
}
return delegate ? getDefaultAction () : preferredAction;
}
/** Gets the default action. Overrides superclass method.
* @return if there is a default action set, then returns it
* @deprecated Use {@link #getPreferredAction} instead.
*/
public SystemAction getDefaultAction () {
Action a = getPreferredAction();
if(a instanceof SystemAction) {
return (SystemAction)a;
}
return null;
}
/** Set a default action for the node.
* @param action the new default action, or null for none
* @deprecated Override {@link #getPreferredAction} instead.
*/
public void setDefaultAction (SystemAction action) {
preferredAction = action;
}
/** Get all actions for the node.
* Initialized with {@link #createActions}, or with the superclass's list.
*
* @return actions for the node
* @deprecated Override {@link #getActions(boolean)} instead.
*/
public SystemAction[] getActions () {
if (systemActions == null) {
systemActions = createActions ();
if (systemActions == null) {
systemActions = super.getActions ();
}
}
return systemActions;
}
/** Lazily initialize set of node's actions (overridable).
* The default implementation returns null.
*
Warning: do not call {@link #getActions} within this method.
* @return array of actions for this node, or null to use the default node actions
* @deprecated Override {@link #getActions(boolean)} instead.
*/
protected SystemAction[] createActions () {
return null;
}
/** Does this node have a customizer?
* @return false
*/
public boolean hasCustomizer () {
return false;
}
/** Get the customizer.
* @return null in the default implementation
*/
public java.awt.Component getCustomizer () {
return null;
}
/** Set the cookie set.
* A listener is attached to the provided cookie set,
* and any change of the sheet is propagated to the node by
* firing {@link #PROP_COOKIE} change events.
*
* @param s the cookie set to use
* @deprecated just use getCookieSet().add(...) instead
* @exception IllegalStateException If you pass a Lookup instance into the constructor, this
* method cannot be called.
*/
protected final synchronized void setCookieSet (CookieSet s) {
if (internalLookup (false) != null) {
throw new IllegalStateException ("CookieSet cannot be used when lookup is associated with the node"); // NOI18N
}
if (sheetCookieL == null) {
sheetCookieL = new SheetAndCookieListener ();
}
CookieSet cookieSet = (CookieSet)lookup;
if (cookieSet != null) {
cookieSet.removeChangeListener (sheetCookieL);
}
s.addChangeListener (sheetCookieL);
lookup = s;
fireCookieChange ();
}
/** Get the cookie set.
*
* @return the cookie set created by {@link #setCookieSet}, or an empty set (never null)
* @exception IllegalStateException If you pass a Lookup instance into the constructor, this
* method cannot be called.
*/
protected final CookieSet getCookieSet () {
if (internalLookup (false) != null) {
throw new IllegalStateException ("CookieSet cannot be used when lookup is associated with the node"); // NOI18N
}
CookieSet s = (CookieSet)lookup;
if (s != null) return s;
synchronized (this) {
if (lookup != null) return (CookieSet)lookup;
// sets empty sheet and adds a listener to it
setCookieSet (new CookieSet ());
return (CookieSet)lookup;
}
}
/** Get a cookie from the node.
* Uses the cookie set as determined by {@link #getCookieSet}.
*
* @param type the representation class
* @return the cookie or null
*/
public Node.Cookie getCookie (Class type) {
if (lookup instanceof CookieSet) {
CookieSet c = (CookieSet)lookup;
return c.getCookie (type);
} else {
return super.getCookie (type);
}
}
/** Get a serializable handle for this node.
* @return a {@link DefaultHandle} in the default implementation
*/
public Handle getHandle () {
return DefaultHandle.createHandle (this);
}
}