All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openide.nodes.Node Maven / Gradle / Ivy

There is a newer version: RELEASE240
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.openide.nodes;

import java.awt.Image;
import java.awt.datatransfer.Transferable;
import java.beans.FeatureDescriptor;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyEditor;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import javax.swing.JPopupMenu;
import javax.swing.event.EventListenerList;
import org.openide.util.HelpCtx;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.NbBundle;
import org.openide.util.Parameters;
import org.openide.util.Utilities;
import org.openide.util.actions.SystemAction;
import org.openide.util.datatransfer.NewType;
import org.openide.util.datatransfer.PasteType;

/** A node represents one element in a hierarchy of objects (beans).
* It provides all methods that are needed for communication between
* an explorer view and the bean.
* 

* The node has three purposes: *

    *
  1. visually represent the object in the tree hierarchy (i.e. Explorer) *
  2. provide sets of properties for that object (i.e. Component Inspector, Property Sheet) *
  3. offer actions to perform on itself *
*

* Frequently nodes are created to represent DataObjects. * But they may also represent anything to be displayed to the user or manipulated programmatically, * even if they have no data directly stored behind them; for example, a control panel or debugger * breakpoint. *

* There are two listeners in this class: {@link PropertyChangeListener} * and {@link NodeListener} (which extends PropertyChangeListener). The first * is designed to listen on properties that can be returned from * {@link #getPropertySets}, the later for listening on changes in the * node itself (including the name, children, parent, set of properties, * icons, etc.). Be sure to distinguish between these two. *

* The node is cloneable. When a node is cloned, it is initialized * with an empty set of listeners and no parent. The display name and short description * are copied to the new node. The set of properties is shared. *

* Implements {@link org.openide.util.Lookup.Provider} since 3.11. * * @author Jaroslav Tulach, */ public abstract class Node extends FeatureDescriptor implements Lookup.Provider, HelpCtx.Provider { /** An empty leaf node. */ public static final Node EMPTY = new AbstractNode(Children.LEAF); /* here is list of property names that can be changed in the Node object. * These properties can be notified to the NodeListener. */ /** Property for node display name. */ public static final String PROP_DISPLAY_NAME = "displayName"; // NOI18N /** Property for internal (not displayable) name of a node. This name is often used to * look up a node in the hierarchy, so it should be chosen to be simple. */ public static final String PROP_NAME = "name"; // NOI18N /** Property for short description of a node. */ public static final String PROP_SHORT_DESCRIPTION = "shortDescription"; // NOI18N /** Property for the normal (closed) icon of a node. */ public static final String PROP_ICON = "icon"; // NOI18N /** Property for the opened icon of a node. */ public static final String PROP_OPENED_ICON = "openedIcon"; // NOI18N /** Property for a node's parent. */ public static final String PROP_PARENT_NODE = "parentNode"; // NOI18N /** Property for a node's list of property sets. */ public static final String PROP_PROPERTY_SETS = "propertySets"; // NOI18N /** Property for a node's cookie set. */ public static final String PROP_COOKIE = "cookie"; // NOI18N /** Property saying whether the Node is Leaf *@since 3.1 */ public static final String PROP_LEAF = "leaf"; // NOI18N /** Error manager used for logging and its precomputed err.isLoggable(Level.FINE) */ private static final Logger err = Logger.getLogger("org.openide.nodes.Node"); //NOI18N; /** cache of all created lookups */ private static Map> lookups = new WeakHashMap>(37); /** class.property names we have warned about for #31413 */ private static final Set warnedBadProperties = new HashSet(100); /** template for changes in cookies */ private static final Lookup.Template TEMPL_COOKIE = new Lookup.Template(Node.Cookie.class); /** Lock for initialization */ private static final Object INIT_LOCK = new Object(); /** private lock to avoid synchronize on this */ private static final Object LOCK = new Object(); /** children representing parent node (Children or ChildrenArray), * for synchronization reasons must be changed only * under the Children.MUTEX lock */ private Object parent; /** children list, for synch. reasons change only under Children.MUTEX * lock */ Children hierarchy; /** listeners for changes in hierarchy. */ private transient EventListenerList listeners; /** Creates a new node with a given hierarchy of children. * @param h the children to use for this node * @exception IllegalStateException if the children object is already in use by * a different node */ protected Node(Children h) throws IllegalStateException { this(h, null); } /** Creates a new node with a given hierarchy of children and a lookup * providing content for {@link #getCookie} and {@link #getLookup} methods. *

* As the lookup needs to be constructed before Node's constructor is called, * it might not be obvious how to add Node or other objects into it without * type casting. Here is the recommended suggestion that uses public/private * pair of constructors: *

    public MyNode(Children ch, FileObject file) {
        this(ch, file, new InstanceContent());
    }

    /** A private constructor that takes an InstanceContent and
         * uses it as internals for the Node lookup and also allow us
         * to modify the content, for example by adding a reference 
         * to the node itself or any other object we want to represent.
         *
         * @param ch children we wish to use
         * @param file sample object we wish to have in lookup
         * @param content the content created by the first constructor
         */
    private MyNode(Children ch, FileObject file, InstanceContent content) {
        super(ch, new AbstractLookup(content));
        // adds the node to our own lookup
        content.add (this);
        // adds additional items to the lookup
        content.add (file);
    }
    
* * @since 3.11 * @param h the children to use for this node * @param lookup the lookup to provide content of {@link #getLookup} * and also {@link #getCookie} * @exception IllegalStateException if the children object is already in use by * a different node */ protected Node(Children h, Lookup lookup) throws IllegalStateException { this.hierarchy = h; // allow subclasses (FilterNode) to update the lookup lookup = replaceProvidedLookup(lookup); if (lookup != null) { this.listeners = new LookupEventList(lookup); } else { this.listeners = new EventListenerList(); } // attaches to this node h.attachTo(this); } /** Method for subclasses to modify provided lookup before its use. * This implementation does nothing. */ Lookup replaceProvidedLookup(Lookup l) { return l; } /** Method that gives access to internal lookup. * @param init should it be really initialized (ready for queries) or need not be * @return lookup or null */ final Lookup internalLookup(boolean init) { if (listeners instanceof LookupEventList) { return ((LookupEventList) listeners).init(init); } else { return null; } } /** Implements {@link Object#clone} to behave correctly if cloning is desired. * But {@link Cloneable} is not declared by default. *

* The default implementation checks whether the child list implements * Cloneable, and if so, it clones the children. * If the child list does not support cloning, {@link Children#LEAF} is used * instead for the children. The parent of this node is set to null and an empty set * of listeners is attached to the node. * * @return the cloned version of the node * @exception CloneNotSupportedException if the children cannot be cloned * in spite of implementing Cloneable */ @Override protected Object clone() throws CloneNotSupportedException { Node n = (Node) super.clone(); Children hier2; if (hierarchy instanceof Cloneable) { hier2 = (Children) hierarchy.cloneHierarchy(); } else { hier2 = Children.LEAF; } // attach the hierarchy n.hierarchy = hier2; hier2.attachTo(n); // no parent n.parent = null; // empty set of listeners if (listeners instanceof LookupEventList) { n.listeners = new LookupEventList(internalLookup(false)); } else { n.listeners = new EventListenerList(); } return n; } /** Clone the node. The newly created node should reference the same * object as this node does, but it may be added as a child * to a different parent node. Also it should have an empty set of listeners. * In all other respects the node should behave exactly as the * original one does. * * @return copy of this node */ public abstract Node cloneNode(); /** Finds the children we are attached to. * @return children */ private Children getParentChildren() { Object p = this.parent; return (p instanceof ChildrenArray) ? ((ChildrenArray)p).getChildren() : (Children)p; } /** Method that allows Children to change the parent children of * the node when the node is add to a children. * * @param parent the children that wants to contain this node * @param index index that will be assigned to this node * @exception IllegalStateException if this node already belongs to a children */ final void assignTo(Children parent, int index) { synchronized (LOCK) { Children ch = getParentChildren(); if ((ch != null) && (ch != parent)) { String parentNodes = null; String chNodes = null; Throwable t = null; try { parentNodes = Arrays.toString(parent.getNodes()); chNodes = Arrays.toString(ch.getNodes()); } catch (StackOverflowError e) { t = e; StackTraceElement[] from = t.getStackTrace(); StackTraceElement[] arr = new StackTraceElement[Math.min(50, from.length)]; System.arraycopy(from, 0, arr, 0, arr.length); t.setStackTrace(arr); } catch (RuntimeException e) { t = e; } IllegalStateException ex = new IllegalStateException( "Cannot initialize " + index + "th child of node " + parent.getNode() + "; it already belongs to node " + ch.getNode() + " (did you forgot to use cloneNode?)\nChildren of new node: " + parentNodes + "\nChildren of old node: " + chNodes ); // NOI18N if (t != null) { ex.initCause(t); } throw ex; } if (!(this.parent instanceof ChildrenArray)) { this.parent = parent; } } } /** Code that reassigns the reference from to parent from its * Children to its ChildrenArray. */ final void reassignTo(Children currentParent, ChildrenArray itsArray) { synchronized (LOCK) { if ((this.parent != currentParent) && (this.parent != itsArray)) { throw new IllegalStateException( "Unauthorized call to change parent: " + this.parent + " and should be: " + currentParent ); } this.parent = itsArray; } } /** Deassigns the node from a children, when it is removed from * a children. */ final void deassignFrom(Children parent) { synchronized (LOCK) { Children p = getParentChildren(); if (parent != p) { throw new IllegalArgumentException("Deassign from wrong parent. Old: " + p + " Caller: " + parent); //NOI18N } this.parent = null; } } /** Set the system name. Fires a property change event. * @param s the new name * @exception IllegalArgumentException if the new name cannot represent * a valid node name */ @Override public void setName(String s) { String name = super.getName(); if ((name == null) || !name.equals(s)) { super.setName(s); fireNameChange(name, s); } } /** Set the display name. Fires a property change event. * @param s the new name */ @Override public void setDisplayName(String s) { String displayName = super.getDisplayName(); if ((displayName == null) || !displayName.equals(s)) { super.setDisplayName(s); fireDisplayNameChange(displayName, s); } } /** Set the short description of the node. Fires a property change event. *

This description may be used for tool tips, etc. * @param s the new description */ @Override public void setShortDescription(String s) { String descr = super.getShortDescription(); if ((descr == null) || !descr.equals(s)) { super.setShortDescription(s); fireShortDescriptionChange(descr, s); } } /** * @deprecated Has no effect. To make a node disappear, simply remove it from the * children of its parent. For example, you might call * {@link Children.Keys#setKeys(Collection)} with a smaller collection. */ @Deprecated @Override public void setHidden(boolean hidden) { super.setHidden(hidden); } /** Find an icon for this node (in the closed state). * @param type constant from {@link java.beans.BeanInfo} * @return icon to use to represent the node */ public abstract Image getIcon(int type); /** Find an icon for this node (in the open state). * This icon is used when the node may have children and is expanded. * * @param type constant from {@link java.beans.BeanInfo} * @return icon to use to represent the node when open */ public abstract Image getOpenedIcon(int type); /** Get context help associated with this node. * @return the context help object (could be null or {@link HelpCtx#DEFAULT_HELP}) */ public abstract HelpCtx getHelpCtx(); /** Get the list of children. * @return the children */ public final Children getChildren() { updateChildren(); return hierarchy; } /** Can be overridden in subclasses (probably in FilterNode) to check * whether children are of the right subclass */ void updateChildren() { Children ch = hierarchy; if (ch instanceof Children.LazyChildren) { // Replace the children with the ones provided lazily: ch = ((Children.LazyChildren) ch).getOriginal(); setChildren(ch); } } /** Allows to change Children of the node. Call to this method aquires * write lock on the nodes hierarchy. Take care not to call this method * under read lock.
* * @param ch New children to be set on the node. * @since 3.1 */ protected final void setChildren(final Children ch) { Parameters.notNull("ch", ch); Children.MUTEX.postWriteRequest(new Runnable() { public void run() { List prevSnapshot = null; boolean wasInited = hierarchy.isInitialized(); boolean wasLeaf = hierarchy == Children.LEAF; if (wasInited && !wasLeaf) { prevSnapshot = hierarchy.snapshot(); } hierarchy.detachFrom(); if (prevSnapshot != null && prevSnapshot.size() > 0) { // set children to LEAF during firing // (cur. snapshot is empty and we should be consistent with children) hierarchy = Children.LEAF; int[] idxs = Children.getSnapshotIdxs(prevSnapshot); // fire remove event fireSubNodesChangeIdx(false, idxs, null, Collections.emptyList(), prevSnapshot); } hierarchy = ch; hierarchy.attachTo(Node.this); if (wasInited && !wasLeaf && hierarchy != Children.LEAF) { // init new children if old was inited hierarchy.getNodesCount(); // fire add event List snapshot = hierarchy.snapshot(); if (snapshot.size() > 0) { int[] idxs = Children.getSnapshotIdxs(snapshot); fireSubNodesChangeIdx(true, idxs, null, snapshot, null); } } if (wasLeaf != (hierarchy == Children.LEAF)) { fireOwnPropertyChange(PROP_LEAF, wasLeaf, hierarchy == Children.LEAF); } }}); } /** Test whether the node is a leaf, or may contain children. * @return true if the children list is actually {@link Children#LEAF} */ public final boolean isLeaf() { updateChildren(); return hierarchy == Children.LEAF; } /** Get the parent node. * @return the parent node, or null if this node is the root of a hierarchy */ public final Node getParentNode() { // if contained in a list return its parent node Children ch = getParentChildren(); return (ch == null) ? null : ch.getNode(); } /** Test whether this node can be renamed. * If true, one can use {@link #getName} to obtain the current name and * {@link #setName} to change it. * * @return true if the node can be renamed */ public abstract boolean canRename(); /** Test whether this node can be deleted. * @return true if can */ public abstract boolean canDestroy(); /** * Called when a node is deleted. * Generally you would never call this method yourself (only override it); * perform modifications on the underlying model itself. *

* The default * implementation obtains write access to * {@link Children#MUTEX}, and removes * the node from its parent (if any). Also fires a property change. *

Subclasses which return true from {@link #canDestroy} should override * this method to remove the associated model object from its parent. There * is no need to call the super method in this case. *

There is no guarantee that after this method has been called, other * methods such as {@link #getIcon} will not also be called for a little while. * @exception IOException if something fails */ public void destroy() throws IOException { Children.MUTEX.postWriteRequest( new Runnable() { public void run() { Children p = getParentChildren(); if (p != null) { // remove itself from parent p.remove(new Node[] { Node.this }); } // sets the valid flag to false and fires prop. change fireNodeDestroyed(); } } ); } /** Get the list of property sets for this node. * E.g. typically there may be one for normal Bean properties, one for expert * properties, and one for hidden properties. * * @return the property sets */ public abstract PropertySet[] getPropertySets(); /** Called when a node is to be copied to the clipboard. * @return the transferable object representing the * content of the clipboard * @exception IOException when the * copy cannot be performed */ public abstract Transferable clipboardCopy() throws IOException; /** Called when a node is to be cut to the clipboard. * @return the transferable object representing the * content of the clipboard * @exception IOException when the * cut cannot be performed */ public abstract Transferable clipboardCut() throws IOException; /** Called when a drag is started with this node. * The node can attach a transfer listener to ExTransferable and * will be then notified about progress of the drag (accept/reject). * * @return transferable to represent this node during a drag * @exception IOException if a drag cannot be started */ public abstract Transferable drag() throws IOException; /** Test whether this node permits copying. * @return true if so */ public abstract boolean canCopy(); /** Test whether this node permits cutting. * @return true if so */ public abstract boolean canCut(); /** Determine which paste operations are allowed when a given transferable is in the clipboard. * For example, a node representing a Java package will permit classes to be pasted into it. * @param t the transferable in the clipboard * @return array of operations that are allowed */ public abstract PasteType[] getPasteTypes(Transferable t); /** Determine if there is a paste operation that can be performed * on provided transferable. Used by drag'n'drop code to check * whether the drop is possible. * * @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 occurred at or -1 if not specified * @return null if the transferable cannot be accepted or the paste type * to execute when the drop occurs */ public abstract PasteType getDropType(Transferable t, int action, int index); /** Get the new types that can be created in this node. * For example, a node representing a Java package will permit classes to be added. * @return array of new type operations that are allowed */ public abstract NewType[] getNewTypes(); /** Get the set of actions that are associated with this node. * This set is used to construct the context menu for the node. * *

* By default this method delegates to the deprecated getActions or getContextActions * method depending on the value of supplied argument. *

* It is supposed to be overridden by subclasses accordingly. * * @param context whether to find actions for context meaning or for the * node itself * @return a list of actions (you may include nulls for separators) * @since 3.29 */ public Action[] getActions(boolean context) { return context ? getContextActions() : getActions(); } /** Get the set of actions associated with this node. * This may be used e.g. in constructing a {@link #getContextMenu context menu}. * *

* By default returns the actions in {@link NodeOp#getDefaultActions}. * * @return system actions appropriate to the node * @deprecated Use getActions (false) instead. */ @Deprecated public SystemAction[] getActions() { return NodeOp.getDefaultActions(); } /** Get a special set of actions * for situations when this node is displayed as a context. *

For example, right-clicking on a parent node in a hierarchical view (such as * the normal Explorer) should use getActions. However, if this node * is serving as the parent of a (say) a window tab full of icons (e.g., in * IconView), and the users right-clicks on * the empty space in this pane, then this method should be used to get * the appropriate actions for a context menu. *

Note that in the Windows UI system, e.g., these action sets are quite different. * * @return actions for a context. In the default implementation, same as {@link #getActions}. * @deprecated Use getActions (true) instead. */ @Deprecated public SystemAction[] getContextActions() { return getActions(); } /** Gets the default action for this node. * @return null indicating there should be none default action * @deprecated Use {@link #getPreferredAction} instead. */ @Deprecated public SystemAction getDefaultAction() { return null; } /** Gets the preferred action for this node. * This action can but need not to be one from the action array returned * from {@link #getActions(boolean)}. * In case it is, the context menu created from those actions * is encouraged to highlight the preferred action. * Override in subclasses accordingly. * * @return the preferred action, or null if there is none * @since 3.29 */ public Action getPreferredAction() { return getDefaultAction(); } /** * Makes a context menu for this node. *

Component action maps are not taken into consideration. * {@link Utilities#actionsToPopup(Action[], Component)} is a better choice * if you want to use actions such as "Paste" which look at action maps. * @return the context menu as per {@link NodeOp#findContextMenu} */ public final JPopupMenu getContextMenu() { return NodeOp.findContextMenu(new Node[] {this}); } /** Test whether there is a customizer for this node. If true, * the customizer can be obtained via {@link #getCustomizer}. * * @return true if there is a customizer */ public abstract boolean hasCustomizer(); /** Get the customizer component. * @return the component, or null if there is no customizer */ public abstract java.awt.Component getCustomizer(); /** Get a cookie for this node. *

* The set of cookies can change. If a node changes its set of * cookies, it fires a property change event with {@link #PROP_COOKIE}. *

* If the Node was constructed with a Lookup in constructor * than this method delegates to the provided lookup object. * * @param type the representation class of the cookie * @return a cookie assignable to that class, or null if this node has no such cookie * @see Lookup */ public T getCookie(Class type) { Lookup l = internalLookup(true); if (l != null) { Object obj = l.lookup(type); if (Node.Cookie.class.isInstance(obj)) { return type.cast(obj); } CookieSet.enhancedQueryMode(l, type); } return null; } /** Obtains a Lookup representing additional content of this Node. * If the lookup was provided in a constructor, it is returned here, * if not, a lookup based on the content of getCookie * method is provided. * * @return lookup for this node * @since 3.11 */ public final Lookup getLookup() { synchronized (listeners) { Lookup l = internalLookup(true); if (l != null) { return l; } l = findDelegatingLookup(); if (l != null) { return l; } // create new lookup and use it NodeLookup nl = new NodeLookup(this); registerDelegatingLookup(nl); return nl; } } /** Return a variant of the display name containing HTML markup * conforming to the limited subset of font-markup HTML supported by * the lightweight HTML renderer org.openide.awt.HtmlRenderer * (font color, bold, italic and strike-through supported; font * colors can be UIManager color keys if they are prefixed with * a ! character, i.e. <font color='!controlShadow'>). * Enclosing <html> tags are not needed. If returning non-null, HTML * markup characters that should be literally rendered must be * escaped (> becomes &gt; and so forth). *

This method should return either an HTML display name * or null; it should not return the non-HTML display name. *

* Note the specified foreground color has to have a high luminescence difference * to the background color in order to be used (displayed). * If not, the specified foreground color will be ignored and the default color will be used. * Luminescence of a color is calculated as following: * (Red value * 299 + Green value * 587 + Blue value * 114) / 1000 * and the difference has to be greater or equal to 80. *

* Note there is no property corresponding to the HTML display name - * if it should change, a change in the display name should be fired; this * should not be a mechanism for returning anything other than a marked * up version of the return value of getDisplayName. * * @see org.openide.awt.HtmlRenderer * @since 4.30 * @return a String containing conformant HTML markup which * represents the display name, or null. The default implementation * returns null. */ public String getHtmlDisplayName() { return null; } /** Register delegating lookup so it can always be found. */ final void registerDelegatingLookup(NodeLookup l) { // to have just one thread accessing the static lookups variable synchronized (lookups) { lookups.put(listeners, new WeakReference(l)); } } /** Finds delegating lookup that was previously registered * @return the lookup or null if nothing was registered or the * lookup was GCed. */ final Lookup findDelegatingLookup() { Reference ref = lookups.get(listeners); return (ref == null) ? null : ref.get(); } /** Obtain handle for this node (for serialization). * The handle can be serialized and {@link Handle#getNode} used after * deserialization to obtain the original node. * * @return the handle, or null if this node is not persistable */ public abstract Node.Handle getHandle(); /** Add a listener to changes in the node's intrinsic properties (name, cookies, etc.). *

* The listener is not notified about changes in subnodes until the * method getChildren().getNodes() is called. * @param l the listener to add */ public final void addNodeListener(NodeListener l) { listeners.add(NodeListener.class, l); listenerAdded(); } /** A method to notify FilterNode that a listenerAdded has been added */ void listenerAdded() { } final int getNodeListenerCount() { return listeners.getListenerCount(NodeListener.class); } /** Remove a node listener. * @param l the listener */ public final void removeNodeListener(NodeListener l) { listeners.remove(NodeListener.class, l); } /** Add a listener to the node's computed Bean properties. * @param l the listener */ public final void addPropertyChangeListener(PropertyChangeListener l) { int count = -1; if (err.isLoggable(Level.FINE)) { count = getPropertyChangeListenersCount(); } listeners.add(PropertyChangeListener.class, l); if (err.isLoggable(Level.FINE)) { err.log( Level.FINE, "ADD - " + getName() + " [" + count + "]->[" + getPropertyChangeListenersCount() + "] " + l ); } notifyPropertyChangeListenerAdded(l); } /** Called to notify subclasses (FilterNode) about addition of * PropertyChangeListener. */ void notifyPropertyChangeListenerAdded(PropertyChangeListener l) { } /** Returns the number of property change listeners attached to this node */ int getPropertyChangeListenersCount() { return listeners.getListenerCount(PropertyChangeListener.class); } /** Allows to figure out, whether the node has any * PropertyChangeListeners attached. * @return True if node has one or more PropertyChangeListeners attached. * @since 1.36 */ protected final boolean hasPropertyChangeListener() { return getPropertyChangeListenersCount() > 0; } /** Remove a Bean property change listener. * @param l the listener */ public final void removePropertyChangeListener(PropertyChangeListener l) { int count = -1; if (err.isLoggable(Level.FINE)) { count = getPropertyChangeListenersCount(); } listeners.remove(PropertyChangeListener.class, l); if (err.isLoggable(Level.FINE)) { err.log( Level.FINE, "RMV - " + getName() + " [" + count + "]->[" + getPropertyChangeListenersCount() + "] " + l ); } notifyPropertyChangeListenerRemoved(l); } /** Called to notify subclasses (FilterNode) about removal of * PropertyChangeListener. */ void notifyPropertyChangeListenerRemoved(PropertyChangeListener l) { } /** Fire a property change event. * * @param name name of changed property (from {@link #getPropertySets}); may be null * @param o old value; may be null * @param n new value; may be null * @see PropertyChangeEvent */ protected final void firePropertyChange(String name, Object o, Object n) { // First check if this property actually exists - if not warn! See #31413. if (err.isLoggable(Level.WARNING) && (name != null) && propertySetsAreKnown()) { Node.PropertySet[] pss = getPropertySets(); boolean exists = false; for (int i = 0; i < pss.length; i++) { Node.Property[] ps = pss[i].getProperties(); for (int j = 0; j < ps.length; j++) { if (ps[j].getName().equals(name)) { exists = true; break; } } } if (!exists) { synchronized (warnedBadProperties) { String clazz = getClass().getName(); if (warnedBadProperties.add(clazz + "." + name)) { StringWriter w = new StringWriter(); IllegalStateException ise = new IllegalStateException("Warning - the node \"" + getDisplayName() + "\" [" + clazz + "] is trying to fire the property " + name + " which is not included in its property sets. This is illegal. See IZ #31413 for details." ); // NOI18N ise.printStackTrace(new PrintWriter(w)); Logger.getLogger(Node.class.getName()).warning(w.toString()); } } } } // do not fire if the values are the same if ((o != null) && (n != null) && ((o == n) || o.equals(n))) { return; } PropertyChangeEvent ev = null; Object[] listeners = this.listeners.getListenerList(); Set dormant = Collections.emptySet(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == PropertyChangeListener.class) { // Lazily create the event: if (ev == null) { ev = new PropertyChangeEvent(this, name, o, n); } final PropertyChangeListener l = (PropertyChangeListener) listeners[i + 1]; l.propertyChange(ev); dormant = FilterNode.PropertyChangeAdapter.checkDormant(l, dormant); } } removeDormant(dormant, PropertyChangeListener.class); } /** * If true, property sets have definitely been computed, and it is fine * to call {@link #getPropertySets} without fear of killing laziness. * Used from {@link #firePropertyChange} to only check for bad properties * if the set of properties has already been computed. Otherwise, don't * bother. Subclasses may override - {@link AbstractNode} does. */ boolean propertySetsAreKnown() { return false; } /** Allow subclasses that override the getName method to fire * the changes of the name by itself. Please notice that default * implementation of setName will fire the change by itself. */ protected final void fireNameChange(String o, String n) { fireOwnPropertyChange(PROP_NAME, o, n); } /** Allow subclasses that override the getDisplayName method to fire * the changes of the name by itself. Please notice that default * implementation of setDisplayName will fire the change by itself. */ protected final void fireDisplayNameChange(String o, String n) { fireOwnPropertyChange(PROP_DISPLAY_NAME, o, n); } /** Allow subclasses that override the getShortDescription method to fire * the changes of the description by itself. Please notice that default * implementation of setShortDescription will fire the change by itself. */ protected final void fireShortDescriptionChange(String o, String n) { fireOwnPropertyChange(PROP_SHORT_DESCRIPTION, o, n); } /** Fire a change event for {@link #PROP_ICON}. */ protected final void fireIconChange() { fireOwnPropertyChange(PROP_ICON, null, null); } /** Fire a change event for {@link #PROP_OPENED_ICON}. */ protected final void fireOpenedIconChange() { fireOwnPropertyChange(PROP_OPENED_ICON, null, null); } /** Fires info about some structural change in children. Providing * type of operation and set of children changed generates event describing * the change. * * * @param addAction true if the set of children has been added, * false if it has been removed * @param delta the array with changed children * @param from the array of nodes to take indices from. * Can be null if one should find indices from current set of nodes */ final void fireSubNodesChange(boolean addAction, Node[] delta, Node[] from) { Set dormant = Collections.emptySet(); try { // enter to readAccess to prevent firing another event before all listeners receive current event Children.PR.enterReadAccess(); if (err.isLoggable(Level.FINER)) { err.finer("fireSubNodesChange() " + this); // NOI18N err.finer(" added: " + addAction); // NOI18N err.finer(" delta: " + Arrays.toString(delta)); // NOI18N err.finer(" from: " + Arrays.toString(from)); // NOI18N } NodeMemberEvent ev = null; Object[] listeners = this.listeners.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == NodeListener.class) { // Lazily create the event: if (ev == null) { ev = new NodeMemberEvent(this, addAction, delta, from); } final NodeListener l = (NodeListener) listeners[i + 1]; if (addAction) { l.childrenAdded(ev); } else { l.childrenRemoved(ev); } dormant = FilterNode.NodeAdapter.checkDormant(l, dormant); } } } finally { Children.PR.exitReadAccess(); } removeDormant(dormant, NodeListener.class); } /** Fires that some indexes has been removed. * * @param indices removed indicies, */ final void fireSubNodesChangeIdx(boolean added, int[] idxs, Children.Entry sourceEntry, List current, List previous) { Set dormant = Collections.emptySet(); try { // enter to readAccess to prevent firing another event before all listeners receive current event Children.PR.enterReadAccess(); if (err.isLoggable(Level.FINER)) { err.finer("fireSubNodesChangeIdx() " + this); // NOI18N err.finer(" added: " + added); // NOI18N err.finer(" idxs: " + Arrays.toString(idxs)); // NOI18N err.finer(" sourceEntry: " + sourceEntry); // NOI18N err.finer(" current size: " + current.size() + " current: " + current); // NOI18N err.finer(previous != null ? (" previous size: " + previous.size() + " previous: " + previous) : " null"); // NOI18N } NodeMemberEvent ev = null; Object[] tmpListeners = this.listeners.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = tmpListeners.length - 2; i >= 0; i -= 2) { if (tmpListeners[i] == NodeListener.class) { // Lazily create the event: if (ev == null) { ev = new NodeMemberEvent(this, added, idxs, current, previous); ev.sourceEntry = sourceEntry; } final NodeListener l = (NodeListener) tmpListeners[i + 1]; if (added) { l.childrenAdded(ev); } else { l.childrenRemoved(ev); } dormant = FilterNode.NodeAdapter.checkDormant(l, dormant); } } } finally { Children.PR.exitReadAccess(); } removeDormant(dormant, NodeListener.class); } /** Fires info about reordering of some children. * * @param indices array of integers describing the permutation */ final void fireReorderChange(int[] indices) { Set dormant = Collections.emptySet(); NodeReorderEvent ev = null; Object[] listeners = this.listeners.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == NodeListener.class) { // Lazily create the event: if (ev == null) { ev = new NodeReorderEvent(this, indices); } final NodeListener l = (NodeListener) listeners[i + 1]; l.childrenReordered(ev); dormant = FilterNode.NodeAdapter.checkDormant(l, dormant); } } removeDormant(dormant, NodeListener.class); } /** To all node listeners fire node destroyed notification. */ protected final void fireNodeDestroyed() { Set dormant = Collections.emptySet(); NodeEvent ev = null; Object[] listeners = this.listeners.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == NodeListener.class) { // Lazily create the event: if (ev == null) { ev = new NodeEvent(this); } final NodeListener l = (NodeListener) listeners[i + 1]; l.nodeDestroyed(ev); dormant = FilterNode.NodeAdapter.checkDormant(l, dormant); } } removeDormant(dormant, NodeListener.class); } /** Fires info about change of parent node. * @param o old node * @param n new parent */ final void fireParentNodeChange(Node o, Node n) { fireOwnPropertyChange(PROP_PARENT_NODE, o, n); } /** Fires a (Bean) property change event (for {@link #PROP_PROPERTY_SETS}). * @param o the old set * @param n the new set */ protected final void firePropertySetsChange(PropertySet[] o, PropertySet[] n) { fireOwnPropertyChange(PROP_PROPERTY_SETS, o, n); } /** Fires a change event for {@link #PROP_COOKIE}. * The old and new values are set to null. */ protected final void fireCookieChange() { Lookup l = findDelegatingLookup(); if (l instanceof NodeLookup && updateNow(this)) { Set prev = blockEvents(); try { ((NodeLookup) l).updateLookupAsCookiesAreChanged(null); } finally { unblockEvents(prev); } } fireOwnPropertyChange(PROP_COOKIE, null, null); } private static final ThreadLocal> BLOCK_EVENTS = new ThreadLocal>(); static Set blockEvents() { Set prev = BLOCK_EVENTS.get(); if (prev != null) { return prev; } BLOCK_EVENTS.set(new HashSet()); return null; } private static boolean updateNow(Node n) { final Set set = BLOCK_EVENTS.get(); if (set == null) { return true; } set.add(n); return false; } static void unblockEvents(Set prev) { final Set set = BLOCK_EVENTS.get(); if (prev == null) { while (!set.isEmpty()) { Node[] arr = set.toArray(new Node[set.size()]); for (Node n : arr) { Lookup l = n.findDelegatingLookup(); if (l instanceof NodeLookup) { ((NodeLookup) l).updateLookupAsCookiesAreChanged(null); } } set.removeAll(Arrays.asList(arr)); } } BLOCK_EVENTS.set(prev); } /** Fires info about change of own property. * @param name name of property * @param o old value * @param n new value */ final void fireOwnPropertyChange(String name, Object o, Object n) { // do not fire if the values are the same if ((o != null) && (n != null) && ((o == n) || o.equals(n))) { return; } Set dormant = Collections.emptySet(); PropertyChangeEvent ev = null; Object[] listeners = this.listeners.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == NodeListener.class) { // Lazily create the event: if (ev == null) { ev = new PropertyChangeEvent(this, name, o, n); } final NodeListener l = (NodeListener) listeners[i + 1]; l.propertyChange(ev); dormant = FilterNode.NodeAdapter.checkDormant(l, dormant); } } removeDormant(dormant, NodeListener.class); } /** Compares for equality. Does special treatment of * FilterNodes. If argument is FilterNode then this node can be * equal with it if it is its original. * * @param obj object to compare * @return true if the obj is == or is filter node of this node */ @Override public boolean equals(Object obj) { if (obj instanceof FilterNode) { return ((FilterNode) obj).equals(this); } return this == obj; } // For the benefit of FindBugs. public @Override int hashCode() { return super.hashCode(); } /** Obtains a resource string from bundle. * @param resName resource name * @return the string */ static String getString(final String resName) { return NbBundle.getBundle(Node.class).getString(resName); } @Override public String toString() { return super.toString() + "[Name=" + getName() + ", displayName=" + getDisplayName() + "]"; // NOI18N } private void removeDormant(Set dormant, Class c) { for (T l : dormant) { listeners.remove(c, l); } } /** Marker interface for all cookies. *

* Most examples are present in {@link org.openide.cookies}. */ public static interface Cookie { } /** Serializable node reference. The node should not * be serialized directly but via this handle. One can obtain a handle * by a call to {@link Node#getHandle}. *

* If that methods returns a non-null value, one can serialize it, * and after deserialization * use {@link #getNode} to obtain the original node. * *

Related documentation * *

* */ public static interface Handle extends java.io.Serializable { /** @deprecated Only public by accident. */ @Deprecated /* public static final */ long serialVersionUID = -4518262478987434353L; /** Reconstitute the node for this handle. * * @return the node for this handle * @exception IOException if the node cannot be created */ public Node getNode() throws java.io.IOException; } /** Class that represents one set of properties. A usual bean has three * sets of properties: normal, expert, and events. *

You may associate context help with this object, if desired, by setting * a {@link FeatureDescriptor#setValue custom property} with the name helpID * and value of type String giving a help ID. * Normally this is unnecessary as help for the whole {@link Node} will be used by default. */ public abstract static class PropertySet extends FeatureDescriptor { /** Default constructor. */ public PropertySet() { } /** Create a property set. * @param name system name of the property set * @param displayName human presentable name * @param shortDescription description for the set */ public PropertySet(String name, String displayName, String shortDescription) { super.setName(name); super.setDisplayName(displayName); super.setShortDescription(shortDescription); } /** Get the list of contained properties. * This list can contain both {@link Node.Property} and {@link Node.IndexedProperty} elements. * * @return the properties */ public abstract Property[] getProperties(); /* Compares just the names. * @param propertySet The object to compare to */ @Override public boolean equals(Object propertySet) { if (!(propertySet instanceof PropertySet)) { return false; } final String n1 = ((PropertySet) propertySet).getName(); if (n1 == null) { return getName() == null; } return n1.equals(getName()); } /* Returns a hash code value for the object. * * @return int hashcode */ @Override public int hashCode() { final String n = getName(); return n == null ? 0 : n.hashCode(); } /** Return a variant of the display name containing HTML markup * conforming to the limited subset of font-markup HTML supported by * the lightweight HTML renderer org.openide.awt.HtmlRenderer * (font color, bold, italic and strikethrough supported; font * colors can be UIManager color keys if they are prefixed with * a ! character, i.e. <font color=&'controlShadow'>). * Enclosing HTML tags are not needed. *

This method should return either an HTML display name * or null; it should not return the non-HTML display name if no * markup is needed. * * @see org.openide.awt.HtmlRenderer * @since 4.30 * @return a String containing conformant, legal HTML markup which * represents the display name, or null. The default implementation * returns null. */ public String getHtmlDisplayName() { return null; } } /** Description of a Bean property on a node, and operations on it. *

You may associate context help with this object, if desired, by setting * a {@link FeatureDescriptor#setValue custom property} with the name helpID * and value of type String giving a help ID. * Normally this is unnecessary as help for the whole {@link Node} will be used by default. *

Important: the {@link FeatureDescriptor#getName code name} you use for the * property is relevant not only for making properties of a node unique, but also for * {@link Node#firePropertyChange firing property changes}. * @param T the type of bean */ public abstract static class Property extends FeatureDescriptor { /** * Contains classNames of incorrectly implemented properties which have * been already logged by an ErrorManager.
* For more information see the * * discussion in issuezilla */ private static final Set warnedNames = new HashSet(); /** type that this property works with */ private Class type; //Soft caching of property editor references to improve JTable //property sheet performance private PropertyEditorRef edRef = null; /** Constructor. * @param valueType type of the property */ public Property(Class valueType) { this.type = valueType; super.setName(""); // NOI18N } /** Get the value type. This is the representation class of the property. * Remember that e.g. {@link Boolean Boolean.class} means that values are Boolean * objects; to specify the primitive type, use e.g. {@link Boolean#TYPE}. * In the latter case, {@link #getValue} and {@link #setValue} will still operate on the wrapper object. * @return the type */ public Class getValueType() { return type; } /** Test whether the property is readable. * @return true if it is */ public abstract boolean canRead(); /** Get the value. * @return the value of the property * @exception IllegalAccessException cannot access the called method * @exception InvocationTargetException an exception during invocation */ public abstract T getValue() throws IllegalAccessException, InvocationTargetException; /** Test whether the property is writable. * @return true if the read of the value is supported */ public abstract boolean canWrite(); /** Set the value. * @param val the new value of the property * @exception IllegalAccessException cannot access the called method * @exception IllegalArgumentException wrong argument * @exception InvocationTargetException an exception during invocation */ public abstract void setValue(T val) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException; /** Test whether the property had a default value. * @return true if it does (false by default) */ public boolean supportsDefaultValue() { return false; } /** Restore this property to its default value, if supported. * In the default implementation, does nothing. * Typically you would just call e.g. setValue(default). * Note that it is not permitted for this call to throw {@link IllegalArgumentException}, * though the other two exceptions from {@link #setValue} may be passed through. * @exception IllegalAccessException cannot access the called method * @exception InvocationTargetException an exception during invocation */ public void restoreDefaultValue() throws IllegalAccessException, InvocationTargetException { } /** * This method indicates whether the current value is the same as * the value that would otherwise be restored by calling * restoreDefaultValue() (if supportsDefaultValue() * returns true). The default implementation returns true and * it is recommended to also return true when supportsDefaultValue() * returns false (if we do not support default value any value can * be considered as the default). If supportsDefaultValue() * returns false this method will not be called by the default * implementation of property sheet. * @since 3.19 */ public boolean isDefaultValue() { String name = getClass().getName(); // Issue 51907 backward compatibility if (supportsDefaultValue() && warnedNames.add(name)) { Logger.getLogger(Node.Property.class.getName()).log( Level.WARNING, "Class " + name + " must override isDefaultValue() since it " + "overrides supportsDefaultValue() to be true" ); } return true; } /** Get a property editor for this property. * The default implementation uses standard Java * {@link java.beans.PropertyEditorManager}. If a property * editor is found, its instance is cached using {@link SoftReference}. * Caching happens per thread - e.g. it is guaranteed that multiple * threads accessing the editor will get different instance. * * @return the property editor, or null if there is no editor */ public PropertyEditor getPropertyEditor() { if (type == null) { return null; } PropertyEditor result = null; if (edRef != null) { result = edRef.get(); } if (result == null) { result = java.beans.PropertyEditorManager.findEditor(type); if (result != null && ( result.getClass().getName().equals("sun.beans.editors.EnumEditor") // NOI18N || result.getClass().getName().equals("com.sun.beans.editors.EnumEditor") // NOI18N )) { result = null; } edRef = new PropertyEditorRef(result); } return result; } /* Standard equals implementation for all property * classes. * @param property The object to compare to */ @Override public boolean equals(Object property) { // fix #32845 - check for non-matching types and also for null values // coming in input parameter 'property' if (!(property instanceof Property)) { return false; } Class propValueType = ((Property) property).getValueType(); Class valueType = getValueType(); if (((propValueType == null) && (valueType != null)) || ((propValueType != null) && (valueType == null))) { return false; } return ((Property) property).getName().equals(getName()) && (((propValueType == null) && (valueType == null)) || propValueType.equals(valueType)); } /* Returns a hash code value for the object. * * @return int hashcode */ @Override public int hashCode() { Class valueType = getValueType(); return getName().hashCode() * ((valueType == null) ? 1 : valueType.hashCode()); } /** Return a variant of the display name containing HTML markup * conforming to the limited subset of font-markup HTML supported by * the lightweight HTML renderer {@link org.openide.awt.HtmlRenderer} * (font color, bold, italic and strike-through supported; font * colors can be UIManager color keys if they are prefixed with * a ! character, i.e. <font color=&'controlShadow'>). * Enclosing HTML tags are not needed. *

This method should return either an HTML display name * or null; it should not return the non-HTML display name. * * @see org.openide.awt.HtmlRenderer * @since 4.30 * @return a String containing conformant, legal HTML markup which * represents the display name, or null. The default implementation * returns null. */ public String getHtmlDisplayName() { return null; } } /** Description of an indexed property and operations on it. * @param T type of the whole property * @param E type of one element */ public abstract static class IndexedProperty extends Node.Property { /** type of element that this property works with */ private Class elementType; /** Constructor. * @param valueType type of the property */ public IndexedProperty(Class valueType, Class elementType) { super(valueType); this.elementType = elementType; } /** Test whether the property is readable by index. * @return true if so */ public abstract boolean canIndexedRead(); /** Get the element type of the property (not the type of the whole property). * @return the type */ public Class getElementType() { return elementType; } /** Get the value of the property at an index. * * @param index the index * @return the value at that index * @exception IllegalAccessException cannot access the called method * @exception IllegalArgumentException wrong argument * @exception InvocationTargetException an exception during invocation */ public abstract E getIndexedValue(int index) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException; /** Test whether the property is writable by index. * @return true if so */ public abstract boolean canIndexedWrite(); /** Set the value of the property at an index. * * @param indx the index * @param val the value to set * @exception IllegalAccessException cannot access the called method * @exception IllegalArgumentException wrong argument * @exception InvocationTargetException an exception during invocation */ public abstract void setIndexedValue(int indx, E val) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException; /** Get a property editor for individual elements in this property. * @return the property editor for elements */ public PropertyEditor getIndexedPropertyEditor() { return java.beans.PropertyEditorManager.findEditor(elementType); } /* Standard equals implementation for all property * classes. * @param property The object to compare to */ @Override public boolean equals(Object property) { try { if (!super.equals(property)) { return false; } Class propElementType = ((IndexedProperty) property).getElementType(); Class elementType = getElementType(); if ( ((propElementType == null) && (elementType != null)) || ((propElementType != null) && (elementType == null)) ) { return false; } return (((propElementType == null) && (elementType == null)) || propElementType.equals(elementType)); } catch (ClassCastException e) { return false; } } /* Returns a hash code value for the object. * * @return int hashcode */ @Override public int hashCode() { Class ementType = getElementType(); return super.hashCode() * ((elementType == null) ? 1 : elementType.hashCode()); } } /** Special subclass of EventListenerList that can also listen on changes in * a lookup. */ private final class LookupEventList extends EventListenerList implements LookupListener { public final Lookup lookup; private Lookup.Result result; public LookupEventList(Lookup l) { this.lookup = l; } public Lookup init(boolean init) { boolean doInit = false; synchronized (INIT_LOCK) { if (init && (result == null)) { result = lookup.lookup(TEMPL_COOKIE); assert result != null : "Null lookup result from " + lookup + " in " + Node.this; result.addLookupListener(this); doInit = true; } } if (doInit) { result.allItems(); } return lookup; } public void resultChanged(LookupEvent ev) { if (Node.this instanceof FilterNode) { FilterNode f = (FilterNode) Node.this; // See #40734 and NodeLookupTest and CookieActionIsTooSlowTest. if (f.getOriginal() == NodeLookup.NO_COOKIE_CHANGE.get()) { // this is not real cookie change, do not fire it // issue 40734 return; } } fireCookieChange(); } } private static final class PropertyEditorRef extends SoftReference { private final Thread createdBy; public PropertyEditorRef(PropertyEditor referent) { super(referent); createdBy = Thread.currentThread(); } @Override public PropertyEditor get() { if (Thread.currentThread() != createdBy) { return null; } return super.get(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy