/*
* 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.
*
* <p>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 = {};
/** empty array of property sets */
private static final PropertySet[] NO_PROPERTY_SETS = {};
/** 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, <code>{0}</code>, which will be filled by a value from <CODE>getName()</CODE>.
*
* <p>The default format just uses the simple name; subclasses may
* change it, though it will not take effect until the next {@link #setName} call.
*
* <p>Can be set to <CODE>null</CODE>. 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
* <link>getCookieSet</link> and <link>setCookieSet</link> they will
* throw an exception.
*
* @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.
*
* <p>For example, for the base <code>resource/MyIcon</code>, the
* following images may be used according to the icon state and
* {@link java.beans.BeanInfo#getIcon presentation type}:
*
* <ul><li><code>resource/MyIcon.gif</code><li><code>resource/MyIconOpen.gif</code>
* <li><code>resource/MyIcon32.gif</code><li><code>resource/MyIconOpen32.gif</code></ul>
*
* <P>
* 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 () {
return Utilities.loadImage(DEFAULT_ICON);
}
/** Can this node be renamed?
* @return <code>false</code>
*/
public boolean canRename () {
return false;
}
/** Can this node be destroyed?
* @return <CODE>false</CODE>
*/
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.
* <P>
* <em>Warning:</em> Do not call <code>getSheet</code> in this method.
* <P>
* The default implementation returns an empty sheet.
*
* @return the sheet with initialized values (never <code>null</code>)
*/
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 <code>null</code>)
*/
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 ();
if (s == null) return NO_PROPERTY_SETS;
return s.toArray ();
}
/** 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 <code>true</code>
*/
public boolean canCopy () {
return true;
}
/** Can this node be cut?
* @return <code>false</code>
*/
public boolean canCut () {
return false;
}
/** Accumulate the paste types that this node can handle
* for a given transferable.
* <P>
* 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.
* <p>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 <code>AbstractNode</code> 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;
}
/** Gets preferred action.
* By default, null.
* @return preferred action
* @see Node#getPreferredAction
* @since 3.29
*/
public Action getPreferredAction() {
return 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 <code>null</code> 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
*/
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 <code>null</code>.
* <p><em>Warning:</em> do not call {@link #getActions} within this method.
* @return array of actions for this node, or <code>null</code> to use the default node actions
*/
protected SystemAction[] createActions () {
return null;
}
/** Does this node have a customizer?
* @return <CODE>false</CODE>
*/
public boolean hasCustomizer () {
return false;
}
/** Get the customizer.
* @return <code>null</code> 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 <code>null</code>)
* @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 <code>null</code>
*/
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);
}
}