/* * 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); } }