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

org.netbeans.modules.viewmodel.TreeModelNode Maven / Gradle / Ivy

/*
 * 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.netbeans.modules.viewmodel;

import java.awt.Image;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyEditor;
import java.lang.ref.WeakReference;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.security.PrivilegedAction;
import java.text.Format;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import org.netbeans.spi.viewmodel.AsynchronousModelFilter;
import org.netbeans.spi.viewmodel.AsynchronousModelFilter.CALL;
import org.netbeans.spi.viewmodel.ColumnModel;
import org.netbeans.spi.viewmodel.ModelEvent;
import org.netbeans.spi.viewmodel.Models;
import org.netbeans.spi.viewmodel.Models.TreeFeatures;
import org.netbeans.spi.viewmodel.UnknownTypeException;
import org.netbeans.swing.etable.ETableColumn;

import org.openide.awt.Actions;
import org.openide.explorer.view.CheckableNode;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Index;
import org.openide.nodes.Node;
import org.openide.nodes.PropertySupport;
import org.openide.nodes.Sheet;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.RequestProcessor.Task;
import org.openide.util.datatransfer.PasteType;
import org.openide.util.lookup.Lookups;


/**
 *
 * @author   Jan Jancura
 */
public class TreeModelNode extends AbstractNode {

    /**
     * The maximum length of text that is interpreted as HTML.
     * This is documented at openide/explorer/src/org/openide/explorer/doc-files/propertyViewCustomization.html
     */
    private static final int MAX_HTML_LENGTH = 511;
    private static final String HTML_START_TAG = "";
    private static final String HTML_END_TAG = "";
    
    // variables ...............................................................

    private Models.CompoundModel model;
    private final ColumnModel[]  columns;
    protected TreeModelRoot      treeModelRoot;
    protected Object             object;

    private final LazyChildrenFactory lazyChildren;
    private String              displayName, oldDisplayName;
    private String              htmlDisplayName;
    private final Object        displayNameLock = new Object();
    private boolean             iconLoaded;
    private String              shortDescription;
    private final Object        shortDescriptionLock = new Object();
    private final Map properties = new HashMap();
    private final Map columnIDsMap;
    private static final String EVALUATING_STR = NbBundle.getMessage(TreeModelNode.class, "EvaluatingProp");

    
    // init ....................................................................

    /**
    * Creates root of call stack for given producer.
    */
    public TreeModelNode (
        final Models.CompoundModel model, 
        final TreeModelRoot treeModelRoot,
        final Object object
    ) {
        this(
            model,
            model.getColumns (),
            treeModelRoot,
            object
        );
    }

    /**
    * Creates root of call stack for given producer.
    */
    public TreeModelNode (
        final Models.CompoundModel model,
        final ColumnModel[] columns,
        final TreeModelRoot treeModelRoot,
        final Object object
    ) {
        this(
            model,
            columns,
            object != model.getRoot() ?
                new LazyChildrenFactory(model, columns, treeModelRoot, object) : null,
            object != model.getRoot() ?
                null : createChildren (model, columns, treeModelRoot, object),
            treeModelRoot,
            object
        );
    }

    /**
    * Creates root of call stack for given producer.
    */
    protected TreeModelNode (
        final Models.CompoundModel model,
        final ColumnModel[] columns,
        final LazyChildrenFactory lazyChildren,
        final Children children,
        final TreeModelRoot treeModelRoot,
        final Object object
    ) {
        this(
            model,
            columns,
            lazyChildren,
            children,
            treeModelRoot,
            object,
            new Index[] { null });
    }

    private TreeModelNode (
        final Models.CompoundModel model,
        final ColumnModel[] columns,
        final LazyChildrenFactory lazyChildren,
        final Children children,
        final TreeModelRoot treeModelRoot,
        final Object object,
        final Index[] indexPtr  // Hack, because we can not declare variables before call to super() :-(
    ) {
        super (
            (lazyChildren != null) ?
                Children.createLazy(lazyChildren) : children,
            createLookup(object, model, children, indexPtr)
        );
        this.model = model;
        this.treeModelRoot = treeModelRoot;
        this.object = object;
        this.lazyChildren = lazyChildren;
        if (indexPtr[0] != null) {
            ((IndexImpl) indexPtr[0]).setNode(this);
            setIndexWatcher(indexPtr[0]);
        }
        
        // 
        // Use the modified CompoundModel class's field to set the 
        // propertiesHelpID for properties sheets if the model's helpID
        // has been set
        if (model.getHelpId() != null) {
            this.setValue("propertiesHelpID", model.getHelpId()); // NOI18N
        }
        // 
        
        treeModelRoot.registerNode (object, this);
        this.columnIDsMap = createColumnIDsMap(columns);
        this.columns = columns;
    }

    private static Lookup createLookup(Object object, Models.CompoundModel model,
                                       Children ch, Index[] indexPtr) {
        CheckNodeCookieImpl cnc = new CheckNodeCookieImpl(model, object);
        boolean canReorder;
        try {
            canReorder = model.canReorder(object);
        } catch (UnknownTypeException ex) {
            if (!(object instanceof String)) {
                Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, ex);
            }
            canReorder = false;
        }
        if (canReorder) {
            Index i = new IndexImpl(model, object);
            indexPtr[0] = i;
            return Lookups.fixed(object, cnc, i);
        } else {
            return Lookups.fixed(object, cnc);
        }
    }
    
    private static Map createColumnIDsMap(ColumnModel[] columns) {
        Map cids = null;
        for (ColumnModel cm : columns) {
            if (cm instanceof HyperColumnModel) {
                if (cids == null) {
                    cids = new HashMap();
                }
                HyperColumnModel hcm = (HyperColumnModel) cm;
                String mainID = cm.getID();
                for (String id : hcm.getAllIDs()) {
                    cids.put(id, mainID);
                }
            }
        }
        return cids;
    }

    private boolean areChildrenInitialized() {
        if (lazyChildren != null) {
            return lazyChildren.areChildrenCreated();
        } else {
            return true;
        }
    }

    private void setIndexWatcher(Index childrenIndex) {
        childrenIndex.addChangeListener(new ChangeListener() {
            public void stateChanged(ChangeEvent e) {
                if (!areChildrenInitialized()) {
                    return ;
                }
                Children ch = getChildren();
                if (ch instanceof TreeModelChildren) {
                    ((TreeModelChildren) ch).refreshChildren(new TreeModelChildren.RefreshingInfo(false));
                }
            }
        });
    }

    private static Executor asynchronous(Models.CompoundModel model, CALL asynchCall, Object object) {
        Executor exec;
        try {
            exec = model.asynchronous(asynchCall, object);
            //System.err.println("Asynchronous("+asynchCall+", "+object+") = "+exec);
            if (exec == null) {
                Exceptions.printStackTrace(Exceptions.attachMessage(new NullPointerException("Provided executor is null."), "model = "+model+", object = "+object));
                exec = AsynchronousModelFilter.CURRENT_THREAD;
            }
        } catch (Exception ex) {
            Exceptions.printStackTrace(Exceptions.attachMessage(ex, "model = "+model+", object = "+object));
            exec = AsynchronousModelFilter.CURRENT_THREAD;
        }
        return exec;
    }


    // Node implementation .....................................................

    @Override
    protected Sheet createSheet() {
        Sheet sheet = Sheet.createDefault();
        Sheet.Set ps = Sheet.createPropertiesSet ();
        int i, k = columns.length;
        for (i = 0; i < k; i++)
            ps.put (new MyProperty (columns [i], treeModelRoot));
        sheet.put (ps);
        return sheet;
    }

    private static Children createChildren (
        Models.CompoundModel model,
        ColumnModel[] columns,
        TreeModelRoot treeModelRoot,
        Object object
    ) {
        if (object == null) 
            throw new NullPointerException ();
        try {
            return model.isLeaf (object) ? 
                Children.LEAF :
                new TreeModelChildren (model, columns, treeModelRoot, object);
        } catch (UnknownTypeException e) {
            if (!(object instanceof String)) {
                Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
            }
            return Children.LEAF;
        }
    }

    @Override
    public String getShortDescription () {
        synchronized (shortDescriptionLock) {
            if (shortDescription != null) {
                return shortDescription;
            }
        }
        Executor exec = asynchronous(model, CALL.SHORT_DESCRIPTION, object);
        if (exec == AsynchronousModelFilter.CURRENT_THREAD) {
            return updateShortDescription();
        } else {
            exec.execute(new Runnable() {
                public void run() {
                    updateShortDescription();
                    fireShortDescriptionChange(null, null);
                }
            });
            return EVALUATING_STR;
        }
    }

    private String updateShortDescription() {
        try {
            String sd = model.getShortDescription (object);
            if (sd != null) {
                sd = adjustHTML(sd);
            }
            synchronized (shortDescriptionLock) {
                shortDescription = sd;
            }
            return sd;
        } catch (UnknownTypeException e) {
            if (!(object instanceof String)) {
                Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
            }
            return null;
        }
    }

    private void doFireShortDescriptionChange() {
        synchronized (shortDescriptionLock) {
            shortDescription = null;
        }
        fireShortDescriptionChange(null, null);
    }
    
    @Override
    public String getHtmlDisplayName () {
        synchronized (displayNameLock) {
            // Compute the HTML display name if the ordinary display name is not available (e.g. was reset)
            if (displayName == null) {
                try {
                    setModelDisplayName();
                } catch (UnknownTypeException ex) {
                    Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, ex);
                }
            }
            if (displayName == null) {
                displayName = "";   // display name was computed
            }
            return htmlDisplayName;
        }
    }
    
    @Override
    public Action[] getActions (boolean context) {
        Action[] actions;
        if (context) 
            actions = treeModelRoot.getRootNode ().getActions (false);
        try {
            actions = filterActionsWhenSorted(model.getActions (object));
        } catch (UnknownTypeException e) {
            // NodeActionsProvider is voluntary
            actions = new Action [0];
        }
        presetActionNodes(actions);
        return actions;
    }
    
    private void presetActionNodes(Action[] actions) {
        for (Action a : actions) {
            if (a instanceof ActionOnPresetNodes) {
                ((ActionOnPresetNodes) a).addNode(this);
            }
        }
    }

    private boolean isTableSorted() {
        TableColumnModel tcm = treeModelRoot.getOutlineView().getOutline().getColumnModel();
        Enumeration cen = tcm.getColumns();
        while (cen.hasMoreElements()) {
            ETableColumn etc = (ETableColumn) cen.nextElement();
            if (etc.isSorted()) {
                return true;
            }
        }
        return false;
    }

    private Action[] filterActionsWhenSorted(Action[] actions) {
        if (actions == null || actions.length == 0) {
            return actions;
        }
        for (int i = 0; i < actions.length; i++) {
            Action a = actions[i];
            if (a == null) continue;
            boolean disabled = Boolean.TRUE.equals(a.getValue("DisabledWhenInSortedTable"));    // NOI18N
            if (disabled) {
                if (a instanceof DisableableAction) {
                    actions[i] = ((DisableableAction) a).createDisableable(new PrivilegedAction() {
                        @Override
                        public Object run() {
                            // Disabled when the table is sorted:
                            return !isTableSorted();
                        }
                    });
                } else {
                    actions[i] = new DisabledWhenSortedAction(a);
                }
            }
        }
        return actions;
    }

    @Override
    public Action getPreferredAction () {
        return new AbstractAction () {
            public void actionPerformed (ActionEvent e) {
                try {
                    model.performDefaultAction (object);
                } catch (UnknownTypeException ex) {
                    // NodeActionsProvider is voluntary
                }
            }
        };
    }
    
    @Override
    public boolean canDestroy () {
        try {
            Action[] as = model.getActions (object);
            int i, k = as.length;
            for (i = 0; i < k; i++) {
                if (as [i] == null) continue;
                Object key = as [i].getValue (Action.ACCELERATOR_KEY);
                if ( (key != null) &&
                     (key.equals (KeyStroke.getKeyStroke ("DELETE")))
                ) return as [i].isEnabled ();
            }
            return false;
        } catch (UnknownTypeException e) {
            // NodeActionsProvider is voluntary
            return false;
        }
    }
    
    @Override
    public boolean canCopy () {
        try {
            return model.canCopy(object);
        } catch (UnknownTypeException e) {
            Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
            return false;
        }
    }
    
    @Override
    public boolean canCut () {
        try {
            return model.canCut(object);
        } catch (UnknownTypeException e) {
            Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
            return false;
        }
    }
    
    @Override
    public void destroy () {
        try {
            Action[] as = model.getActions (object);
            int i, k = as.length;
            for (i = 0; i < k; i++) {
                if (as [i] == null) continue;
                Object key = as [i].getValue (Action.ACCELERATOR_KEY);
                if ( (key != null) &&
                     (key.equals (KeyStroke.getKeyStroke ("DELETE")))
                ) {
                    as [i].actionPerformed (null);
                    return;
                }
            }
        } catch (UnknownTypeException e) {
            Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
        }
        if (model.getRoot() == object) {
            treeModelRoot.destroy();
        }
    }

    
    // other methods ...........................................................
    
    void setObject (Object o) {
        setObject(model, o);
    }
    
    void setObject (Models.CompoundModel model, Object o) {
        setObjectNoRefresh (o);
        refresh (model);
    }

    private void setObjectNoRefresh (Object o) {
        object = o;
        if (areChildrenInitialized()) {
            Children ch = getChildren ();
            if (ch instanceof TreeModelChildren)
                ((TreeModelChildren) ch).object = o;
        }
    }
    
    public Object getObject () {
        return object;
    }

    Models.CompoundModel getModel() {
        return model;
    }

    private Task refreshTask;
    private final Object refreshTaskLock = new Object();
    private final Set childrenRefreshModels = new HashSet();
    
    void refresh (Models.CompoundModel model) {
        //System.err.println("TreeModelNode.refresh("+model+") on "+object);
        //Thread.dumpStack();
        // 1) empty cache
        synchronized (properties) {
            properties.clear();
        }
        
        
        // 2) refresh name, displayName and iconBase
        synchronized (childrenRefreshModels) {
            childrenRefreshModels.add(model);
        }
        synchronized (refreshTaskLock) {
            if (refreshTask == null) {
                refreshTask = getRequestProcessor ().create (new Runnable () {
                    public void run () {
                        if (!SwingUtilities.isEventDispatchThread()) {
                            try {
                                SwingUtilities.invokeAndWait(this);
                            } catch (InterruptedException ex) {
                            } catch (InvocationTargetException ex) {
                                Exceptions.printStackTrace(ex);
                            }
                            return ;
                        }
                        refreshNode ();
                        doFireShortDescriptionChange();

                        // 3) refresh children
                        Set modelsToRefresh;
                        synchronized (childrenRefreshModels) {
                            modelsToRefresh = new HashSet(childrenRefreshModels);
                            childrenRefreshModels.clear();
                        }
                        if (modelsToRefresh.size() > 0) {
                            refreshTheChildren(modelsToRefresh, new TreeModelChildren.RefreshingInfo(true));
                        }
                    }
                });
            }
            refreshTask.schedule(10);
        }
    }
    
    void refresh (final Models.CompoundModel model, int changeMask) {
        if (changeMask == 0xFFFFFFFF) {
            refresh(model);
            return ;
        }
        boolean refreshed = false;
        if ((ModelEvent.NodeChanged.DISPLAY_NAME_MASK & changeMask) != 0) {
            boolean doFireDisplayNameChange;
            synchronized (displayNameLock) {
                doFireDisplayNameChange = displayName != null;
                displayName = null;
            }
            if (doFireDisplayNameChange) {
                fireDisplayNameChange(null, null);
            }
            refreshed = true;
        }
        if ((ModelEvent.NodeChanged.ICON_MASK & changeMask) != 0) {
            if (iconLoaded) {
                iconLoaded = false;
                fireIconChange();
                //fireOpenedIconChange(); - not necessary, just adds more events!
                // VisualizerNode.propertyChange() interprets all name/icon changes as one kind.
            }
            refreshed = true;
        }
        if ((ModelEvent.NodeChanged.SHORT_DESCRIPTION_MASK & changeMask) != 0) {
            doFireShortDescriptionChange();
            refreshed = true;
        }
        if ((ModelEvent.NodeChanged.CHILDREN_MASK & changeMask) != 0) {
            boolean doRefresh;
            synchronized (childrenRefreshModels) {
                doRefresh = childrenRefreshModels.add(model);
            }
            if (doRefresh) {
                SwingUtilities.invokeLater (new Runnable () {
                    public void run () {
                        synchronized (childrenRefreshModels) {
                            childrenRefreshModels.remove(model);
                        }
                        refreshTheChildren(Collections.singleton(model), new TreeModelChildren.RefreshingInfo(false));
                    }
                });
            }
            refreshed = true;
        }
        if ((ModelEvent.NodeChanged.EXPANSION_MASK & changeMask) != 0) {
            SwingUtilities.invokeLater (new Runnable () {
                public void run () {
                    expandIfSetToExpanded();
                }
            });
        }
        if (!refreshed) {
            refresh(model);
        }
    }
    
    private static RequestProcessor requestProcessor;
    // Accessed from test
    RequestProcessor getRequestProcessor () {
        /*RequestProcessor rp = treeModelRoot.getRequestProcessor();
        if (rp != null) {
            return rp;
        }*/
        synchronized (TreeModelNode.class) {
            if (requestProcessor == null)
                requestProcessor = new RequestProcessor ("TreeModel", 1);
            return requestProcessor;
        }
    }

    private boolean setName (String name, boolean italics) {
        // XXX HACK: HTMLDisplayName is missing in the models!
        synchronized (displayNameLock) {
            String oldHtmlDisplayName = htmlDisplayName;
            String _oldDisplayName = oldDisplayName;

            String newDisplayName;
            if (name.startsWith (HTML_START_TAG)) {
                htmlDisplayName = name;
                newDisplayName = removeHTML(name);
            } else if (name.startsWith ("<_html>")) { //[TODO] use empty string as name in the case of <_html> tag
                htmlDisplayName = '<' + name.substring(2);
                newDisplayName = "";
            } else {
                htmlDisplayName = null;
                newDisplayName = name;
            }
            displayName = newDisplayName;
            oldDisplayName = newDisplayName;
            return _oldDisplayName == null || !_oldDisplayName.equals(newDisplayName) ||
                   oldHtmlDisplayName == null || !oldHtmlDisplayName.equals(htmlDisplayName);
        }
    }
    
    private String parseDisplayFormat(String name) {
        MessageFormat treeNodeDisplayFormat = treeModelRoot.getTreeNodeDisplayFormat();
        if (treeNodeDisplayFormat == null) {
            return name;
        }
        if (propertyDisplayNameListener == null) {
            propertyDisplayNameListener = new PropertyDisplayNameListener();
            addPropertyChangeListener(propertyDisplayNameListener);
        }
        Property[] nodeProperties = getPropertySets()[0].getProperties();
        Format[] formatsByArgumentIndex = treeNodeDisplayFormat.getFormatsByArgumentIndex();
        String pattern = treeNodeDisplayFormat.toPattern();
        int n = formatsByArgumentIndex.length;
        Object[] args = new Object[n];
        String[] argsHTML = new String[n];
        boolean nonEmptyArgs = false;
        for (int i = 0; i < n; i++) {
            if (pattern.indexOf("{"+i) >= 0) {
            //if (formatsByArgumentIndex[i] != null) {
                if (columns[i].getType() == null) {
                    if (name.startsWith (HTML_START_TAG)) {
                        argsHTML[i] = name;
                        args[i] = removeHTML(name);
                    } else if (name.startsWith ("<_html>")) {
                        argsHTML[i] = '<' + name.substring(2);
                        args[i] = removeHTML((String) argsHTML[i]);
                    } else {
                        argsHTML[i] = null;
                        args[i] = name;
                    }
                } else {
                    try {
                        args[i] = nodeProperties[i].getValue();
                        argsHTML[i] = (String) nodeProperties[i].getValue("htmlDisplayValue");
                        if (!"".equals(args[i])) {
                            propertyDisplayNameListener.addPropertyName(nodeProperties[i].getName());
                            nonEmptyArgs = true;
                        }
                    } catch (IllegalAccessException ex) {
                        Exceptions.printStackTrace(ex);
                    } catch (InvocationTargetException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                }
            } else {
                args[i] = null;
            }
        }
        if (nonEmptyArgs) {
            boolean isHTML = false;
            int iHTML = -1;
            for (int i = 0; i < n; i++) {
                if (argsHTML[i] != null) {
                    isHTML = true;
                    iHTML = i;
                    args[i] = stripHTMLTags(argsHTML[i]);
                } else if (isHTML && args[i] instanceof String) {
                    args[i] = adjustHTML((String) args[i]);
                }
            }
            for (int i = 0; i < iHTML; i++) {
                if (args[i] instanceof String) {
                    args[i] = adjustHTML((String) args[i]);
                }
            }
            String format = treeNodeDisplayFormat.format(args);
            if (isHTML) {
                format = HTML_START_TAG+format+HTML_END_TAG;
            }
            return format; //new Object[] { name });
        } else {
            return name;
        }
    }
    
    private static String stripHTMLTags(String str) {
        if (str.startsWith(HTML_START_TAG)) {
            str = str.substring(HTML_START_TAG.length());
        }
        if (str.endsWith(HTML_END_TAG)) {
            str = str.substring(0, str.length() - HTML_END_TAG.length());
        }
        return str;
    }
    
    private PropertyDisplayNameListener propertyDisplayNameListener;
    
    private class PropertyDisplayNameListener implements PropertyChangeListener {
        
        private Set propertyNames = new HashSet();
        
        PropertyDisplayNameListener() {
        }
        
        void addPropertyName(String propertyName) {
            propertyNames.add(propertyName);
        }

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (propertyNames.contains(evt.getPropertyName())) {
                try {
                    setModelDisplayName();
                    fireDisplayNameChange(null, null);
                } catch (UnknownTypeException ex) {
                    Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, ex);
                }
            }
        }
        
    }

    private void setModelDisplayName() throws UnknownTypeException {
        Executor exec = asynchronous(model, CALL.DISPLAY_NAME, object);
        if (exec == AsynchronousModelFilter.CURRENT_THREAD) {
            String name = model.getDisplayName (object);
            if (name == null) {
                Throwable t =
                    new NullPointerException (
                        "Model: " + model + ".getDisplayName (" + object +
                        ") = null!"
                    );
                Exceptions.printStackTrace(t);
            } else {
                name = parseDisplayFormat(name);
                setName (name, false);
            }
        } else {
            final String originalDisplayName = (oldDisplayName != null) ? oldDisplayName : "";
            setName(EVALUATING_STR, false);
            exec.execute(new Runnable() {
                public void run() {
                    String name;
                    try {
                        name = model.getDisplayName(object);
                    } catch (UnknownTypeException ex) {
                        Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, ex);
                        setName(originalDisplayName, false);
                        fireDisplayNameChange(null, originalDisplayName);
                        return ;
                    }
                    if (name == null) {
                        Throwable t =
                            new NullPointerException (
                                "Model: " + model + ".getDisplayName (" + object +
                                ") = null!"
                            );
                        Exceptions.printStackTrace(t);
                        setName(originalDisplayName, false);
                        fireDisplayNameChange(null, originalDisplayName);
                    } else {
                        if (setName (name, false)) {
                            fireDisplayNameChange(null, name);
                        }
                    }
                }
            });
        }
    }

    @Override
    public String getDisplayName() {
        synchronized (displayNameLock) {
            if (displayName == null) {
                try {
                    setModelDisplayName();
                } catch (UnknownTypeException ex) {
                    Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, ex);
                }
            }
            if (displayName == null) {
                displayName = "";
            }
            return displayName;
        }
    }

    @Override
    public void setDisplayName(String s) {
        String sOld;
        synchronized (displayNameLock) {
            if ((displayName != null) && displayName.equals(s)) {
                return ;
            }
            sOld = displayName;
            displayName = oldDisplayName = s;
        }
        fireDisplayNameChange(sOld, s);
    }

    private void setModelIcon() throws UnknownTypeException {
        String iconBase = null;
        if (model.getRoot() != object) {
            iconBase = model.getIconBaseWithExtension (object);
        }
        if (iconBase != null)
            setIconBaseWithExtension (iconBase);
        else
            setIconBaseWithExtension ("org/openide/resources/actions/empty.gif");
    }

    @Override
    public Image getIcon(int type) {
        if (!iconLoaded) {
            try {
                setModelIcon();
            } catch (UnknownTypeException ex) {
                Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, ex);
            }
            iconLoaded = true;
        }
        return super.getIcon(type);
    }

    @Override
    public Image getOpenedIcon(int type) {
        if (!iconLoaded) {
            try {
                setModelIcon();
            } catch (UnknownTypeException ex) {
                Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, ex);
            }
            iconLoaded = true;
        }
        return super.getOpenedIcon(type);
    }

    private void refreshNode () {
        boolean doFireDisplayNameChange;
        synchronized (displayNameLock) {
            doFireDisplayNameChange = displayName != null;
            displayName = null;
        }
        if (doFireDisplayNameChange) {
            fireDisplayNameChange(null, null);
        }
        if (iconLoaded) {
            iconLoaded = false;
            fireIconChange();
            //fireOpenedIconChange(); - not necessary, just adds more events!
            // VisualizerNode.propertyChange() interprets all name/icon changes as one kind.
        }
        firePropertyChange(null, null, null);
    }
    
    void refreshColumn(String column, int changeMask) {
        String visualColumn = column;
        if (columnIDsMap != null) {
            String c = columnIDsMap.get(column);
            if (c != null) {
                visualColumn = c;
            }
        }
        synchronized (properties) {
            if ((ModelEvent.TableValueChanged.VALUE_MASK & changeMask) != 0) {
                properties.remove(column);
            }
            if ((ModelEvent.TableValueChanged.HTML_VALUE_MASK & changeMask) != 0) {
                properties.remove(column + "#html");
            }
            if ((ModelEvent.TableValueChanged.IS_READ_ONLY_MASK & changeMask) != 0) {
                properties.remove(column + "#canWrite");
            }
        }
        
        firePropertyChange(visualColumn, null, null);
    }

    /**
     * @param model The associated model - necessary for hyper node.
     * @param refreshSubNodes If recursively refresh subnodes.
     */
    protected void refreshTheChildren(Set models, TreeModelChildren.RefreshingInfo refreshInfo) {
        for (Models.CompoundModel model: models) {
            refreshTheChildren(model, refreshInfo);
        }
    }
    /**
     * @param model The associated model - necessary for hyper node.
     * @param refreshSubNodes If recursively refresh subnodes.
     */
    private void refreshTheChildren(Models.CompoundModel model, TreeModelChildren.RefreshingInfo refreshInfo) {
        if (!areChildrenInitialized()) {
            return ;
        }
        Children ch = getChildren();
        try {
            if (ch instanceof TreeModelChildren) {
                if (model.isLeaf(object)) {
                    setChildren(Children.LEAF);
                } else {
                    ((TreeModelChildren) ch).refreshChildren(refreshInfo);
                }
            } else if (!model.isLeaf (object)) {
                setChildren(new TreeModelChildren (model, columns, treeModelRoot, object));
            }
        } catch (UnknownTypeException utex) {
            // not known - do not change children
            if (!(object instanceof String)) {
                Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, utex);
            }
            setChildren(Children.LEAF);
        }
    }
    
    private static String htmlValue (String name) {
        if (!(name.length() > 6 && name.substring(0, 6).equalsIgnoreCase(HTML_START_TAG))) return null;
        if (name.length() > MAX_HTML_LENGTH) {
            int endTagsPos = findEndTagsPos(name);
            String ending = name.substring(endTagsPos + 1);
            name = name.substring(0, MAX_HTML_LENGTH - 3 - ending.length());
            // Check whether we haven't cut "&...;" in between:
            int n = name.length();
            for (int i = n - 1; i > n - 6; i--) {
                if (name.charAt(i) == ';') {
                    break; // We have an end of the group
                }
                if (name.charAt(i) == '&') {
                    name = name.substring(0, i);
                    break;
                }
            }
            name += "..." + ending;
        }
        return adjustHTML(name);
    }
    
    private static int findEndTagsPos(String s) {
        int openings = 0;
        int i;
        for (i = s.length() - 1; i >= 0; i--) {
            if (s.charAt(i) == '>') openings++;
            else if (s.charAt(i) == '<') openings--;
            else if (openings == 0) break;
        }
        return i;
    }
    
    private static String removeHTML (String text) {
        if (!(text.length() > 6 && text.substring(0, 6).equalsIgnoreCase(HTML_START_TAG))) {
            return text;
        }
        text = text.replace ("", "")
                   .replace ("", "")
                   .replace ("", "")
                   .replace ("", "")
                   .replace (HTML_START_TAG, "")
                   .replace (HTML_END_TAG, "")
                   .replace ("", "");
        int i = text.indexOf ("= 0) {
            int j = text.indexOf (">", i);
            text = text.substring (0, i) + text.substring (j + 1);
            i = text.indexOf ("")
                   .replace ("&", "&");
    }
    
    /** Adjusts HTML text so that it's rendered correctly.
     * In particular, this assures that white characters are visible.
     */
    private static String adjustHTML(String text) {
        text = text.replace("\\", "\\\\");
        StringBuffer sb = null;
        int j = 0;
        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);
            String replacement = null;
            if (c == '\n') {
                replacement = "\\n";
            } else if (c == '\r') {
                replacement = "\\r";
            } else if (c == '\f') {
                replacement = "\\f";
            } else if (c == '\b') {
                replacement = "\\b";
            }
            if (replacement != null) {
                if (sb == null) {
                    sb = new StringBuffer(text.substring(0, i));
                } else {
                    sb.append(text.substring(j, i));
                }
                sb.append(replacement);
                j = i+1;
            }
        }
        if (sb == null) {
            return text;
        } else {
            sb.append(text.substring(j));
            return sb.toString();
        }
    }
    
    
    @Override
    public boolean canRename() {
        try {
            return model.canRename(object);
        } catch (UnknownTypeException e) {
            Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
            return false;
        }
    }

    @Override
    public void setName(String s) {
        try {
            model.setName(object, s);
            super.setName(s);
        } catch (UnknownTypeException e) {
            Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
        }
    }
    
    @Override
    public Transferable clipboardCopy() throws IOException {
        Transferable t;
        try {
            t = model.clipboardCopy(object);
        } catch (UnknownTypeException e) {
            Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
            t = null;
        }
        if (t == null) {
            return super.clipboardCopy();
        } else {
            return t;
        }
    }
    
    @Override
    public Transferable clipboardCut() throws IOException {
        Transferable t;
        try {
            t = model.clipboardCut(object);
        } catch (UnknownTypeException e) {
            Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
            t = null;
        }
        if (t == null) {
            return super.clipboardCut();
        } else {
            return t;
        }
    }
    
    @Override
    public Transferable drag() throws IOException {
        Transferable t;
        try {
            t = model.drag(object);
        } catch (UnknownTypeException e) {
            Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
            t = null;
        }
        if (t == null) {
            return super.drag();
        } else {
            return t;
        }
    }
    
    @Override
    public void createPasteTypes(Transferable t, List l) {
        PasteType[] p;
        try {
            p = model.getPasteTypes(object, t);
        } catch (UnknownTypeException e) {
            Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
            p = null;
        }
        if (p == null) {
            super.createPasteTypes(t, l);
        } else {
            l.addAll(Arrays.asList(p));
        }
    }
    
    @Override
    public PasteType getDropType(Transferable t, int action, int index) {
        PasteType p;
        try {
            p = model.getDropType(object, t, action, index);
        } catch (UnknownTypeException e) {
            Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
            p = null;
        }
        if (p == null) {
            return super.getDropType(t, action, index);
        } else {
            return p;
        }
    }
    
    private final void expandIfSetToExpanded() {
        try {
            DefaultTreeExpansionManager.get(model).setChildrenToActOn(getTreeDepth());
            if (model.isExpanded (object)) {
                TreeFeatures treeTable = treeModelRoot.getTreeFeatures ();
                if (treeTable != null) {
                    treeTable.expandNode (object);
                }
            }
        } catch (UnknownTypeException ex) {
        }
    }

    private Integer depth;

    private Integer getTreeDepth() {
        Node p = getParentNode();
        if (p == null) {
            return 0;
        } else if (depth != null) {
            return depth;
        } else {
            int d = 1;
            while ((p = p.getParentNode()) != null) d++;
            depth = Integer.valueOf(d);
            return depth;
        }
    }

    // innerclasses ............................................................

    public static interface DisableableAction extends Action {

        Action createDisableable(PrivilegedAction enabledTest);
        
    }
    
    /**
     * An action, that can act on a pre-set set of nodes.
     */
    public static interface ActionOnPresetNodes extends Action {
        
        /**
         * Add a node to act on.
         * The set of nodes is cleared in the next cycle of event dispatch loop.
         * When no nodes are provided, the TopComponent.getRegistry ().getActivatedNodes () are used.
         * @param n a node to act on
         */
        void addNode(Node n);
        
    }

    private class DisabledWhenSortedAction implements Action {

        private Action a;

        public DisabledWhenSortedAction(Action a) {
            this.a = a;
        }

        @Override
        public Object getValue(String key) {
            return a.getValue(key);
        }

        @Override
        public void putValue(String key, Object value) {
            a.putValue(key, value);
        }

        @Override
        public void setEnabled(boolean b) {
            a.setEnabled(b);
        }

        @Override
        public boolean isEnabled() {
            if (isTableSorted()) {
                return false;
            } else {
                return a.isEnabled();
            }
        }

        @Override
        public void addPropertyChangeListener(PropertyChangeListener listener) {
            a.addPropertyChangeListener(listener);
        }

        @Override
        public void removePropertyChangeListener(PropertyChangeListener listener) {
            a.removePropertyChangeListener(listener);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            a.actionPerformed(e);
        }
    }

    private static final class CheckNodeCookieImpl implements CheckableNode {

        private final Models.CompoundModel model;
        private final Object object;

        public CheckNodeCookieImpl(Models.CompoundModel model, Object object) {
            this.model = model;
            this.object = object;
        }

        public boolean isCheckable() {
            try {
                return model.isCheckable(object);
            } catch (UnknownTypeException ex) {
                Exceptions.printStackTrace(Exceptions.attachMessage(ex, "Model = "+model));
                return false;
            }
        }

        public boolean isCheckEnabled() {
            try {
                return model.isCheckEnabled(object);
            } catch (UnknownTypeException ex) {
                Exceptions.printStackTrace(Exceptions.attachMessage(ex, "Model = "+model));
                return false;
            }
        }

        public Boolean isSelected() {
            try {
                return model.isSelected(object);
            } catch (UnknownTypeException ex) {
                Exceptions.printStackTrace(Exceptions.attachMessage(ex, "Model = "+model));
                return false;
            }
        }

        public void setSelected(Boolean selected) {
            try {
                model.setSelected(object, selected);
            } catch (UnknownTypeException ex) {
                Exceptions.printStackTrace(Exceptions.attachMessage(ex, "Model = "+model));
            }
        }

    }
    
    /** Special locals subnodes (children) */
    static class TreeModelChildren extends Children.Keys
                                   implements Runnable {// LazyEvaluator.Evaluable {
            
        private boolean             initialezed = false;
        private final Models.CompoundModel model;
        private final ColumnModel[]   columns;
        protected final TreeModelRoot      treeModelRoot;
        protected Object            object;
        protected final WeakHashMap> objectToNode = new WeakHashMap>();
        private final int[]         evaluated = { 0 }; // 0 - not yet, 1 - evaluated, -1 - timeouted
        private RefreshingInfo      evaluatingRefreshingInfo;
        private Object[]            children_evaluated;
        private RefreshingInfo      refreshInfo = null;
        private boolean             refreshingStarted = true;

        private RequestProcessor.Task   task;
        private RequestProcessor        lastRp;
        
        protected static final Object WAIT_KEY = new Object();
        
        
        TreeModelChildren (
            Models.CompoundModel model,
            ColumnModel[]   columns,
            TreeModelRoot   treeModelRoot,
            Object          object
        ) {
            this.model = model;
            this.columns = columns;
            this.treeModelRoot = treeModelRoot;
            this.object = object;
        }
        
        @Override
        protected void addNotify () {
            if (initialezed) {
                //System.err.println("\n\nTreeModelChildren.addNotify() called more that once! Parent = "+getNode()+"\n\n");
                return ;
            }
            initialezed = true;
            refreshChildren (new RefreshingInfo(true));
        }
        
        @Override
        protected void removeNotify () {
            initialezed = false;
            setKeys (Collections.emptySet());
        }
        
        void refreshChildren (RefreshingInfo refreshSubNodes) {
            if (!initialezed) return;

            refreshLazyChildren(refreshSubNodes);
        }
        
        public void run() {
            RefreshingInfo rinfo;
            synchronized (evaluated) {
                refreshingStarted = false;
                rinfo = refreshInfo;
                if (evaluatingRefreshingInfo == null) {
                    evaluatingRefreshingInfo = refreshInfo;
                } else {
                    if (refreshInfo != null) {
                        evaluatingRefreshingInfo = evaluatingRefreshingInfo.mergeWith(refreshInfo);
                    }
                }
                refreshInfo = null; // reset after use
            }
            Object[] ch;
            try {
                ch = getModelChildren(rinfo);
            } catch (UnknownTypeException e) {
                ch = new Object [0];
                if (!(object instanceof String)) {
                    Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e);
                }
            } catch (ThreadDeath td) {
                throw td;
            } catch (Throwable t) {
                // recover from defect in getChildren()
                // Otherwise there would remain "Please wait..." node.
                Exceptions.printStackTrace(t);
                ch = new Object[0];
            }
            //evaluatedNotify.run();
            boolean fire;
            synchronized (evaluated) {
                int eval = evaluated[0];
                if (refreshingStarted) {
                    fire = false;
                } else {
                    fire = evaluated[0] == -1;
                    if (!fire) {
                        children_evaluated = ch;
                    } else {
                        evaluatingRefreshingInfo = null;
                    }
                    evaluated[0] = 1;
                    evaluated.notifyAll();
                }
                //System.err.println(this.hashCode()+" evaluateLazily() ready, evaluated[0] = "+eval+" => fire = "+fire+", refreshingStarted = "+refreshingStarted+", children_evaluated = "+(children_evaluated != null));
            }
            if (fire) {
                applyChildren(ch, rinfo, false);
            }
        }

        protected Object[] getModelChildren(RefreshingInfo refreshInfo) throws UnknownTypeException {
            //System.err.println("! getModelChildren("+object+", "+getNode()+")");
            int count = model.getChildrenCount (object);
            Object[] ch = model.getChildren (
                object,
                0,
                count
            );
            if (ch == null) {
                Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model+"\nreturned null children for parent '"+object+"'");
                ch = new Object[] {};
            }
            return ch;
        }

        protected Executor getModelAsynchronous() {
            return asynchronous(model, CALL.CHILDREN, object);
        }
        
        private void refreshLazyChildren (RefreshingInfo refreshInfo) {
            //System.err.println("\n!! refreshLazyChildren("+getNode()+") from:");
            //Thread.dumpStack();
            //System.err.println("");
            Executor exec = getModelAsynchronous();
            if (exec == AsynchronousModelFilter.CURRENT_THREAD) {
                Object[] ch;
                try {
                    ch = getModelChildren(refreshInfo);
                } catch (UnknownTypeException ex) {
                    ch = new Object [0];
                    if (!(object instanceof String)) {
                        Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, ex);
                    }
                }
                applyChildren(ch, refreshInfo, true);
                return ;
            }
            synchronized (evaluated) {
                evaluated[0] = 0;
                refreshingStarted = true;
                if (this.refreshInfo == null) {
                    this.refreshInfo = refreshInfo;
                } else {
                    this.refreshInfo = this.refreshInfo.mergeWith(refreshInfo);
                }
                //System.err.println(this.hashCode()+" refreshLazyChildren() started = true, evaluated = 0");
            }
            /*if (exec instanceof RequestProcessor) {
                // Have a single task for RP
                RequestProcessor rp = (RequestProcessor) exec;
                if (rp != lastRp) {
                    task = rp.create(this);
                    lastRp = rp;
                }
                task.schedule(0);
            } else {*/
                exec.execute(this);
            //}
            // It's refresh => do not check for this children already being evaluated
            //treeModelRoot.getChildrenEvaluator().evaluate(this, false);
            Object[] ch;
            synchronized (evaluated) {
                if (evaluated[0] != 1) {
                    try {
                        evaluated.wait(getChildrenRefreshWaitTime());
                    } catch (InterruptedException iex) {}
                    if (evaluated[0] != 1) {
                        evaluated[0] = -1; // timeout
                        ch = null;
                    } else {
                        ch = children_evaluated;
                    }
                } else {
                    ch = children_evaluated;
                }
                //System.err.println(this.hashCode()+" refreshLazyChildren() ending, evaluated[0] = "+evaluated[0]+", refreshingStarted = "+refreshingStarted+", children_evaluated = "+(children_evaluated != null)+", ch = "+(ch != null));
                // Do nothing when it's evaluated, but already unset.
                if (children_evaluated == null && evaluated[0] == 1) return;
                children_evaluated = null;
                if (ch != null) {
                    refreshInfo = evaluatingRefreshingInfo;
                    evaluatingRefreshingInfo = null;
                    //refreshInfo = this.refreshInfo;
                    //this.refreshInfo = null;
                }
            }
            if (ch == null) {
                applyWaitChildren();
            } else {
                applyChildren(ch, refreshInfo, true);
            }
        }

        private static AtomicLong lastChildrenRefresh = new AtomicLong(0);
        
        private static long getChildrenRefreshWaitTime() {
            long now = System.currentTimeMillis();
            long last = lastChildrenRefresh.getAndSet(now);
            if ((now - last) < 1000) {
                // Refreshes in less than a second - the system needs to respond fast
                return 1;
            } else {
                return 200;
            }
        }
        
        private void applyChildren(final Object[] ch, RefreshingInfo refreshInfo, boolean doSetObject) {
            //System.err.println(this.hashCode()+" applyChildren("+refreshSubNodes+")");
            //System.err.println("applyChildren("+Arrays.toString(ch)+", "+doSetObject+")");
            int i, k = ch.length; 
            for (i = 0; i < k; i++) {
                if (ch [i] == null) {
                    throw new NullPointerException("Null child at index "+i+", parent: "+object+", model: "+model+"\nAll children are: "+Arrays.toString(ch));
                }
                if (doSetObject) {
                    WeakReference wr;
                    synchronized (objectToNode) {
                        wr = objectToNode.get(ch [i]);
                    }
                    if (wr == null) continue;
                    TreeModelNode tmn = wr.get ();
                    if (tmn == null) continue;
                    if (refreshInfo == null || refreshInfo.isRefreshSubNodes(ch[i])) {
                        tmn.setObject (ch [i]);
                    } else {
                        tmn.setObjectNoRefresh(ch[i]);
                    }
                }
            }
            setKeys (ch);

            SwingUtilities.invokeLater (new Runnable () {
                public void run () {
                    int i, k = ch.length;
                    for (i = 0; i < k; i++)
                        expandIfSetToExpanded(ch[i]);
                }
            });
        }
        
        protected void expandIfSetToExpanded(Object child) {
            try {
                DefaultTreeExpansionManager.get(model).setChildrenToActOn(getTreeDepth());
                if (model.isExpanded (child)) {
                    TreeFeatures treeTable = treeModelRoot.getTreeFeatures ();
                    if (treeTable != null && treeTable.isExpanded(object)) {
                        // Expand the child only if the parent is expanded
                        treeTable.expandNode (child);
                    }
                }
            } catch (UnknownTypeException ex) {
            }
        }
        
        private Integer depth;
        
        Integer getTreeDepth() {
            Node p = getNode();
            if (p == null) {
                return 0;
            } else if (depth != null) {
                return depth;
            } else {
                int d = 1;
                while ((p = p.getParentNode()) != null) d++;
                depth = Integer.valueOf(d);
                return depth;
            }
        }
        
        private void applyWaitChildren() {
            //System.err.println(this.hashCode()+" applyWaitChildren()");
            setKeys(new Object[] { WAIT_KEY });
        }
        
//        protected void destroyNodes (Node[] nodes) {
//            int i, k = nodes.length;
//            for (i = 0; i < k; i++) {
//                TreeModelNode tmn = (TreeModelNode) nodes [i];
//                String name = null;
//                try {
//                    name = model.getDisplayName (tmn.object);
//                } catch (UnknownTypeException e) {
//                }
//                if (name != null)
//                    nameToChild.remove (name);
//            }
//        }
        
        public Node[] createNodes (Object object) {
            if (object == WAIT_KEY) {
                AbstractNode n = new AbstractNode(Children.LEAF);
                n.setName(NbBundle.getMessage(TreeModelNode.class, "WaitNode"));
                n.setIconBaseWithExtension("org/netbeans/modules/viewmodel/wait.gif");
                return new Node[] { n };
            }
            if (object instanceof Exception)
                return new Node[] {
                    new ExceptionNode ((Exception) object)
                };
            TreeModelNode tmn = new TreeModelNode (
                model,
                columns,
                treeModelRoot, 
                object
            );
            //System.err.println("created node for ("+object+") = "+tmn);
            synchronized (objectToNode) {
                objectToNode.put (object, new WeakReference(tmn));
            }
            return new Node[] {tmn};
        }

        @Override
        protected void destroyNodes(Node[] nodes) {
            super.destroyNodes(nodes);
            for (Node n : nodes) {
                if (n instanceof TreeModelNode) {
                    TreeModelNode tmn = (TreeModelNode) n;
                    treeModelRoot.unregisterNode (tmn.object, tmn);
                    if (tmn.areChildrenInitialized()) {
                        Node[] childrenNodes;
                        try {
                            // Use Children.testNodes() that does not re-create nodes.
                            // https://netbeans.org/bugzilla/show_bug.cgi?id=199202
                            java.lang.reflect.Method testNodes = Children.class.getDeclaredMethod("testNodes"); // NOI18N
                            testNodes.setAccessible(true);
                            childrenNodes = (Node[]) testNodes.invoke(tmn.getChildren());
                        } catch (Exception ex) {
                            Logger.getLogger(TreeModelNode.class.getName()).log(Level.INFO, "Children.testNodes() method access problem:", ex); // NOI18N
                            childrenNodes = tmn.getChildren().getNodes();
                        }
                        if (childrenNodes != null) {
                            destroyNodes(childrenNodes);
                        }
                    }
                }
            }
        }

        public static class RefreshingInfo {

            protected boolean refreshSubNodes;

            public RefreshingInfo(boolean refreshSubNodes) {
                this.refreshSubNodes = refreshSubNodes;
            }

            public RefreshingInfo mergeWith(RefreshingInfo rinfo) {
                this.refreshSubNodes = this.refreshSubNodes || rinfo.refreshSubNodes;
                return this;
            }

            public boolean isRefreshSubNodes(Object child) {
                return refreshSubNodes;
            }
        }
    } // TreeModelChildren

    private static final class IndexImpl extends Index.Support {

        private Models.CompoundModel model;
        private Object object;
        private Node node;

        IndexImpl(Models.CompoundModel model, Object object) {
            this.model = model;
            this.object = object;
        }

        void setNode(Node node) {
            this.node = node;
        }

        @Override
        public Node[] getNodes() {
            return node.getChildren().getNodes();
        }

        @Override
        public int getNodesCount() {
            return node.getChildren().getNodesCount();
        }

        @Override
        public void reorder(int[] perm) {
            try {
                model.reorder(object, perm);
                fireChangeEvent(new ChangeEvent(this));
            } catch (UnknownTypeException ex) {
                if (!(object instanceof String)) {
                    Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, ex);
                }
            }
        }

        void fireChange() {
            fireChangeEvent(new ChangeEvent(this));
        }

    }

    private static class LazyChildrenFactory implements Callable {

        private final Models.CompoundModel model;
        private final ColumnModel[] columns;
        private final TreeModelRoot treeModelRoot;
        private final Object object;
        private volatile boolean childrenCreated = false;

        LazyChildrenFactory(final Models.CompoundModel model,
                            final ColumnModel[] columns,
                            final TreeModelRoot treeModelRoot,
                            final Object object) {
            this.model = model;
            this.columns = columns;
            this.treeModelRoot = treeModelRoot;
            this.object = object;
        }

        @Override
        public Children call() throws Exception {
            childrenCreated = true;
            return createChildren(model, columns, treeModelRoot, object);
        }

        boolean areChildrenCreated() {
            return childrenCreated;
        }
        
    }

    // Adaptive property refresh time. Belongs to MyProperty, but can not be there since it's static :-(
    private static AtomicLong lastPropertyRefresh = new AtomicLong(0);

    private static long getPropertyRefreshWaitTime() {
        long now = System.currentTimeMillis();
        long last = lastPropertyRefresh.getAndSet(now);
        if ((now - last) < 1000) {
            // Refreshes in less than a second - the system needs to respond fast
            return 1;
        } else {
            return 25;
        }
    }

    private class MyProperty extends PropertySupport implements Runnable { //LazyEvaluator.Evaluable {
        
        private final String      id;
        private final String      propertyId;
        private final ColumnModel columnModel;
        private final boolean nodeColumn;
        private TreeModelRoot treeModelRoot;
        private final int[] evaluated = { 1 }; // 0 - not yet, 1 - evaluated, -1 - timeouted
        
        
        MyProperty (
            ColumnModel columnModel, TreeModelRoot treeModelRoot
        ) {
            super (
                columnModel.getID (),
                (columnModel.getType() == null) ? String.class : columnModel.getType (),
                Actions.cutAmpersand(columnModel.getDisplayName ()),
                columnModel.getShortDescription (), 
                true,
                true
            );
            this.nodeColumn = columnModel.getType() == null;
            this.treeModelRoot = treeModelRoot;
            if (columnModel instanceof HyperColumnModel) {
                propertyId = columnModel.getID(); // main column ID
                this.columnModel = ((HyperColumnModel) columnModel).getSpecific();
                id = this.columnModel.getID ();   // specific column ID
            } else {
                id = propertyId = columnModel.getID ();
                this.columnModel = columnModel;
            }
            //System.err.println("new MyProperty("+TreeModelNode.this+", "+id+") = "+this);
        }
        
        // A hack - see org/netbeans/modules/debugger/jpda/ui/models/ValuePropertyEditor.java
        boolean forcedReadOnly;
        void forceNotEditable() {
            forcedReadOnly = true;
        }

        /* Can write the value of the property.
        * Returns the value passed into constructor.
        * @return true if the read of the value is supported
        */
        @Override
        public boolean canWrite () {
            if (forcedReadOnly) {
                return false;
            }
            synchronized (properties) {
                Boolean canWrite = (Boolean) properties.get(id + "#canWrite");
                if (canWrite != null) {
                    return canWrite;
                }
            }
            boolean canEdit;
            try {
                canEdit = model.canEditCell(object, columnModel.getID());
            } catch (UnknownTypeException ex) {
                canEdit = false;
            }
            if (!canEdit) {
                if (nodeColumn) {
                    canEdit = false;
                } else {
                    try {
                        canEdit = !model.isReadOnly (object, columnModel.getID ());
                    } catch (UnknownTypeException e) {
                        if (!(object instanceof String)) {
                            Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Column id:" + columnModel.getID ()+"\nModel: "+model, e);
                        }
                        canEdit = false;
                    }
                }
            }
            synchronized (properties) {
                properties.put(id + "#canWrite", canEdit);
            }
            return canEdit;
        }
        
        public void run() {
            Object value = "";
            String htmlValue = null;
            Object nonHtmlValue = null;
            try {
                //System.err.println("getValueAt("+object+", "+id+") of node "+TreeModelNode.this);
                value = model.getValueAt (object, id);
                nonHtmlValue = value;
                boolean hasHTML = model.hasHTMLValueAt(object, id);
                if (hasHTML) {
                    htmlValue = model.getHTMLValueAt(object, id);
                }
                //System.err.println("  Value of ("+object+") executed in "+Thread.currentThread()+" is "+value);
                //System.out.println("  evaluateLazily("+TreeModelNode.this.getDisplayName()+", "+id+"): have value = "+value);
                //System.out.println("      object = "+object+" class = "+((object != null) ? object.getClass().toString() : "null"));
                if (!hasHTML && (value instanceof String)) { // For backward compatibility
                    htmlValue = htmlValue ((String) value);
                    nonHtmlValue = removeHTML ((String) value);
                }
            } catch (UnknownTypeException e) {
                if (!(object instanceof String)) {
                    e.printStackTrace ();
                    System.out.println("  Column id:" + columnModel.getID ());
                    System.out.println (model);
                    System.out.println ();
                }
            } catch (Throwable t) {
                Exceptions.printStackTrace(t);
            } finally {
                //evaluatedNotify.run();
                boolean fire;
                synchronized (properties) {
                    properties.put (id, nonHtmlValue);
                    properties.put (id + "#html", htmlValue);
                    synchronized (evaluated) {
                        fire = evaluated[0] == -1;
                        evaluated[0] = 1;
                        //System.err.println("  value of ("+TreeModelNode.this.getDisplayName()+", "+id+") was evaluated to "+value+", evaluated = "+evaluated[0]);
                        evaluated.notifyAll();
                    }
                }
                //System.out.println("\nTreeModelNode.evaluateLazily("+TreeModelNode.this.getDisplayName()+", "+id+"): value = "+value+", fire = "+fire);
                if (fire) {
                    firePropertyChange (propertyId, null, value);
                }
                
            }
        }
        
        public synchronized Object getValue () { // Sync the calls
            //System.err.println("TreeModelNode("+object+").getValue("+id+")...");
            if (nodeColumn) {
                return TreeModelNode.this.getDisplayName();
            }
            // 1) return value from cache
            synchronized (properties) {
                //System.err.println("getValue("+TreeModelNode.this.getDisplayName()+", "+id+"): contains = "+properties.containsKey (id)+", value = "+properties.get (id)+" property object = "+this);
                if (properties.containsKey (id)) {
                    return properties.get (id);
                }
                synchronized (evaluated) {
                    //System.err.println("  value of ("+TreeModelNode.this.getDisplayName()+", "+id+") is being evaluated = "+(evaluated[0] != 1)+", evaluated = "+evaluated[0]);
                    if (evaluated[0] != 1) { // is being evaluated...
                        //System.err.println("  value is being evaluated...");
                        if (getValueType() != null && getValueType() != String.class) {
                            return null;
                        } else {
                            return EVALUATING_STR;
                        }
                    }
                }
            }
            
            Executor exec = asynchronous(model, CALL.VALUE, object);

            if (exec == AsynchronousModelFilter.CURRENT_THREAD) {
                return getTheValue();
            }

            synchronized (evaluated) {
                evaluated[0] = 0;
                //System.err.println("Evaluated of ("+TreeModelNode.this.getDisplayName()+", "+id+"): evaluated = "+evaluated[0]);
            }
            /*if (exec instanceof RequestProcessor) {
                RequestProcessor rp = (RequestProcessor) exec;
                if (rp != lastRp) {
                    task = rp.create(this);
                    lastRp = rp;
                }
                task.schedule(0);
            } else {*/
            //System.err.println("getTheValue of ("+object+", "+id+") executed in "+exec);
                exec.execute(this);
            //}
            //treeModelRoot.getValuesEvaluator().evaluate(this);
            
            Object ret = null;
            
            synchronized (evaluated) {
                if (evaluated[0] != 1) {
                    try {
                        evaluated.wait(getPropertyRefreshWaitTime());
                    } catch (InterruptedException iex) {}
                    if (evaluated[0] != 1) {
                        evaluated[0] = -1; // timeout
                        //System.err.println("Timeout of ("+TreeModelNode.this.getDisplayName()+", "+id+"): evaluated = "+evaluated[0]);
                        ret = EVALUATING_STR;
                    }
                }
            }
            if (ret == null) {
                synchronized (properties) {
                    ret = properties.get(id);
                }
            }
            
            if (ret == EVALUATING_STR &&
                    getValueType() != null && getValueType() != String.class) {
                ret = null; // Must not provide String when the property type is different.
                            // htmlDisplayValue attr will assure that the Evaluating str is there.
            }
            return ret;
        }

        private Object getTheValue() {
            Object value = "";
            String htmlValue = null;
            Object nonHtmlValue = null;
            try {
                value = model.getValueAt (object, id);
                nonHtmlValue = value;
                boolean hasHTML = model.hasHTMLValueAt(object, id);
                if (hasHTML) {
                    htmlValue = model.getHTMLValueAt(object, id);
                }
                //System.err.println("  Value of ("+object+") executed in "+Thread.currentThread()+" is "+value);
                //System.out.println("  evaluateLazily("+TreeModelNode.this.getDisplayName()+", "+id+"): have value = "+value);
                //System.out.println("      object = "+object+" class = "+((object != null) ? object.getClass().toString() : "null"));
                if (!hasHTML && (value instanceof String)) { // For backward compatibility
                    htmlValue = htmlValue ((String) value);
                    nonHtmlValue = removeHTML ((String) value);
                }
            } catch (UnknownTypeException e) {
                if (!(object instanceof String)) {
                    Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model+"\n,Column id:" + columnModel.getID (), e);
                }
            } finally {
                synchronized (properties) {
                    properties.put (id, nonHtmlValue);
                    properties.put (id + "#html", htmlValue);
                }
            }
            return value;
        }
        
        @Override
        public Object getValue (String attributeName) {
            if (attributeName.equals ("htmlDisplayValue")) {
                if (nodeColumn) {
                    return TreeModelNode.this.getHtmlDisplayName();
                }
                synchronized (evaluated) {
                    if (evaluated[0] != 1) {
                        return HTML_START_TAG+""+EVALUATING_STR+""+HTML_END_TAG;
                    }
                }
                synchronized (properties) {
                    return properties.get (id + "#html");
                }
            }
            if (attributeName.equals("suppressCustomEditor")) {
                // Do not invoke custom property editor when we render the cell.
                try {
                    if (model.canRenderCell(object, id)) {
                        return Boolean.TRUE;
                    }
                } catch (UnknownTypeException ex) {
                }
            }
            return super.getValue (attributeName);
        }

        @Override
        public String getShortDescription() {
            if (nodeColumn) {
                return TreeModelNode.this.getShortDescription();
            }
            synchronized (properties) {
                if (!properties.containsKey(id)) {
                    return null; // The same as value => EVALUATING_STR
                }
                String shortDescription = (String) properties.get (id + "#shortDescription");
                if (shortDescription != null) {
                    return shortDescription;
                }
            }
            Executor exec = asynchronous(model, CALL.SHORT_DESCRIPTION, object);
            
            if (exec == AsynchronousModelFilter.CURRENT_THREAD) {
                return updateShortDescription();
            } else {
                exec.execute(new Runnable() {
                    public void run() {
                        updateShortDescription();
                        firePropertyChange(propertyId, null, null);
                    }
                });
                return null;
            }
        }

        private String updateShortDescription() {
            try {
                javax.swing.JToolTip tooltip = new javax.swing.JToolTip();
                String sd = null;
                try {
                    tooltip.putClientProperty("getShortDescription", object); // NOI18N
                    Object tooltipObj = model.getValueAt(object, id);
                    if (tooltipObj != null) {
                        sd = adjustHTML(tooltipObj.toString());
                    }
                    return sd;
                } finally {
                    // We MUST clear the client property, Swing holds this in a static reference!
                    tooltip.putClientProperty("getShortDescription", null); // NOI18N
                    synchronized (properties) {
                        properties.put (id + "#shortDescription", sd);
                    }
                }
            } catch (UnknownTypeException e) {
                // Ignore models that do not define tooltips for values.
                return null;
            }
        }
        
        public void setValue (final Object value) throws IllegalAccessException,
        IllegalArgumentException, java.lang.reflect.InvocationTargetException {
            Executor exec = asynchronous(model, CALL.VALUE, object);
            if (exec == AsynchronousModelFilter.CURRENT_THREAD) {
                try {
                    setTheValue(value);
                } catch (ThreadDeath td) {
                    throw td;
                } catch (Throwable t) {
                    throw new InvocationTargetException(t);
                }
            } else {
                exec.execute(new Runnable() {
                    public void run() {
                        setTheValue(value);
                    }
                });
            }
        }

        private void setTheValue(final Object value) {
            try {
                Object v = value;
                model.setValueAt (object, id, v);
                v = model.getValueAt(object, id); // Store the new value
                String htmlValue = null;
                Object nonHtmlValue = v;
                boolean hasHTML = model.hasHTMLValueAt(object, id);
                if (hasHTML) {
                    htmlValue = model.getHTMLValueAt(object, id);
                }
                if (!hasHTML && (v instanceof String)) { // For backward compatibility
                    htmlValue = htmlValue ((String) v);
                    nonHtmlValue = removeHTML ((String) v);
                }
                synchronized (properties) {
                    properties.put (id, nonHtmlValue);
                    properties.put (id + "#html", htmlValue);
                }
                firePropertyChange (propertyId, null, null);
            } catch (UnknownTypeException e) {
                Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Column id:" + columnModel.getID ()+"\nModel: "+model, e);
            }
        }
        
        @Override
        public PropertyEditor getPropertyEditor () {
            PropertyEditor pe = null;
            try {
                pe = model.getPropertyEditor(object, id);
            } catch (UnknownTypeException ex) {
                Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Column id:" + columnModel.getID ()+"\nModel: "+model, ex);
            }
            if (pe == null) {
                pe = columnModel.getPropertyEditor ();
            }
            if (pe != null) {
                return pe;
            } else {
                return super.getPropertyEditor();
            }
        }

        @Override
        public String toString() {
            return super.toString() + ", Value = "+properties.get(id);
        }
        
    }

    /** The single-threaded evaluator of lazy models. *//*
    static class LazyEvaluator implements Runnable {
        
        /** Release the evaluator task after this time. *//*
        private static final long EXPIRE_TIME = 1000L;

        private final List objectsToEvaluate = new LinkedList();
        private Evaluable currentlyEvaluating;
        private Task evalTask;
        
        public LazyEvaluator(RequestProcessor prefferedRequestProcessor) {
            if (prefferedRequestProcessor == null) {
                prefferedRequestProcessor = new RequestProcessor("Debugger Values Evaluator", 1); // NOI18N
            }
            evalTask = prefferedRequestProcessor.create(this, true);
        }
        
        public void evaluate(Evaluable eval) {
            evaluate(eval, true);
        }
        
        public void evaluate(Evaluable eval, boolean checkForEvaluating) {
            synchronized (objectsToEvaluate) {
                for (Iterator it = objectsToEvaluate.iterator(); it.hasNext(); ) {
                    if (eval == it.next()) return ; // Already scheduled
                }
                if (checkForEvaluating && currentlyEvaluating == eval) return ; // Is being evaluated
                objectsToEvaluate.add(eval);
                objectsToEvaluate.notify();
                if (evalTask.isFinished()) {
                    evalTask.schedule(0);
                }
            }
        }

        public void run() {
            while(true) {
                Evaluable eval;
                synchronized (objectsToEvaluate) {
                    if (objectsToEvaluate.size() == 0) {
                        try {
                            objectsToEvaluate.wait(EXPIRE_TIME);
                        } catch (InterruptedException iex) {
                            return ;
                        }
                        if (objectsToEvaluate.size() == 0) { // Expired
                            return ;
                        }
                    }
                    eval = (Evaluable) objectsToEvaluate.remove(0);
                    currentlyEvaluating = eval;
                }
                Runnable evaluatedNotify = new Runnable() {
                    public void run() {
                        synchronized (objectsToEvaluate) {
                            currentlyEvaluating = null;
                        }
                    }
                };
                eval.evaluateLazily(evaluatedNotify);
            }
        }

        public interface Evaluable {

            public void evaluateLazily(Runnable evaluatedNotify);

        }

    }*/

}