tufts.vue.ds.DataTree.java Source code

Java tutorial

Introduction

Here is the source code for tufts.vue.ds.DataTree.java

Source

/*
* Copyright 2003-2010 Tufts University  Licensed under the
 * Educational Community 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.osedu.org/licenses/ECL-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.
 */

// TODO: see http://publicobject.com/glazedlists/ as a possible baseline for a much
// fancier data interface -- tabular, and big, but may have enough flexibility
// to still be handy.

package tufts.vue.ds;

import tufts.vue.LWSelection;
import tufts.vue.MapViewer;
import tufts.vue.VUE;
import tufts.vue.DEBUG;
import tufts.vue.Resource;
import tufts.vue.VueResources;
import tufts.vue.LWComponent;
import tufts.vue.LWNode;
import tufts.vue.LWLink;
import tufts.vue.LWMap;
import tufts.vue.LWKey;
import tufts.vue.gui.GUI;
import tufts.vue.gui.Widget;
import tufts.Util;
import tufts.vue.VueConstants;

import java.util.List;
import java.util.*;
import java.net.URL;
import java.awt.*;
import java.awt.event.*;
import java.awt.dnd.*;
import java.awt.geom.Point2D;
import java.awt.geom.RectangularShape;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.tree.*;

import com.google.common.collect.*;

/**
 * UI component for browsing the Fields and Rows of a fully loaded
 * Schema, providing the status of data elements releative to the
 * currently active map, code for adding new nodes to the current map,
 * and initiating drags of fields or rows destined for a map.
 *
 * @version $Revision: 1.109 $ / $Date: 2010-02-03 19:13:16 $ / $Author: mike $
 * @author  Scott Fraize
 */

public class DataTree extends javax.swing.JTree implements DragGestureListener {
    private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(DataTree.class);

    protected final Schema mSchema;

    private DataNode mRootNode;
    protected DataNode mAllRowsNode;
    private DataNode mSelectedSearchNode;
    private final JLabel mNewRowsLabel = new JLabel();
    private final JLabel mChangedRowsLabel = new JLabel();
    private final JCheckBox mNewRowsCheckBox = new JCheckBox();
    private final JCheckBox mChangedRowsCheckBox = new JCheckBox();
    private final AbstractButton mUpdateButton = new JButton(
            VueResources.getString("dockWindow.contentPanel.sync.updateMap"));
    private final AbstractButton mSendToMapButton = new JButton("Send to Map");
    private final DefaultTreeModel mTreeModel;
    private final static Color MEDIUM_DARK_GRAY = new Color(96, 96, 96);
    private final static boolean DEBUG_LOCAL = false;

    private Thread mAnnotateThread;

    private static final MapListener ActiveMapListener = new MapListener();
    private static final java.util.concurrent.atomic.AtomicBoolean FirstInstance = new java.util.concurrent.atomic.AtomicBoolean(
            true);

    private static final Collection<DataTree> ActiveTrees = new java.util.concurrent.CopyOnWriteArrayList();
    private static volatile DataTree ForegroundTree;
    private static volatile LWMap mActiveMap;

    private static volatile MapViewer mActiveViewer;
    private static volatile Collection<LWComponent> ActiveMapDataNodes = Collections.EMPTY_LIST;

    public static final class MapListener implements LWComponent.Listener, LWSelection.Listener {

        private static boolean mDataEventWasSeen;

        /** if the active map changes, we need to wake the annotation thread to re-annotate against the newly active map,
         * as well as start listening for changes in the active map for running future annotation updates */
        public void activeChanged(tufts.vue.ActiveEvent e, final LWMap map) {
            if (mActiveMap == map)
                return;

            if (mActiveMap != null) {
                mActiveMap.removeLWCListener(this);
                mActiveViewer.getSelection().removeListener(this);
            }

            mActiveMap = map;
            mActiveViewer = VUE.getActiveViewer();

            if (map == null)
                return;

            kickOffAnnotations();

            mActiveMap.addLWCListener(this);
            mActiveViewer.getSelection().addListener(this);

        }

        public void LWCChanged(tufts.vue.LWCEvent e) {

            if (mActiveMap == null) {
                Log.warn("LWCEvent w/no active map: " + e);
                return;
            }

            //if (DEBUG.ANNOTATE) Log.debug("SCANNING DATA EVENT: " + e + "; seenOne=" + mDataEventWasSeen);

            // TODO: pull one copy of all the active map's descendents, once, in the AWT thread,
            // before all the annotate's kick off.  Will probably need to have a single active map
            // listener with a list of all active data-tree's instead of each tree listening
            // to the active map itself.

            if (e.key == LWKey.UserActionCompleted && mDataEventWasSeen) {
                // technically, don't need to check after ANY action has been completed:
                // only if a data node was added/removed from the map.  todo: we'll need
                // a data-changed LWCEvent.
                if (DEBUG.ANNOTATE)
                    Log.debug("RUNNING ANNOTATE on: " + e);
                kickOffAnnotations();
                mDataEventWasSeen = false;
            } else if (isDataEvent(e)) {
                mDataEventWasSeen = true;
                if (DEBUG.ANNOTATE)
                    Log.debug("   FOUND DATA EVENT: " + e + "; seenOne=" + mDataEventWasSeen);
            }
        }

        private void kickOffAnnotations() {

            if (DEBUG.ANNOTATE)
                Log.debug("kicking off annotations for: " + Util.tags(ActiveTrees));

            if (ActiveTrees.size() > 0) {
                loadGlobalDataForAnnotations();
                for (DataTree tree : ActiveTrees)
                    tree.kickAnnotate();
            }
        }

        public void selectionChanged(LWSelection selection) {

            if (selection.isEmpty()) {
                if (ActiveTrees.size() > 0) {
                    for (DataTree tree : ActiveTrees)
                        tree.setSelectionPath(null);
                }
            }

        }

    }

    // note: this should be called from the AWT thread as it's going to access the main map model
    private static void loadGlobalDataForAnnotations() {
        if (mActiveMap == null) {
            mActiveMap = VUE.getActiveMap();
            mActiveViewer = VUE.getActiveViewer();
        }
        if (DEBUG.Enabled)
            Log.debug("loading global annotation data from map " + mActiveMap);

        if (mActiveMap != null) {

            final Collection<LWComponent> allNodes = mActiveMap.getAllDescendents();
            final Collection<LWComponent> dataNodes = new ArrayList(allNodes.size());

            for (LWComponent c : allNodes) {
                if (c.isDataNode() && c instanceof LWNode) // leave out data-links for now
                    dataNodes.add(c);
            }
            ActiveMapDataNodes = dataNodes;

        } else {
            ActiveMapDataNodes = Collections.EMPTY_LIST;
        }

    }

    private static boolean isDataEvent(tufts.vue.LWCEvent e) {
        // we need to check for any childrenAdded/childrenRemoved right now, just in case ANY of them were data nodes
        return e.key == LWKey.DataUpdate || e.key == LWKey.HierarchyChanging || e.key == LWKey.Created;
    }

    // note: this is usually NOT called from the AWT thread -- is called from a data-source load thread
    public static JComponent create(Schema schema) {
        final DataTree tree = new DataTree(schema);

        //tree.setBorder(new LineBorder(Color.red, 4));

        try {
            tree.restoreAnyExpandedState();
        } catch (Throwable t) {
            Log.warn(t); // an assistave measure only -- non fatal
        }

        GUI.invokeOnEDT(new Runnable() {
            public void run() {
                if (FirstInstance.getAndSet(false)) {
                    if (DEBUG.ANNOTATE)
                        Log.debug("FIRST RUN");
                    // Listen for changes to the ActiveMap
                    VUE.addActiveListener(LWMap.class, ActiveMapListener);

                    // Simulate an active-map changed event to make sure we're listening to the active map.
                    ActiveMapListener.activeChanged(null, VUE.getActiveMap());
                    // ensure the global data is loaded 1st time from the ActiveMap
                    loadGlobalDataForAnnotations();
                }
                tree.kickAnnotate();
                ActiveTrees.add(tree); // make sure to do this last -- activeChanged above will kick all 1st time
            }
        });

        return buildControllerUI(tree);
    }

    private void sendSelectedToMap() {
        sendToMap(getSelectedNode(), mActiveMap);
    }

    private void addMissingRowsToMap() {
        // failsafe: tho the Schema and our tree nodes should already
        // be updated, make absolutely certian we're current to the
        // active map by running adding new rows based on our detection
        // of the rows already in the map.
        VUE.activateWaitCursor();
        try {
            annotateForMap(mActiveMap);
            addMissingRowsToMap(mActiveMap);
        } catch (Throwable t) {
            Log.warn("addMissingRowsToMap", t);
        } finally {
            VUE.clearWaitCursor();
        }
    }

    private void applyChangesToMap() {
        // failsafe: tho the Schema and our tree nodes should already
        // be updated, make absolutely certian we're current to the
        // active map by running adding new rows based on our detection
        // of the rows already in the map.
        VUE.activateWaitCursor();
        try {
            annotateForMap(mActiveMap);
            applyDataUpdatesToMap(mActiveMap);
        } catch (Throwable t) {
            Log.warn("applyChangesToMap", t);
        } finally {
            VUE.clearWaitCursor();
        }
    }

    private void updateMap() {
        // failsafe: tho the Schema and our tree nodes should already
        // be updated, make absolutely certian we're current to the
        // active map by running adding new rows based on our detection
        // of the rows already in the map.

        VUE.activateWaitCursor();
        try {
            annotateForMap(mActiveMap);

            if (mNewRowsCheckBox.isSelected()) {
                addMissingRowsToMap(mActiveMap);

                final LWSelection newNodes = VUE.getSelection().clone();

                if (mSchema.isMatrixDataSet) {
                    GUI.invokeAfterAWT(new Runnable() {
                        public void run() {
                            applyMatrixRelations(newNodes);
                        }
                    });
                }
            }

            if (mChangedRowsCheckBox.isSelected()) {
                applyDataUpdatesToMap(mActiveMap);
                final LWSelection newNodes = VUE.getSelection().clone();

                if (newNodes != null) {
                    VUE.getSelection().add(newNodes);
                }

                if (mSchema.isMatrixDataSet)
                    GUI.invokeAfterAWT(new Runnable() {
                        public void run() {
                            applyMatrixRelations(newNodes);
                        }
                    });
            }
        } catch (Throwable t) {
            Log.warn("updateMap", t);
        } finally {
            VUE.clearWaitCursor();
        }

    }

    int call = 0;

    public synchronized void applyMatrixRelations(List<LWComponent> newNodes) {
        //System.out.println("APPLY MATRIX RELATIONS : " + call++);
        List<MatrixRelationship> relations = mSchema.matrixRelations;
        //for (MatrixRelationship relation: relations)
        //   System.out.println("Relations : " + relation.getFromLabel() + ", " + relation.getToLabel());       

        for (LWComponent newNode : newNodes) {
            String trueName = newNode.getRawData().getString(Schema.MATRIX_NAME_FIELD);

            for (MatrixRelationship relation : relations) {
                if (relation.getFromLabel().equals(trueName) || relation.getToLabel().equals(trueName)) {
                    for (DataNode n : mAllRowsNode.getChildren()) {
                        RowNode rn = (RowNode) n;

                        if (rn.isMapPresent()) {

                            String potentialTargetName = rn.getRow().getValue(Schema.MATRIX_NAME_FIELD);
                            if (potentialTargetName.equals(relation.getToLabel())
                                    || potentialTargetName.equals(relation.getFromLabel())) {
                                //System.out.println("Relation : " + relation.getFromLabel() + "," + relation.getToLabel());
                                //try to find a place to draw it.
                                final Collection<LWComponent> searchSet = VUE.getActiveViewer().getMap()
                                        .getAllDescendents(LWComponent.ChildKind.EDITABLE);
                                final Criteria criteria = dataNodeToSearchCriteria(rn);
                                SmartSearch currentSearch = new SmartSearch();
                                currentSearch.addCriteria(criteria);
                                List<LWComponent> hits = currentSearch.search(searchSet);

                                for (LWComponent hit : hits) {
                                    LWLink link = null;
                                    LWLink link2 = null;

                                    if (newNode.getLabel().equals(hit.getLabel()))
                                        continue;

                                    if (relation.getFromLabel().equals(trueName) && !newNode.hasDirectedLinkTo(hit))
                                        link = new LWLink(newNode, hit);

                                    if (relation.getToLabel().equals(trueName) && !hit.hasDirectedLinkTo(newNode))
                                        link2 = new LWLink(hit, newNode);

                                    LWSelection sel = VUE.getSelection();
                                    VUE.getSelection().clear();

                                    if (link != null) {
                                        link.setLabel(relation.getRelationLabel());
                                        link.setAsDataLink(relation.getRelationLabel());
                                        VUE.getActiveViewer().getMap().add(link);
                                        VUE.getSelection().add(link);
                                        //System.out.println("Add Link 1 : " + link.toString() + " :: " + call + " :: " + newNode.toString() + " ::: " + hit.toString());
                                    }

                                    if (link2 != null) {
                                        link2.setLabel(relation.getRelationLabel());
                                        link2.setAsDataLink(relation.getRelationLabel());
                                        VUE.getActiveViewer().getMap().add(link2);
                                        VUE.getSelection().add(link2);
                                        //System.out.println("Add Link 2 : " + link2.toString() + " :: " + call + " :: " + newNode.toString() + " ::: " + hit.toString());
                                    }

                                    if (hit.hasMultipleLinksTo(newNode) && VUE.getSelection().size() > 0)
                                        tufts.vue.Actions.LinkMakeQuadCurved.act();

                                }
                            }
                        }
                    } // for each data node

                }
            }
        }
    }

    private void enableUpdateButton() {
        mUpdateButton.setEnabled((mNewRowsCheckBox.isEnabled() && mNewRowsCheckBox.isSelected())
                || (mChangedRowsCheckBox.isEnabled() && mChangedRowsCheckBox.isSelected()));
    }

    /*
     * This will find all nodes on the map where fresher/newer data is in the given
     * data-set and update those nodes with the new data.  When done, it will leave the
     * selection set to all nodes that have been updated with new data.
     */

    private void applyDataUpdatesToMap(final LWMap map) {
        final Map<String, DataRow> freshData = new HashMap();
        final Field keyField = mSchema.getKeyField();
        final String keyFieldName = keyField.getName();

        for (DataNode n : mAllRowsNode.getChildren()) {
            final DataRow row = n.getRow();
            if (row.isContextChanged()) {
                //Log.debug("Context changed: " + Util.tag(row));
                String keyValue = row.getValue(keyField);
                freshData.put(keyValue, row);
            }
        }

        if (DEBUG.Enabled)
            Log.debug("Found " + freshData.size() + " data rows with newer data for map");

        final Collection<LWComponent> nodes = map.getAllDescendents();
        final Collection<LWComponent> patched = new ArrayList();

        for (LWComponent c : nodes) {
            if (c.isDataRow(mSchema)) {
                DataRow newRow = freshData.get(c.getDataValue(keyFieldName));
                if (newRow != null) {
                    //Log.debug("patching " + c);
                    c.setDataMap(newRow.getData());
                    patched.add(c);
                }
            }
        }

        if (DEBUG.Enabled)
            Log.debug("Updated " + patched.size() + " nodes with fresh data");

        // Note: kicking the annotation may no longer be required, as we also listen for
        // changes to the map to kick annotations, but that code isn't as smart as it
        // could be (though actually, it generally does overkill -- annotation happens
        // more often than it need be).

        kickAnnotate();

        VUE.getSelection().setTo(patched);

        map.getUndoManager().mark(String.format("Update %d Data Nodes", patched.size()));
    }

    private static JComponent buildControllerUI(final DataTree tree) {
        final Schema schema = tree.mSchema;
        final JPanel wrap = new JPanel(new BorderLayout()) {
            @Override
            public void firePropertyChange(String property, boolean oldVal, boolean newVal) {
                if (tufts.vue.gui.GUI.FINALIZE.equals(property)) {
                    if (DEBUG.Enabled)
                        Log.debug("firePropertyChange: " + property);
                    tree.destroy();
                } else {
                    super.firePropertyChange(property, oldVal, newVal);
                }
            }
        };

        tree.mUpdateButton.setOpaque(false);
        tree.mSendToMapButton.setOpaque(false);
        tree.mUpdateButton.setFont(tufts.vue.gui.GUI.LabelFace);
        tree.mSendToMapButton.setFont(tufts.vue.gui.GUI.LabelFace);

        tree.mNewRowsCheckBox.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                tree.enableUpdateButton();
            }
        });
        tree.mChangedRowsCheckBox.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                tree.enableUpdateButton();
            }
        });
        tree.mUpdateButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                tree.updateMap();
            }
        });
        tree.mSendToMapButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                tree.sendSelectedToMap();
            }
        });

        tree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);

        tree.setBorder(GUI.makeSpace(3, 0, 0, 0));

        JLabel dataSourceLabel = null;

        String imagePath = schema.getSingletonValue("rss.channel.image.url");
        if (imagePath == null)
            imagePath = schema.getSingletonValue("rdf:RDF.image.url");
        if (imagePath != null) {
            URL imageURL = Resource.makeURL(imagePath);
            if (imageURL != null) {
                dataSourceLabel = new JLabel(new ImageIcon(imageURL));
                dataSourceLabel.setBorder(GUI.makeSpace(2, 2, 1, 0));
            }
            //addNew.setIcon(new ImageIcon(imageURL));
            //addNew.setLabel(imageURL);
        }

        //         JComboBox keyBox = new JComboBox(schema.getPossibleKeyFieldNames());
        //         keyBox.setOpaque(false);
        //         keyBox.setSelectedItem(schema.getKeyField().getName());
        //         keyBox.addItemListener(new ItemListener() {
        //            public void itemStateChanged(ItemEvent e) {
        //                     if (e.getStateChange() == ItemEvent.SELECTED) {
        //                         String newKey = (String) e.getItem();
        //                         //Log.debug("KEY FIELD SELECTED: " + newKey);
        //                         schema.setKeyField(newKey);
        //                         tree.refreshRoot();
        //                     }
        //            }
        //             });
        //         toolbar.add(keyBox, BorderLayout.WEST);
        //         toolbar.add(addNew, BorderLayout.EAST);

        final int GUTTER = 4;
        JPanel toolbar = new JPanel(), remainderPanel = new JPanel(), newRowsPanel = new JPanel(),
                changedRowsPanel = new JPanel();
        Insets noInset = new Insets(0, 0, 0, 0), //top, left, bottom, right
                panelInset = new Insets(0, 0, 0, GUTTER), buttonInset = new Insets(0, 0, GUTTER, GUTTER);
        GridBagConstraints gbcCheckBox = new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
                GridBagConstraints.NONE, noInset, 0, 0),
                gbcTextArea = new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
                        GridBagConstraints.HORIZONTAL, noInset, 0, 0),
                gbcPanel = new GridBagConstraints(0, 0, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
                        GridBagConstraints.HORIZONTAL, panelInset, 0, 0),
                gbcRemainder = new GridBagConstraints(0, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
                        GridBagConstraints.REMAINDER, buttonInset, 0, 0),
                gbcButton = new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
                        GridBagConstraints.NONE, buttonInset, 0, 0);
        int checkBoxHeight = tree.mNewRowsCheckBox.getPreferredSize().height;

        toolbar.setLayout(new GridBagLayout());
        newRowsPanel.setLayout(new GridBagLayout());
        changedRowsPanel.setLayout(new GridBagLayout());

        tree.mNewRowsLabel.setFont(tufts.vue.gui.GUI.LabelFace);
        tree.mNewRowsLabel.setOpaque(false);
        newRowsPanel.add(tree.mNewRowsCheckBox, gbcCheckBox);
        newRowsPanel.add(tree.mNewRowsLabel, gbcTextArea);
        toolbar.add(newRowsPanel, gbcPanel);

        gbcPanel.gridy = 1;
        tree.mChangedRowsLabel.setFont(tufts.vue.gui.GUI.LabelFace);
        tree.mChangedRowsLabel.setOpaque(false);
        changedRowsPanel.add(tree.mChangedRowsCheckBox, gbcCheckBox);
        changedRowsPanel.add(tree.mChangedRowsLabel, gbcTextArea);
        toolbar.add(changedRowsPanel, gbcPanel);

        toolbar.add(remainderPanel, gbcRemainder);

        toolbar.add(tree.mUpdateButton, gbcButton);

        if (DEBUG_LOCAL) {
            toolbar.setOpaque(true);
            toolbar.setBackground(Color.CYAN);
            newRowsPanel.setOpaque(true);
            newRowsPanel.setBackground(Color.CYAN);
            changedRowsPanel.setOpaque(true);
            changedRowsPanel.setBackground(Color.CYAN);
            remainderPanel.setOpaque(true);
            remainderPanel.setBackground(Color.YELLOW);
            tree.mNewRowsCheckBox.setOpaque(true);
            tree.mNewRowsCheckBox.setBackground(Color.YELLOW);
            tree.mNewRowsLabel.setOpaque(true);
            tree.mNewRowsLabel.setBackground(Color.YELLOW);
            tree.mChangedRowsCheckBox.setOpaque(true);
            tree.mChangedRowsCheckBox.setBackground(Color.YELLOW);
            tree.mChangedRowsLabel.setOpaque(true);
            tree.mChangedRowsLabel.setBackground(Color.YELLOW);
            tree.mUpdateButton.setOpaque(true);
            tree.mUpdateButton.setBackground(Color.YELLOW);
        }

        if (dataSourceLabel != null)
            wrap.add(dataSourceLabel, BorderLayout.SOUTH);

        //         if (dataSourceLabel == null) {
        //             //                 dataSourceLabel = new JLabel(schema.getName());
        //             //                 dataSourceLabel.setFont(tufts.vue.VueConstants.SmallFont);
        //             //                 dataSourceLabel.setBorder(GUI.makeSpace(0,2,0,0));
        //             //                 toolbar.add(dataSourceLabel, BorderLayout.WEST);
        //             //                 toolbar.add(addNew, BorderLayout.EAST);
        //             toolbar.add(addNew, BorderLayout.CENTER);
        //         } else {
        //             toolbar.add(dataSourceLabel, BorderLayout.WEST);
        //             toolbar.add(addNew, BorderLayout.EAST);
        //         }

        toolbar.setBorder(new MatteBorder(0, 0, 1, 0, Color.gray));

        wrap.add(toolbar, BorderLayout.NORTH);
        // todo: if save entire schema with map, include date of creation (last refresh before save)
        wrap.add(tree, BorderLayout.CENTER);
        return wrap;
    }

    @Override
    protected void setExpandedState(final TreePath path, final boolean expanded) {

        final DataNode treeNode = path == null ? null : (DataNode) path.getLastPathComponent();

        if (DEBUG.Enabled)
            Log.debug("setExpandedState " + path + " = " + expanded + "; " + Util.tags(treeNode));
        // we can interrupt tree expansion here on our double-clicks for searches
        // (which may obviate part of the workaround we needed with the ClearSearchMouseListener,
        // tho not for the JScrollPane problem if that is really happening)

        if (treeNode != null && treeNode.isField()) {
            // we record the expanded state in the visibility bit of the style node
            // so we can restore it later from the skeletal Schema's saved with maps
            treeNode.getField().getStyleNode().setVisible(expanded);
        }

        GUI.invokeAfterAWT(new Runnable() {
            public void run() {
                if (!inDoubleClick) {
                    if (DEBUG.FOCUS)
                        Log.debug("setExpandedState " + path + " = " + expanded + " RELAYING");
                    DataTree.super.setExpandedState(path, expanded);
                } else if (DEBUG.FOCUS)
                    Log.debug("setExpandedState " + path + " = " + expanded + " SKIPPING");
                inDoubleClick = false;
            }
        });
    }

    private boolean inDoubleClick;

    private class ClickHandler extends tufts.vue.MouseAdapter {
        private TreePath mClickPath;

        @Override
        public void mousePressed(java.awt.event.MouseEvent e) {
            mClickPath = getPathForLocation(e.getX(), e.getY());
            if (DEBUG.Enabled) {
                //Log.debug("MOUSE PRESSED ON " + Util.tags(mClickPath));
                if (mClickPath != null)
                    Log.debug("MOUSE PRESSED ON: " + Util.tags(mClickPath.getLastPathComponent()));
                else
                    Log.debug("MOUSE PRESSED ON: nothing");
            }

            // it's possible that the node under the mouse changes from the time of the
            // first press, to the time mouseClicked is called (e.g., due to tree
            // expansion and/or possible scrolling of the entire tree component), so we
            // capture it here.  Could make this a class a generic subclassable helper
            // class for JTree's.

            inDoubleClick = GUI.isDoubleClick(e);
            if (DEBUG.Enabled)
                Log.debug("IN DOUBLE CLICK = " + inDoubleClick);

        }

        @Override
        public void mouseClicked(java.awt.event.MouseEvent e) {
            if (mClickPath == null)
                return;

            final DataNode treeNode = (DataNode) mClickPath.getLastPathComponent();

            // TODO: below selectMatchingNodes is often a repeat run after the one trigger in TreeSelectionListener.valueChanged
            //             if (GUI.isSingleClick(e)) {
            //                 if (mSelectedSearchNode == treeNode) {
            //                     // re-run search: we're clicking on already selected, and can't select it again
            //                     selectMatchingNodes(treeNode, false);
            //                 }
            //                 return;
            //             }

            if (!GUI.isDoubleClick(e))
                return;

            if (DEBUG.Enabled)
                Log.debug("ACTIONABLE DOUBLE CLICK ON " + Util.tags(treeNode));

            //             if (treeNode.hasStyle()) {
            //                 final tufts.vue.LWSelection selection = VUE.getSelection();
            //                 selection.setSource(DataTree.this);
            //                 // prevents from ever drawing through on map:
            //                 selection.setSelectionSourceFocal(null);
            //                 selection.setTo(treeNode.getStyle());
            //             } else
            if (treeNode.isRow() || (treeNode.getField() != null && treeNode.getField().isPossibleKeyField())) {
                selectMatchingNodes(treeNode, false);
            }
        }

    }

    private void selectMatchingNodes(final DataNode treeNode, final boolean extendSearch) {
        if (mActiveMap == null)
            return;

        // we search only amongst EDITBALE nodes, so that we ignore hidden/locked layers & nodes, etc
        final Collection<LWComponent> searchSet = mActiveMap.getAllDescendents(LWComponent.ChildKind.EDITABLE);

        if (DEBUG.Enabled)
            Log.debug("SEARCH:\n\nSEARCHING ALL EDITABLE DESCENDENTS of " + mActiveMap + "; count="
                    + searchSet.size() + "; treeNode=" + Util.tags(treeNode));
        //,new Throwable("HERE"));

        findAndSelectMatchingNodes(searchSet, treeNode, extendSearch);
    }

    private SmartSearch mCurrentSearch;
    private Criteria mLastCriteria;

    /**
     * Note: This method also has the side effect of picking an active style record for
     * the selection if the DataNode represents a Field (the style for all enumerated
     * values on the map from that Field), as well as setting a description in the
     * selection of the search that produced it.
     */
    private void findAndSelectMatchingNodes(final Collection<LWComponent> searchSet, final DataNode treeNode,
            final boolean extendSearch) {
        Field field = treeNode.getField();
        LWComponent styleRecord = null;

        if (field == null) {
            if (treeNode == mAllRowsNode || treeNode instanceof RowNode) {
                field = mSchema.getKeyField();
                if (treeNode == mAllRowsNode)
                    styleRecord = mSchema.getRowNodeStyle();
            } else {
                // todo: must be root node: select a row-node items
                return;
            }
        }

        if (extendSearch) {
            // only use the style record for single criteria searches; otherwise
            // makes no sense -- can't hang a style off a search, only single Fields
            styleRecord = null;
        } else {
            if (treeNode.isField() && styleRecord == null)
                styleRecord = field.getStyleNode();
        }

        final String fieldName = field.getName();

        final Criteria criteria = dataNodeToSearchCriteria(treeNode);

        final List<LWComponent> hits;
        String desc;

        if (extendSearch) {

            if (mCurrentSearch == null) {
                mCurrentSearch = new SmartSearch();
                mCurrentSearch.addCriteria(mLastCriteria);
                mLastCriteria = null;
            }
            mCurrentSearch.addCriteria(criteria);

            Log.debug("Running search " + mCurrentSearch);
            hits = mCurrentSearch.search(searchSet);

            desc = mCurrentSearch.toString();

        } else {

            // if we've got a single Criteria, no need to mess with a SmartSearch

            mCurrentSearch = null;

            hits = new ArrayList();

            Log.debug("SEARCHING WITH CRITERIA " + criteria);

            for (LWComponent c : searchSet)
                if (criteria.matches(c))
                    hits.add(c);

            mLastCriteria = criteria;

            desc = criteria.description();

        }

        desc = "matching<br>" + desc;

        if (DEBUG.Enabled) {
            if (hits.size() == 1)
                Log.debug("hits=" + hits.get(0) + " [single hit]");
            else
                Log.debug("hits=" + hits.size());
            Log.debug("styleRecord: " + styleRecord);
            //if (styleRecord != null) desc += "<p>style: " + styleRecord;
        }

        final tufts.vue.LWSelection selection = VUE.getSelection();
        // make sure selection bounds are drawn in MapViewer:
        selection.setSelectionSourceFocal(VUE.getActiveFocal());
        // now set the selection, along with a description
        selection.setWithStyle(hits, desc, styleRecord);
    }

    public Criteria dataNodeToSearchCriteria(final DataNode treeNode) {

        final Field field = treeNode.getField();
        final String fieldName = field == null ? null : field.getName();

        Criteria criteria = null;

        if (treeNode == mAllRowsNode) {

            // search for ANY row-node in the schema
            if (DEBUG.Enabled)
                Log.debug("searching for all data records in schema " + mSchema);

            criteria = new SchemaMatch(mSchema);
        }

        else if (treeNode.isRow()) {
            // search for a particular row-node in the schema based on the key field -- this will
            // normally only find a single node on the map, unless there are duplicate nodes on
            // the map referencing the same row

            final String keyField = ((RowNode) treeNode).getSchema().getKeyFieldName();
            final String keyValue = treeNode.getRow().getValue(keyField);

            criteria = new ValueMatch(keyField, keyValue);
        }

        else if (treeNode.isField()) {

            // search for all nodes anchoring a particular value for the given Field
            if (DEBUG.Enabled)
                Log.debug("searching for any enumerated value from a field named " + fieldName);

            criteria = new FieldMatch(field);
            //criteria = new KeyMatch(fieldName);
        }

        else if (treeNode.isValue()) {

            // search for a particular key=value

            final String fieldValue = treeNode.getValue();

            if (DEBUG.Enabled)
                Log.debug(String.format("searching for %s=[%s]", fieldName, fieldValue));

            criteria = new ValueMatch(fieldName, fieldValue);
        }

        return criteria;
    }

    // todo: better to move all this Search stuff to a DataSearch.java or some such.

    public abstract static class Criteria {
        boolean matches(LWComponent c) {
            throw new UnsupportedOperationException("unimplemented matches in " + this);
        }

        abstract String description();

        String getKey() {
            return null;
        }

        public List<LWComponent> search(final Collection<LWComponent> searchSet) {
            final List<LWComponent> hits = new ArrayList();

            for (LWComponent c : searchSet)
                if (matches(c))
                    hits.add(c);

            return hits;
        }

        @Override
        public String toString() {
            return String.format("%s[%s]", getClass().getSimpleName(), description());
        }

    }

    public static final class SchemaMatch extends Criteria {
        final Schema schema;

        public SchemaMatch(Schema s) {
            schema = s;
        }

        @Override
        public boolean matches(LWComponent c) {
            return c.isDataRow(schema);
        }

        @Override
        public String description() {
            return String.format("in data set: <i>%s</i>", schema.getName());
        }
    }

    public static final class FieldMatch extends Criteria {
        final Field field;

        public FieldMatch(Field f) {
            field = f;
        }

        @Override
        public boolean matches(LWComponent c) {
            return c.isDataValueNode(field);
        }

        @Override
        public String description() {
            return String.format("enumerated values of: <b>%s</b>", field);
        }
        //@Override String getKey() { return key; }

        @Override
        public String toString() {
            return String.format("enumerated values of <i>%s</i>", field);
        }
    }

    public static final class KeyMatch extends Criteria {
        final String key;

        public KeyMatch(String fieldName) {
            key = fieldName;
        }

        @Override
        public boolean matches(LWComponent c) {
            return c.isDataValueNode(key);
        }

        @Override
        public String description() {
            return String.format("any enumerated values of: <b>%s</b>", key);
        }
        //@Override String getKey() { return key; }

        @Override
        public String toString() {
            return String.format("any enumerated values of <i>%s</i>", key);
        }
    }

    public static final class ValueMatch extends Criteria {
        final String key;
        final String value;

        public ValueMatch(String k, String v) {
            key = k;
            value = v;
        }

        @Override
        public boolean matches(LWComponent c) {
            return c.hasDataValue(key, value);
        }

        @Override
        public String description() {
            return String.format("<b>%s: <i>%s</i>", key, valueText(value));
        }

        @Override
        String getKey() {
            return key;
        }

        @Override
        public String toString() {
            return String.format("%s=%s", key, value);
        }

    }

    /**
     * A multiple criteria search that is automagically smart about how to create boolean
     * AND and OR groups to create reasonable searches.
     */
    public static final class SmartSearch /*extends Crtieria*/ {

        /** a boolean AND group of OR lists for each key used in any key=value searches present */
        final Multimap<String, Criteria> criteriaByKey = Multimaps.newHashMultimap();
        /** a special OR group that takes priority: anything matching criteria in this group
         * is "hit" no matter what */
        final Collection<Criteria> globalBooleanOr = new ArrayList();

        public void addCriteria(Criteria criteria) {

            if (DEBUG.Enabled)
                Log.debug("SmartSearch adding " + criteria);

            if (criteria == null)
                return;

            final String key = criteria.getKey();
            if (key != null) {
                criteriaByKey.put(key, criteria);
            } else {

                // note: this impl means any search containing a SchemaMatch will match ALL
                // rows in the schema, and it will only be meaninful to add Fields to
                // the search (not values selecting particular rows, as they'll allready
                // be all selected).

                globalBooleanOr.add(criteria);
            }
        }

        public List<LWComponent> search(final Collection<LWComponent> searchSet) {
            final List<LWComponent> hits = new ArrayList();

            if (DEBUG.Enabled) {

                for (Criteria c : globalBooleanOr)
                    Log.debug("GlobalOR: " + c);

                final Collection<Map.Entry<String, Collection<Criteria>>> allKeyEntries = criteriaByKey.asMap()
                        .entrySet();

                for (Map.Entry e : allKeyEntries) {
                    Log.debug(String.format("Key: %-12s criteria=%s", e.getKey(), e.getValue()));
                }
            }

            final Collection<Collection<Criteria>> keyBasedCriteria;

            if (criteriaByKey.size() > 0)
                keyBasedCriteria = criteriaByKey.asMap().values();
            else
                keyBasedCriteria = null;

            if (globalBooleanOr.size() > 0) {
                // search method 1: includes the global OR tests
                for (LWComponent c : searchSet) {
                    if (anyCriteriaMatches(c, globalBooleanOr))
                        hits.add(c);
                    else if (keyBasedCriteria != null && allGroupsMatch(c, keyBasedCriteria))
                        hits.add(c);
                }
            } else if (keyBasedCriteria != null) {
                // search method 2: just the AND'd group of OR tests
                for (LWComponent c : searchSet) {
                    if (allGroupsMatch(c, keyBasedCriteria)) {
                        hits.add(c);
                    }
                }
            }

            return hits;
        }

        /** perform a boolean AND of a bunch of OR groups: at least one criteria must match from each collection of criteria
         * Note: if the group is empty (no collections of Criteria), this will always return TRUE */
        private boolean allGroupsMatch(final LWComponent c,
                final Collection<Collection<Criteria>> booleanAndGroup) {
            for (Collection<Criteria> booleanOrGroup : booleanAndGroup) {
                boolean atLeastOneMatched = false;

                //Log.debug(c.getUniqueComponentTypeLabel() + "; against " + booleanOrGroup);

                // todo: could make this even faster by exploiting the underlying
                // Multimap impl in the LWComponent MetaMap to have it pull all values
                // for the key, which should be a Set, and just check for set membership
                // for each of the values we're looking for under that key.  Could pull
                // the key to use from the first criteria in the group, or just pass in
                // Map.Entry's of the keys and their collections of criteria.  We'd
                // abandon the matches(c) API and manually pull the value from key=value
                // criteria to check the set membership.

                for (Criteria criteria : booleanOrGroup) {
                    if (criteria.matches(c)) {
                        //Log.debug(c.getUniqueComponentTypeLabel() + "; ok");
                        atLeastOneMatched = true;
                        break;
                    }
                }

                if (!atLeastOneMatched) {
                    // nothing matched in this or group: entire search immediately fails
                    //Log.debug(c.getUniqueComponentTypeLabel() + " failed; no values matched in key");
                    return false; // immediate boolean short-circuit
                }
            }

            return true;
        }

        /** perform a boolean OR for a group of criteria */
        private boolean anyCriteriaMatches(final LWComponent c, final Collection<Criteria> booleanOrGroup) {
            for (Criteria criteria : booleanOrGroup) {
                if (criteria.matches(c)) {
                    //Log.debug(criteria + " hit " + c);
                    return true; // immediate boolean short-circuit
                }
            }

            return false;
        }

        @Override
        public String toString() {

            final Collection<Collection<Criteria>> groupedByKey = criteriaByKey.asMap().values();

            StringBuilder b = new StringBuilder("search terms:<br>");
            for (Criteria c : globalBooleanOr) {
                b.append(c.description());
                b.append("<br>");
            }

            for (Collection<Criteria> eachKey : groupedByKey) {
                for (Criteria c : eachKey) {
                    b.append(c.description());
                    b.append("<br>");
                }
            }
            return b.toString();
        }

    }

    private void debug(String s) {
        Log.debug(
                String.format("%s%-20s%s %s", Util.TERM_PURPLE, "[" + mSchema.getName() + "]", Util.TERM_CLEAR, s));
    }

    // For annotations, we create a single handler for listening to user changes to the
    // current map (UseActionCompleted), that keeps background threads running for the
    // annotations for each loaded DataTree.  It is designed so that the annotate thread
    // for the currently visible DataTree(s) runs at a higher priority, and the loaded
    // but not currently displated DataTree's run at a lower priority.  If a second
    // update comes through before the current pass is completed, the prior update can
    // be aborted.

    private void kickAnnotate() {

        GUI.invokeOnEDT(new Runnable() {
            public void run() {
                mUpdateButton.setEnabled(false);
            }
        });

        if (DEBUG.THREAD)
            debug("WAKING-> ANNOTATION THREAD " + mAnnotateThread);

        // note: if we're already on a thread that's NOT the AWT EDT, we could
        // assume we're in a builder thread and instead of waking the annotation
        // thread just run in the builder thread, tho better to keep all that
        // done there as sometimes that can take a while to run, and this will
        // allow initial tree creations to run faster.

        synchronized (mAnnotateThread) {
            mAnnotateThread.notify();
        }
        if (DEBUG.THREAD || DEBUG.ANNOTATE)
            debug("NOTIFIED ANNOTATION THREAD " + mAnnotateThread);
    }

    // TODO: add another kind of annotation pass that runs after a search, and greys out enumerated
    // values that have dropped out of the search set.  (?)

    /** @return true if interrupted */
    private boolean annotateForMap() {
        return annotateForMap(mActiveMap);
    }

    /** @return true if interrupted */
    private boolean annotateForMap(final LWMap map) {
        if (DEBUG.THREAD || DEBUG.SCHEMA || DEBUG.ANNOTATE)
            Log.debug("ANNOTATING against " + map + "; " + Util.tags(ActiveMapDataNodes));

        mSchema.annotateFor(ActiveMapDataNodes);

        // note: the map isn't actually needed by any of the below annotation calls

        if (map != null) {
            final String annot = map.getLabel();
            for (DataNode n : mRootNode.getChildren()) {
                if (Thread.interrupted())
                    return true;
                n.annotate(map);
                if (!n.isLeaf())
                    for (DataNode cn : n.getChildren()) {
                        if (Thread.interrupted())
                            return true;
                        cn.annotate(map);
                    }
            }
        }

        int _newRowCount = 0;
        int _changedRowCount = 0;
        //   if (mAllRowsNode.getChildren() !=null)
        for (DataNode n : mAllRowsNode.getChildren()) {
            if (!n.isMapPresent())
                _newRowCount++;
            if (n.isContextChanged())
                _changedRowCount++;
        }

        final int newRowCount = _newRowCount;
        final int changedRowCount = _changedRowCount;

        if (DEBUG.THREAD || DEBUG.SCHEMA)
            Log.debug("annotateForMap: newRows " + newRowCount + "; changedRows " + changedRowCount);

        if (Thread.interrupted())
            return true;

        GUI.invokeOnEDT(new Runnable() {
            public void run() {
                String newRowsMessage = "", changedRowsMessage = "";

                newRowsMessage = (newRowCount == 0
                        ? VueResources.getString("dockWindow.contentPanel.sync.noNewRecords")
                        : (newRowCount == 1 ? VueResources.getString("dockWindow.contentPanel.sync.oneNewRecord")
                                : String.format(
                                        VueResources.getString("dockWindow.contentPanel.sync.manyNewRecords"),
                                        newRowCount)));
                changedRowsMessage = (changedRowCount == 0
                        ? VueResources.getString("dockWindow.contentPanel.sync.noChangedRecords")
                        : (changedRowCount == 1
                                ? VueResources.getString("dockWindow.contentPanel.sync.oneChangedRecord")
                                : String.format(
                                        VueResources.getString("dockWindow.contentPanel.sync.manyChangedRecords"),
                                        changedRowCount)));

                mNewRowsLabel.setText(newRowsMessage);
                mNewRowsLabel.setToolTipText(newRowsMessage);
                mNewRowsLabel.setForeground(newRowCount != 0 ? Color.BLACK : MEDIUM_DARK_GRAY);
                mNewRowsCheckBox.setEnabled(newRowCount != 0);
                mChangedRowsLabel.setText(changedRowsMessage);
                mChangedRowsLabel.setToolTipText(changedRowsMessage);
                mChangedRowsLabel.setForeground(changedRowCount != 0 ? Color.BLACK : MEDIUM_DARK_GRAY);
                mChangedRowsCheckBox.setEnabled(changedRowCount != 0);
                enableUpdateButton();

                // TODO: don't bother with refresh if annotations didn't change at all
                refreshAll();
            }
        });

        return false;
    }

    //     private void refreshRoot() {
    //         if (DEBUG.Enabled) Log.debug("REFRESHING " + Util.tags(mRootNode));
    //         refreshAllChildren(mRootNode);
    //     }

    private void refreshTopLevel() {
        refreshAllChildren(mRootNode);
    }

    private void refreshAllStyleNodes() {
        refreshTopLevel();
    }

    private void refreshAll() {
        //mTreeModel.reload(mRootNode);

        // using nodesChanged instead of reload preserves the expanded state of nodes in the tree

        if (DEBUG.THREAD)
            Log.debug("REFRESHING " + Util.tags(mRootNode.getChildren()));
        refreshTopLevel();
        for (TreeNode n : mRootNode.getChildren())
            if (!n.isLeaf())
                refreshAllChildren(n);
        if (DEBUG.THREAD)
            Log.debug(" REFRESHED " + Util.tags(mRootNode.getChildren()));

        // This gets close, but doesn't always handle updating NON expanded nodes, plus
        // it often leaves labels truncated with "..."
        // invalidate();
        // super.treeDidChange();
    }

    private void restoreAnyExpandedState() {
        int i = 0;
        for (TreeNode n : mRootNode.getChildren()) {
            if (n instanceof FieldNode) {
                //Log.debug("found field node " + Util.tags(n));
                Field f = ((FieldNode) n).getField();
                if (f != null) {
                    //Log.debug("found field " + f);
                    LWComponent style = ((FieldNode) n).getField().getStyleNode();
                    if (style != null && style.isVisible()) {
                        final TreePath path = getPathForRow(i);
                        if (DEBUG.Enabled)
                            Log.debug("EXPAND " + Util.tags(n) + " " + Util.tags(path));
                        GUI.invokeOnEDT(new Runnable() {
                            public void run() {
                                DataTree.super.setExpandedState(path, true);
                            }
                        });
                    }
                }
            }
            i++;
        }
    }

    private void refreshAllChildren(TreeNode node) {
        final int[] childIndexes = new int[node.getChildCount()];
        for (int i = 0; i < childIndexes.length; i++)
            childIndexes[i] = i; // why there's isn't an API to do this automatically, i don't know...

        // using nodesChanged instead of mTreeModel.reload preserves the expanded state of nodes in the tree
        if (DEBUG.META)
            Log.debug("refreshing " + childIndexes.length + " children of " + node);
        mTreeModel.nodesChanged(node, childIndexes);
    }

    private void refreshRootNode() {
        mTreeModel.nodesChanged(mRootNode, new int[] { 0 });
    }

    @Override
    public String toString() {
        return String.format("DataTree[%s]", mSchema.toString());
    }

    private static volatile int AnnotationThreadCount = 0;

    private void destroy() {
        if (DEBUG.Enabled)
            Log.debug("destroying w/" + mAnnotateThread);
        mAnnotateThread.interrupt();
        mAnnotateThread = null;

        // it's crucial to flush the old schema so that if it isn't reloaded with new
        // data (a new Schema instance is created when/if this schema is reloaded), the
        // old schema will be empty and will no longer match to any nodes on any map.
        // todo: the XmlDataSource impl current decides which happens (e.g., re-loaded
        // for .csv, new instances for XML) -- if we keep the new-instance
        // functionality, eventually we should actually remove the defunct schema's from
        // the Schema global instance lists, instead of just leaving them in there but
        // empty.

        mSchema.flushData();

        ActiveTrees.remove(this);

        //         if (mActiveMap != null)
        //             mActiveMap.removeLWCListener(this);
        //         VUE.removeActiveListener(LWMap.class, this);
    }

    private DataTree(final Schema schema) {

        mSchema = schema;

        setCellRenderer(new DataRenderer());
        //setSelectionModel(null);

        setModel(mTreeModel = new DefaultTreeModel(buildTree(schema), false));

        final int ac = AnnotationThreadCount++;

        mAnnotateThread = new Thread(String.format("Annotate%d: %-20.20s", ac, schema.getName())) {
            {
                setPriority(MAX_PRIORITY);
            }

            public synchronized void run() {
                while (true) {
                    try {
                        // must be careful: if we get a notify before the 1st time we go to
                        // sleep, we'll never wake up!  So we start this thread at high
                        // priority, and kick it off ("start()") immediately, because as soon
                        // as the DataTree is done constructing, we're going to get notified
                        // the first time -- still theoretically risky but it appears to 
                        // be working reliably.

                        if (DEBUG.THREAD || DEBUG.ANNOTATE)
                            Log.debug("annotation thread sleeping, pri=" + getPriority());
                        wait();
                    } catch (InterruptedException e) {
                        Log.error("interrupted; exiting; " + schema);
                        return;
                    }
                    if (DataTree.this == ForegroundTree && !VUE.isApplet()) // referring to NORM_PRIORITY can fail in Applets
                        setPriority(NORM_PRIORITY - 1);
                    else
                        setPriority(MIN_PRIORITY);
                    if (DEBUG.THREAD || DEBUG.ANNOTATE)
                        Log.debug("annotation thread woke, pri=" + getPriority() + "; running...");
                    final boolean interrupted = annotateForMap();
                    if (DEBUG.Enabled) {
                        if (interrupted)
                            Log.debug("annotation aborted");
                        else if (DEBUG.THREAD)
                            Log.debug("annotation completed");
                    }
                }
            }
        };

        if (DEBUG.THREAD)
            Log.debug("STARTING " + mAnnotateThread + "; (tree constructing)");
        mAnnotateThread.start();

        setRowHeight(0);
        setRootVisible(false);
        setShowsRootHandles(true);

        java.awt.dnd.DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(this,
                java.awt.dnd.DnDConstants.ACTION_COPY | java.awt.dnd.DnDConstants.ACTION_MOVE
                        | java.awt.dnd.DnDConstants.ACTION_LINK,
                this);

        addMouseListener(new ClickHandler());

        addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() {
            public void valueChanged(javax.swing.event.TreeSelectionEvent e) {
                //final TreePath[] paths = e.getPaths();
                final TreePath[] paths = getSelectionModel().getSelectionPaths();

                if (DEBUG.Enabled)
                    Log.debug("valueChanged: isAddedPath=" + e.isAddedPath() + "; PATHS:");
                if (DEBUG.Enabled)
                    Util.dump(paths);
                //if (DEBUG.Enabled) Log.debug("OLD LeadPath: " + e.getOldLeadSelectionPath());
                //if (DEBUG.Enabled) Log.debug("NEW LeadPath: " + e.getNewLeadSelectionPath());

                // TODO: change from checking getPaths to model.getSelectionPaths & ignoring isAddedPath
                //                     if (!e.isAddedPath() || e.getPath().getLastPathComponent() == null)
                //                         return;

                final DataNode treeNode = (DataNode) e.getPath().getLastPathComponent();
                if (treeNode instanceof RowNode) {
                    VUE.setActive(tufts.vue.MetaMap.class, DataTree.this, treeNode.getRow().getData());
                } else if (treeNode instanceof ValueNode && treeNode.getField().isPossibleKeyField()) {
                    DataRow row = mSchema.findRow(treeNode.getField(), treeNode.getValue());
                    if (row != null) {
                        VUE.setActive(tufts.vue.MetaMap.class, DataTree.this, row.getData());
                    } else {
                        Log.warn("Row is NULL while trying to set active in tree selection value changed");
                    }
                }
                //                         else if (treeNode.hasStyle()) {
                //                             final tufts.vue.LWSelection selection = VUE.getSelection();
                //                             selection.setSource(DataTree.this);
                //                             // prevents from ever drawing through on map:
                //                             selection.setSelectionSourceFocal(null);
                //                             selection.setTo(treeNode.getStyle());
                //                         }
                else if (paths != null) {

                    boolean multipleSearchTerms = false;
                    DataNode node = null;
                    for (TreePath path : paths) {
                        node = (DataNode) path.getLastPathComponent();
                        selectMatchingNodes(node, multipleSearchTerms);
                        multipleSearchTerms = true;
                    }
                    if (paths.length == 1)
                        mSelectedSearchNode = node;
                    else
                        mSelectedSearchNode = null;

                } else {
                    if (DEBUG.Enabled)
                        Log.warn("null search path from selection model");
                }

                //                         else if (treeNode instanceof ValueNode) {
                //                             if (treeNode.getField().isPossibleKeyField()) {
                //                                 DataRow row = mSchema.findRow(treeNode.getField(), treeNode.getValue());
                //                                 VUE.setActive(tufts.vue.MetaMap.class,
                //                                               DataTree.this,
                //                                               row.getData());
                //                             } else {
                //                                 selectMapForNode(treeNode, false);
                //                             }
                //                         }
                //VUE.setActive(LWComponent.class, this, node.styleNode);
            }
        });

    }

    @Override
    public void addNotify() {
        ForegroundTree = this;
        super.addNotify();
    }

    @Override
    public void removeNotify() {
        if (ForegroundTree == this)
            ForegroundTree = null;
        super.removeNotify();
    }

    private static String HTML(String s) {
        //if (true) return s;
        final StringBuilder b = new StringBuilder(s.length() + 6);
        //b.append("<html>");
        // we add space before and after to widen the painted background selection around the text a bit
        b.append("<html>&nbsp;");
        b.append(s);
        b.append("&nbsp;");
        return b.toString();
    }

    private static int sortPriority(Field f) {
        if (f.isSingleton())
            return -4;
        else if (f.isSingleValue())
            return -3;
        else if (f.isUntrackedValue())
            return -2;
        else if (f.getName().contains(":") && !f.getName().startsWith("dc:"))
            return -1;
        else
            return 0;
    }

    /** build the model and return the root node */
    private TreeNode buildTree(final Schema schema) {
        mAllRowsNode = new AllRowsNode(schema, this);

        final DataNode root = new DataNode("Data Set: " + schema.getName());
        //new VauleNode("Data Set: " + schema.getName());
        //             new DataNode(null, null,
        //                          String.format("%s [%d %s]",
        //                                        schema.getName(),
        //                                        schema.getRowCount(),
        //                                        "items"//isCSV ? "rows" : "items"));

        final Field keyField = schema.getKeyField();
        Field labelField = schema.getField("title");
        if (labelField == null)
            labelField = keyField;
        for (DataRow row : schema.getRows()) {
            mAllRowsNode.add(new RowNode(row, labelField));
            //String label = row.getValue(labelField);
            //rowNodeTemplate.add(new ValueNode(keyField, row.getValue(keyField), label));
        }

        root.add(mAllRowsNode);
        mRootNode = root;

        final Field sortedFields[] = new Field[schema.getFieldCount()];

        schema.getFields().toArray(sortedFields);

        Arrays.sort(sortedFields, new Comparator<Field>() {
            public int compare(Field f1, Field f2) {
                return sortPriority(f2) - sortPriority(f1);
            }
        });

        final LWComponent.Listener styleRepainter = new LWComponent.Listener() {
            // todo: schema style nodes are currently parentless, which means
            // their property change events don't go up through the map to
            // the undo-manager, making changes to them not undoable -- either
            // manually relay style property change events up through the appropriate
            // map, or have a way for a map to have hidden list of style children.
            public void LWCChanged(tufts.vue.LWCEvent e) {
                if (DEBUG.EVENTS)
                    Log.debug("REPAINTER UPDATE ON " + e);
                if (e.getKey() == LWKey.FillColor) {
                    DataTree.this.refreshAllStyleNodes();
                }
            }
        };

        //The current data model doesn't make sense for matrix data and so it's not going to make a ton of sense
        if (!DataTree.this.mSchema.isMatrixDataSet)
            for (Field field : sortedFields) {

                //             if (field.isSingleton())
                //                 continue;

                DataNode fieldNode = new FieldNode(field, styleRepainter, null);
                root.add(fieldNode);

                //             if (field.uniqueValueCount() == schema.getRowCount()) {
                //                 //Log.debug("SKIPPING " + f);
                //                 continue;
                //             }

                final Set values = field.getValues();

                // could add all style nodes to the schema node to be put in an internal layer for
                // persistance: either that or store them with the datasources, which
                // probably makes more sense.

                if (values.size() > 1) {
                    try {
                        buildValueChildren(field, fieldNode);
                    } catch (Throwable t) {
                        Log.error("building child values for: " + Util.tags(field) + "; " + Util.tags(fieldNode),
                                t);
                    }
                }

            }

        return root;
    }

    private static final boolean SORT_BY_COUNT = false;
    private static final boolean SORT_BY_VALUE = !SORT_BY_COUNT;

    private static void buildValueChildren(Field field, DataNode fieldNode) {
        final Multiset<String> valueCounts = field.getValueSet();
        final Set<Multiset.Entry<String>> entrySet = valueCounts.entrySet();

        final Iterable<Multiset.Entry<String>> valueEntries;

        if (field.isQuantile() || (SORT_BY_COUNT && field.isPossibleKeyField())) {
            // cases we don't need to bother sorting: (1) quantiles, which are
            //  pre-sorted (2) possible key fields: if sorting by counts (frequency)
            //  don't need to bother sorting if field is a possible key field, as all
            //  value counts == 1
            valueEntries = entrySet;
        } else {
            final ArrayList<Multiset.Entry<String>> sortedValues = new ArrayList(entrySet);

            Collections.sort(sortedValues, new Comparator<Multiset.Entry<String>>() {
                public int compare(final Multiset.Entry<String> e1, final Multiset.Entry<String> e2) {
                    // always put any empty value item last, otherwise sort on frequency
                    if (e1.getElement() == Field.EMPTY_VALUE)
                        return 1;
                    else if (e2.getElement() == Field.EMPTY_VALUE)
                        return -1;
                    else if (SORT_BY_COUNT)
                        return e2.getCount() - e1.getCount();
                    else // SORT_BY_VALUE
                        return tufts.Strings.compareNaturalIgnoreCaseAscii(e1.getElement(), e2.getElement());
                }
            });

            valueEntries = sortedValues;
        }

        //-----------------------------------------------------------------------------
        // Add the enumerated values
        //-----------------------------------------------------------------------------

        for (Multiset.Entry<String> e : valueEntries) {

            final String value = e.getElement();
            final String display;

            int nValues = e.getCount();
            if (field.isQuantile() && value != Field.EMPTY_VALUE) {
                // non-empty Quantile values always have an extra count,
                // which was the "init" count to enforce quantile-order
                // on the values list.
                nValues--;
            }

            if (field.isPossibleKeyField()) {

                display = field.valueDisplay(value);

            } else {

                final String countTxt = String.format("%3d", nValues).replaceAll(" ", "&nbsp;");

                final String color;

                if (nValues <= 0)
                    display = String.format(HTML("<font color=#AAAAAA><code>%s</code> %s"), countTxt,
                            valueText(value));
                else
                    display = String.format(HTML("<code><font color=#888888>%s</font></code> %s"), countTxt,
                            valueText(value));
            }

            final ValueNode valueNode = new ValueNode(field, value, display, nValues);

            fieldNode.add(valueNode);

        }

        for (String comment : field.getDataComments()) {
            fieldNode.add(new DataNode(HTML("<font color=#AAAAAA>" + comment)));
        }

    }

    public void dragGestureRecognized(DragGestureEvent e) {
        if (getSelectionPath() == null) {
            Log.debug("dragGestureRecognized: no selection path; " + e);
            return;
        }

        Log.debug("SELECTED: " + Util.tags(getSelectionPath().getLastPathComponent()));
        final DataNode treeNode = (DataNode) getSelectionPath().getLastPathComponent();
        //if (resource != null) 
        //GUI.startRecognizedDrag(e, resource, this);

        //tufts.vue.gui.GUI.startRecognizedDrag(e, Resource.instance(node.value), null);

        final LWComponent dragNode;
        final Field field = treeNode.getField();
        boolean stylesAlreadyApplied = false;

        if (treeNode.isValue()) {

            if (treeNode.getCount() <= 0)
                dragNode = null;
            else
                dragNode = DataAction.makeValueNode(field, treeNode.getValue());
        } else if (treeNode.isField()) {
            //if (field.isPossibleKeyField())
            //return;
            dragNode = new LWNode(String.format("  %d unique  \n  '%s'  \n  values  ", field.uniqueValueCount(),
                    field.getName()));
            //dragNode.setClientData(java.awt.datatransfer.DataFlavor.stringFlavor,
            //" ${" + field.getName() + "}");

        } else if (treeNode instanceof RowNode) {

            final DataRow row = ((RowNode) treeNode).getRow();
            final List<LWComponent> nodes = DataAction.makeSingleRowNode(treeNode.getSchema(), row);
            if (DEBUG.Enabled)
                Log.debug("made row nodes: " + Util.tags(nodes));
            if (nodes.isEmpty()) {
                Log.error("no row node made from row: " + row);
                dragNode = null;
            } else
                dragNode = nodes.get(0);
            stylesAlreadyApplied = true;
        } else {
            //assert treeNode instanceof TemplateNode;
            final Schema schema = treeNode.getSchema();
            dragNode = new LWNode(
                    String.format("  '%s'  \n  dataset  \n  (%d items)  ", schema.getName(), schema.getRowCount()));
        }

        if (dragNode == null) {
            Log.warn("Unable to create nodes from drag of " + treeNode);
            return;
        }

        dragNode.copyStyle(treeNode.getStyle(), ~LWKey.Label.bit);

        //dragNode.setFillColor(null);
        //dragNode.setStrokeWidth(0);
        if (!treeNode.isValue()) {
            dragNode.mFontSize.setTo(24);
            dragNode.mFontStyle.setTo(java.awt.Font.BOLD);
            //                 dragNode.setClientData(LWComponent.ListFactory.class,
            //                                        new NodeProducer(treeNode));
        }
        dragNode.setFlag(LWComponent.Flag.INTERNAL);
        dragNode.setClientData(tufts.vue.MapDropTarget.DropHandler.class,
                new DataDropHandler(treeNode, DataTree.this));
        dragNode.setClientData(Field.class, treeNode.getField()); // for associations panel

        tufts.vue.gui.GUI.startRecognizedDrag(e, dragNode);

    }

    private static String valueText(Object value) {
        return DataAction.valueText(value);
    }

    private DataNode getSelectedNode() {
        return (DataNode) getLastSelectedPathComponent();
    }

    private void sendToMap(final DataNode treeNode, final LWMap map) {
        if (map == null || treeNode == null)
            return;
        Log.debug("SENDING TO MAP: " + treeNode);
    }

    private static final Ordering<Multiset.Entry> ByDecreasingFrequency = new Ordering<Multiset.Entry>() {
        /*@Override*/ public int compare(Multiset.Entry a, Multiset.Entry b) {
            return b.getCount() - a.getCount();
        }
    };

    /*
     * This will find all rows of data in this given data-set that are NOT in the map,
     * create row-nodes for them, and send them to the map.  This also kicks off an
     * annotation run to update the tree after the nodes have been added to the map.
     */

    // For NEW DATA CLUSTERING: whenever new nodes are added to the map and there is no
    // layout specified / going to be applied, we want to place nodes near items their
    // related to.  We do this based on the links.
    //
    // There are many versions of this.  E.g.:
    //
    // 1 - re-clustering around the last clustered nodes as marked by the clustering
    // time-stamp for row-node additions
    //
    // 2 - placing new value-nodes most near the nodes their related to based on links

    /** adding more than this # of new row-nodes to the map permits a map-reorg */
    private static final int NEW_ROW_NODE_MAP_REORG_THRESHOLD = 20;

    private void addMissingRowsToMap(final LWMap map) {
        // todo: we'll want to merge some of this code w/DropHandler code, as
        // this is somewhat of a special case of doing a drop
        final List<DataRow> newRows = new ArrayList();

        for (DataNode n : mAllRowsNode.getChildren()) {
            if (!n.isMapPresent()) {
                //Log.debug("ADDING TO MAP: " + n);
                newRows.add(n.getRow());
            }
        }

        final List<LWComponent> newRowNodes = DataAction.makeRowNodes(mSchema, newRows);

        Multiset<LWComponent> targetsUsed = null;
        List<LWLink> linksAdded = Collections.EMPTY_LIST;

        try {
            final Object[] result = DataAction.addDataLinksForNodes(map, newRowNodes, (Field) null);
            targetsUsed = (Multiset) result[0];
            linksAdded = (List) result[1];
        } catch (Throwable t) {
            Log.error("problem creating links on " + map + " for new nodes: " + Util.tags(newRowNodes), t);
        }

        if (DEBUG.Enabled && targetsUsed != null) {
            final Set entries = targetsUsed.entrySet();
            Log.debug("TARGETS USED: " + targetsUsed.size() + " / " + entries.size());
            Util.dump(entries);
        }

        if (newRowNodes.size() > 0) {

            // we cannot run setXYByClustering before adding to the map w/out refactoring projectNodes
            // (or for that matter, centroidCluster, which also uses projectNodes).  E.g. -- we
            // can't use this as an initial fallback/failsafe.
            //tufts.vue.VueUtil.setXYByClustering(map, nodes);

            //-----------------------------------------------------------------------------
            // add all the "missing" / newly-arrived rows to the map
            //-----------------------------------------------------------------------------
            map.getOrCreateLayer("New Data Nodes").addChildren(newRowNodes);

            // PROBLEM/BUG: the above add to a special layer appears to be failing (to
            // the user) somtimes and the nodes wind up in the same layer as the
            // relating nodes -- this is when ArrangeAction.clusterLinked is then used
            // below.  It does some reparenting which it needs to do in case nodes had
            // been collected as children, but in some cases, it doesn't need doing and
            // ends up just pulling the nodes right back out of the "New Data Nodes"
            // layer after we just moved them there.
            // -----------------------------------------------------------------------------

            if (newRowNodes.size() > NEW_ROW_NODE_MAP_REORG_THRESHOLD) {

                if (targetsUsed.size() > 0) { // Note: won't currently trigger for cross-schema joins, as targesUsed aren't reported

                    //-------------------------------------------------------
                    // RE-CLUSTER THE ENTIRE MAP
                    //-------------------------------------------------------

                    // If there is was more than one value-node link per row-node created (e.g.,
                    // multiple sets of value nodes are already on the map), prioritizing those
                    // targets with the most first spreads the nodes out the most as the targets
                    // with the fewest links would are at least be guaranteed to get some of the
                    // row nodes.  Using the push-method in this case would be far too slow -- we'd
                    // have to push based on every row node.

                    final List<Multiset.Entry<LWComponent>> ordered = ByDecreasingFrequency
                            .sortedCopy(targetsUsed.entrySet());

                    for (Multiset.Entry<LWComponent> e : ordered) {
                        tufts.vue.Actions.ArrangeAction.clusterLinked(e.getElement());
                    }

                    // note: if we wished, we could also decide here
                    // what to cluster on based on what targets are
                    // selected (currently have the selection bit set)

                } else {
                    // fallback: randomly layout anything that isn't first XY clustered:
                    tufts.vue.LayoutAction.random.act(tufts.vue.VueUtil.setXYByClustering(newRowNodes));
                }
            } else {
                //-------------------------------------------------------
                // Centroid cluster
                //-------------------------------------------------------
                DataAction.centroidCluster(map, newRowNodes, true);
                //-------------------------------------------------------
            }

            VUE.getSelection().setTo(newRowNodes);
        }

        map.getUndoManager().mark("Add New Data Nodes");
    }

    private static String makeFieldLabel(final Field field) {
        if (field.isQuantile())
            return HTML(field.getName());
        //return HTML("<font color=gray>" + field.getName());

        final Set values = field.getValues();
        //Log.debug("EXPANDING " + colNode);

        //LWComponent schemaNode = new LWNode(schema.getName() + ": " + schema.getSource());
        // add all style nodes to the schema node to be put in an internal layer for
        // persistance: either that or store them with the datasources, which
        // probably makes more sense.

        String label = field.toString();

        if (values.size() == 0) {

            if (field.getMaxValueLength() == 0) {
                //label = String.format("<html><b><font color=gray>%s", field.getName());
                label = String.format(HTML("<font color=gray>%s"), field.getName());
            } else {
                //label = String.format("<html><b>%s (max size: %d bytes)",
                label = String.format(HTML("%s (max size: %d bytes)"), field.getName(), field.getMaxValueLength());
            }
        } else if (values.size() == 1) {

            label = String.format(HTML("%s: <font color=green>%s"), field.getName(),
                    field.getValues().toArray()[0]);

        } else if (values.size() > 1) {

            //final Map<String,Integer> valueCounts = field.getValueMap();

            //             if (field.isPossibleKeyField())
            //                 //label = String.format("<html><i><b>%s</b> (%d)", field.getName(), field.uniqueValueCount());
            //             else
            //             // we add space before and after to widen the painted background selection around the text a bit
            //             label = String.format(HTML("&nbsp;%s (%d)&nbsp;"), field.getName(), field.uniqueValueCount());
            label = String.format(HTML("%s (%d)"), field.getName(), field.uniqueValueCount());

        }

        return label;
    }

    static class DataNode extends DefaultMutableTreeNode {

        String display;

        protected DataNode(String description) {
            setDisplay(description);
        }

        protected DataNode() {
        }

        Vector<DataNode> getChildren() {
            return super.children;
        }

        Schema getSchema() {
            Util.printStackTrace("getSchema: unimplemented");
            return null;
        }

        DataRow getRow() {
            Util.printStackTrace("getRow: unimplemented");
            return null;
        }

        /** @return false -- override for row nodes */
        boolean isRow() {
            return false;
        }

        /** @return null -- override for value nodes */
        String getValue() {
            return null;
        }

        /** @return null -- override for field nodes */
        Field getField() {
            return null;
        }

        /** @return -1 -- override for value nodes */
        int getCount() {
            return -1;
        }

        /** @return true if this node represents the collection of all possible values found in a column of data */
        boolean isField() {
            return false;
        }

        /** @return true if this node represents a paricular enumerated value from a given column */
        boolean isValue() {
            return !isField();
        }

        LWComponent getStyle() {
            return null;
        }

        boolean hasStyle() {
            return false;
        }

        /** set the label visually displayed in the tree (unannotated) */
        void setDisplay(String s) {
            display = s;
            setUserObject(s); // sets display label
        }

        public String getDisplay() {
            return (String) getUserObject();
        }

        /** noop -- override to provide annotations againast the given map */
        void annotate(LWMap map) {
        }

        void setAnnotation(String s) {
            setPostfix(s);
        }

        void setPostfix(String s) {
            //Log.debug("postfix " + this + " with [" + s + "]");
            if (s == null || s.length() == 0) {
                setUserObject(display);
            } else {
                setUserObject(display + " " + s);
            }
        }

        void setPrefix(String s) {
            //Log.debug("prefix " + this + " with [" + s + "]");
            if (s == null || s.length() == 0) {
                setUserObject(display);
            } else {
                //final String cs = (String) getUserObject();
                if (display.startsWith("<html>")) {
                    setUserObject("<html>" + s + " " + display.substring(6));
                } else
                    setUserObject(s + " " + display);
            }
        }

        /** @return true if this node is tracked for presence in the active map */
        boolean isMapTracked() {
            return false;
            //return isValue();
        }

        boolean isRowNode() {
            return getField() == null;
        }

        /** @return false -- override for semantics */
        boolean isMapPresent() {
            return false;
        }

        /** @return false -- override for semantics */
        boolean isContextChanged() {
            return false;
        }

    }

    final class RowNode extends DataNode {

        final DataRow row;
        boolean isMapPresent;

        RowNode(DataRow row, Field labelField) {
            this.row = row;
            //setDisplay(row.getValue(labelField));
            setDisplay(row.toString());
        }

        @Override
        boolean isRow() {
            return true;
        }

        @Override
        DataRow getRow() {
            return row;
        }

        @Override
        boolean isMapPresent() {
            return isMapPresent;
        }

        @Override
        boolean isContextChanged() {
            return row.isContextChanged();
        }

        @Override
        void annotate(LWMap map) {
            //             final Field keyField = getSchema().getKeyField();
            //             final String keyValue = row.getValue(keyField);
            //             isMapPresent = keyField.countContextValue(keyValue) > 0;

            isMapPresent = row.getContextCount() > 0;
        }

        @Override
        boolean isMapTracked() {
            return true;
        }

        @Override
        boolean isField() {
            return false;
        }

        @Override
        boolean isValue() {
            return false;
        }

        @Override
        Schema getSchema() {
            // return row.getSchema() -- row's don't currently encode the schema
            // if pull a schema stored in the root template node from parent.parent,
            // could skip making this an inner class, and save 4 bytes per row-node at runtime
            return mSchema;
        }

    }

    /**
     * A "field" is really a column from a particular data set, with the additional
     * semantics that we usually always keep around an enumerated list of all the possible
     * unique values that appear in that column.  A FieldNode node will have a list of ValueNodes
     * as children to represent these values.
     */
    private static class FieldNode extends DataNode {

        final Field field;

        FieldNode(Field field, LWComponent.Listener repainter, String description) {
            this.field = field;

            if (description == null) {
                if (field != null)
                    setDisplay(makeFieldLabel(field));
            } else
                setDisplay(description);

            //if (field != null && field.isEnumerated() && !field.isPossibleKeyField())
            //if (field != null && !field.hasStyleNode() && !field.isSingleValue() && field.isEnumerated()) {
            if (field != null && field.hasStyleNode() && !field.isSingleValue() && field.isEnumerated()) {
                // TODO: this means on refresh, the old style node will be pointing via repainter to
                // an AWT component that is no longer displayed, breaking updates!

                // DO NOT CREATE THE STYLE NODE HERE: DO SO IN FIELD -- just UPDATE it with
                // the new repainter here

                //field.setStyleNode(DataAction.makeStyleNode(field, repainter));

                field.getStyleNode().addLWCListener(repainter);
            }
        }

        protected FieldNode(Field field) {
            this.field = field;
        }

        @Override
        Field getField() {
            return field;
        }

        @Override
        Schema getSchema() {
            return field.getSchema();
        }

        @Override
        LWComponent getStyle() {
            return field == null ? null : field.getStyleNode();
        }

        @Override
        boolean hasStyle() {
            return field != null && field.getStyleNode() != null;
        }

        @Override
        boolean isField() {
            return field != null;
        }
    }

    private static final class ValueNode extends FieldNode {

        final String value;
        final int dataSetCount;
        boolean isMapPresent;

        ValueNode(Field field, String value, String label, int dataSetValueCount) {
            super(field);
            setDisplay(label);
            this.value = value;
            this.dataSetCount = dataSetValueCount;
        }

        @Override
        final boolean isMapTracked() {
            return true;
        }

        @Override
        String getValue() {
            return value;
        }

        @Override
        int getCount() {
            return dataSetCount;
        }

        @Override
        void annotate(LWMap map) {

            final int mapCount = field.countContextValue(value);
            if (mapCount > 0) {
                isMapPresent = true;
                if (mapCount != dataSetCount)
                    setPostfix(String.format("[%+d]", mapCount - dataSetCount));
                else
                    setPostfix(null);
                //setPrefix("=");
            } else {
                isMapPresent = false;
                setPostfix(null);
                //setPrefix("-");
            }

            //             // TODO: INCLUDE CURRENT MAP COUNTS
            //             if (field.hasContextValue(value))
            //                 setPrefix("=");
            //             //setAnnotation(null);
            //             else
            //                 setPrefix("+");
            //             //setAnnotation("<font color=red>(new)");
        }

        @Override
        public boolean isField() {
            return false;
        }

        @Override
        public boolean hasStyle() {
            return false;
        }
        //         @Override
        //         public LWComponent getStyle() { return null; }

        @Override
        boolean isMapPresent() {
            return isMapPresent;
        }

        //         @Override
        //         public String toString() { return "ValueNode[" + super.toString() + "; value=" + getValue() + "]"; }

    }

    final class AllRowsNode extends FieldNode {

        final Schema schema;

        //AllRowsNode(Schema schema, LWComponent.Listener _repainter_ignored) {
        AllRowsNode(Schema schema, Object _repainter_ignored) {
            super(null, /*repainter*/null, "All Rows");
            //String.format(HTML("<b><u>All Records in %s (%d)"), schema.getName(), schema.getRowCount()));
            this.schema = schema;
            if (schema.getRowNodeStyle() == null)
                schema.setRowNodeStyle(DataAction.makeStyleNode(schema));
            schema.getRowNodeStyle().addLWCListener(new LWComponent.Listener() {
                public void LWCChanged(tufts.vue.LWCEvent e) {
                    if (DEBUG.EVENTS)
                        Log.debug("ALL-ROWS-NODE UPDATE " + e);
                    DataTree.this.refreshRootNode();
                    //updateLabel(true);
                }
            });
            updateLabel(false);
        }

        private void updateLabel(boolean refresh) {

            // as it currently stands, this never actually needs updating

            setDisplay(String.format(HTML("All Records (%d)"), schema.getRowCount()));

            //             String labelFormat = schema.getRowNodeStyle().getLabel().trim();
            //             if (labelFormat.startsWith("${") && labelFormat.endsWith("}"))
            //                 labelFormat = labelFormat.substring(2, labelFormat.length()-1);
            //             setDisplay(String.format(HTML("<b>All Records</b> (%d) : <b><font color=red>%s"),
            //                                      schema.getRowCount(),
            //                                      labelFormat));

            if (refresh)
                DataTree.this.refreshRootNode();
        }

        @Override
        void annotate(LWMap map) {
            //if (DEBUG.Enabled) setAnnotation(String.format("[%s]", map.getLabel()));
        }

        @Override
        Schema getSchema() {
            return schema;
        }

        @Override
        boolean isField() {
            return false;
        }

        @Override
        boolean isValue() {
            return false;
        }

        @Override
        boolean hasStyle() {
            return true;
        }

        @Override
        LWComponent getStyle() {
            return schema.getRowNodeStyle();
        }
    }

    private static final int IconWidth = 27;
    private static final int IconHeight = 20;

    //private static final Border TopBorder = BorderFactory.createLineBorder(Color.gray);
    //     private static final Border TopBorder = new CompoundBorder(new MatteBorder(3,0,3,0, Color.white),
    //                                                                new CompoundBorder(new LineBorder(Color.gray),
    //                                                                                   GUI.makeSpace(1,0,1,2)));
    //private static final Border TopBorder = GUI.makeSpace(3,0,2,0);
    //private static final Border TopBorder = GUI.makeSpace(0,0,2,0);
    //private static final Border TopBorder = new CompoundBorder(GUI.makeSpace(0,0,10,0), new MatteBorder(0,0,1,0, Color.gray));
    private static final Border TopBorderCollapsed = new CompoundBorder(
            new CompoundBorder(GUI.makeSpace(0, 0, 7, 0), new MatteBorder(0, 0, 1, 0, Color.gray)),
            GUI.makeSpace(0, 0, 7, 0));

    private static final Border TopBorderExpanded = null;

    private static final Border TopTierBorder = GUI.makeSpace(0, 0, 2, 0);
    private static final Border LeafBorder = GUI.makeSpace(0, IconWidth - 16, 2, 0);

    //private static final Icon IncludedInMapIcon = VueResources.getIcon(VUE.class, "images/data_onmap.png");
    //private static final Icon IncludedInMapIcon = GUI.reframeIcon(VueResources.getIcon(VUE.class, "images/data_onmap.png"), 8, 16);
    //private static final Icon NewToMapIcon = VueResources.getIcon(VUE.class, "images/data_offmap.png");

    private static final int RIPS = Util.isMacPlatform() ? 20 : 16; // RowIconPointSize

    private static final Icon RowHasChangedIcon = makeIcon(0x229B, RIPS, Color.green.darker(), -2, -1);
    private static final Icon RowOnMapIcon = makeIcon(0x229B, RIPS, VueConstants.COLOR_SELECTION, -2, -1);
    private static final Icon RowOffMapIcon = makeIcon(0x229B, RIPS, Color.lightGray, -2, -1);
    private static final Icon ValueOnMapIcon = makeIcon(0x25C9, 12, VueConstants.COLOR_SELECTION, 0, 0);
    private static final Icon ValueOffMapIcon = makeIcon(0x25C9, 12, Color.lightGray, 0, 0);
    //     private static final Icon RowOnMapIcon = makeIcon(0x25C9, 14, VueConstants.COLOR_SELECTION);
    //     private static final Icon RowOffMapIcon = makeIcon(0x25C9, 14, Color.lightGray);
    //     private static final Icon ValueOnMapIcon = makeIcon(0x229B, 18, VueConstants.COLOR_SELECTION, 0, -1);
    //     private static final Icon ValueOffMapIcon = makeIcon(0x229B, 18, Color.lightGray, 0, -1);

    //private static final Icon UniqueValueOnMapIcon = makeIcon(0x29BF, 16, VueConstants.COLOR_SELECTION, 0, -1);
    //private static final Icon UniqueValueOffMapIcon = makeIcon(0x29BF, 16, Color.lightGray, 0, -1);
    private static final Icon UniqueValueOnMapIcon = makeIcon(0x229A, 16, VueConstants.COLOR_SELECTION, 0, 0);
    private static final Icon UniqueValueOffMapIcon = makeIcon(0x229A, 16, Color.lightGray, 0, 0);

    // 29BE: 
    // 29BF: 
    // 25C9:  
    // 25CE: 
    // 229A: 
    // 25E6: 
    // 229D: 
    // 229B:     

    private static Icon makeIcon(int code, int pointSize, Color color) {
        return makeIcon(code, pointSize, color, 0, 0);
    }

    private static Icon makeIcon(int code, int pointSize, Color color, int xoff, int yoff) {
        return GUI.makeUnicodeIcon(code, pointSize, color, 16, // fixed width
                16, // fixed height
                4 + xoff, // xoff
                4 + yoff // yoff
        );

    }

    //     private static final GUI.ResizedIcon NewToMapIcon =
    //         new GUI.ResizedIcon(VueResources.getIcon(GUI.class, "icons/MacSmallCloseIcon.gif"), 16, 16);

    private static final Color KeyFieldColor = Color.green.darker();

    private static final Icon TestIcon = VueResources.getImageIcon("dataSourceRSS");

    private class DataRenderer extends DefaultTreeCellRenderer {

        {
            //setIconTextGap(2);
            //setBorder(LeafBorder);
            setVerticalTextPosition(SwingConstants.CENTER);
            //setTextSelectionColor(Color.blue); // text color selected
            //setTextNonSelectionColor(Color.green); // text color normal
            //setBackgroundSelectionColor(VueConstants.COLOR_SELECTION.brighter());
            setBackgroundSelectionColor(VueResources.getColor("dataTree.selected.background", Color.blue));
            setBorderSelectionColor(VueConstants.COLOR_SELECTION);
            //setBackgroundSelectionColor(VueConstants.COLOR_HIGHLIGHT);
            //setFont(tufts.vue.VueConstants.SmallFixedFont);
        }

        //@Override public int getWidth() { return 500; }

        public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean selected,
                final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) {
            //Log.debug(Util.tags(value));
            //Log.debug(Util.tags(value));
            if (!(value instanceof DataNode))
                return super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);

            final DataNode treeNode = (DataNode) value;
            final Field field = treeNode.getField();

            //setIconTextGap(4); // pre &nbsp; standard HTML
            setIconTextGap(1);

            super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);

            setForeground(Color.black); // must do every time for some reason, or de-selected text goes invisible

            if (treeNode.hasStyle()) {
                //setIconTextGap(4);
                // Note: icons at top level do not get the background selection color painted
                // behind them for some reason, whereas leaf node icons do, so this icon
                // needs to take that into account, and should be careful not to paint over
                // the selected & focus-active border of the row item.
                setIcon(FieldIconPainter.load(treeNode.getStyle(), selected ? backgroundSelectionColor : null));

            } else {

                if (field != null && field.isSingleton()) {

                    setIcon(null);

                } else if (!treeNode.isMapTracked()) {

                    setIcon(null);

                } else {

                    setIconTextGap(1);

                    if (treeNode.isRow()) {
                        if (treeNode.isContextChanged())
                            setIcon(RowHasChangedIcon);
                        else if (treeNode.isMapPresent())
                            setIcon(RowOnMapIcon);
                        else
                            setIcon(RowOffMapIcon);
                    } else {
                        if (field != null && field.isPossibleKeyField()) {
                            if (treeNode.isMapPresent())
                                setIcon(UniqueValueOnMapIcon);
                            else
                                setIcon(UniqueValueOffMapIcon);
                            //                         } else if (field != null && field.isQuantile() && treeNode.getCount() <= 0) {
                            //                             setIcon(null);
                        } else {
                            if (treeNode.isMapPresent())
                                setIcon(ValueOnMapIcon);
                            else
                                setIcon(ValueOffMapIcon);
                        }
                    }
                }
            }

            if (row == 0) {
                if (expanded)
                    setBorder(TopBorderExpanded);
                else
                    setBorder(TopBorderCollapsed);
                //setBorder(TopBorder);
                //setBackgroundNonSelectionColor(Color.lightGray);
                //setFont(EnumFont);
            } else {
                //setBackgroundNonSelectionColor(null);
                //setFont(null);
                //setBorder(leaf ? LeafBorder : null);
                if (leaf) {
                    if (treeNode.isField() && treeNode.getField().isSingleton())
                        setBorder(TopTierBorder);
                    else
                        setBorder(LeafBorder);
                } else {
                    setBorder(null);
                }
            }

            return this;
        }
    }

    private static final java.awt.geom.Rectangle2D IconSize = new java.awt.geom.Rectangle2D.Float(0, 0, IconWidth,
            IconHeight);
    //     private static final java.awt.geom.Rectangle2D IconInsetSize
    //         = new java.awt.geom.Rectangle2D.Float(1,2,IconWidth-2,IconHeight-4);

    private static final NodeIconPainter FieldIconPainter = new NodeIconPainter();

    private static final Icon EmptyIcon = new GUI.EmptyIcon(IconWidth, IconHeight);

    private static final double ViewScaleDown = 0.5;
    private static final double ViewScale = 1 / ViewScaleDown;
    private static final java.awt.geom.Rectangle2D IconViewSize = new java.awt.geom.Rectangle2D.Double(
            2 * ViewScale, 4 * ViewScale + 0.5, (IconWidth - 4) * ViewScale, (IconHeight - 8) * ViewScale);

    private static final Stroke NodeIconBorder = new BasicStroke(0.5f);

    private static class NodeIconPainter implements Icon {

        LWComponent node;
        Color fill;

        public Icon load(LWComponent c, Color fill) {
            if (c == null)
                Log.error("null node; fill=" + fill, new Throwable("HERE"));
            this.node = c;
            this.fill = fill;
            return this;
        }

        public int getIconWidth() {
            return IconWidth;
        }

        public int getIconHeight() {
            return IconHeight;
        }

        public void paintIcon(Component c, Graphics _g, int x, int y) {
            //Log.debug("x="+x+", y="+y);

            if (node == null) {
                if (DEBUG.Enabled)
                    Log.warn("null node in " + getClass().getName());
                return;
            }

            java.awt.Graphics2D g = (java.awt.Graphics2D) _g;

            g.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING,
                    java.awt.RenderingHints.VALUE_ANTIALIAS_ON);

            if (fill != null) {
                if (DEBUG.BOXES) {
                    g.setColor(Color.red);
                    g.fillRect(x, y, IconWidth, IconHeight);
                } else {
                    //                     g.setColor(fill);
                    //                     g.setColor(Color.red);
                    //                     // add to width to also fill the IconTextGap
                    //                     // TODO: this is painting over edge of active selected border color
                    //                     g.fillRect(0,0,IconWidth+8,IconHeight+8);
                }
            }

            // we should only be seeing LWNode's, which always have RectanularShape
            final RectangularShape shape = (RectangularShape) node.getZeroShape();

            shape.setFrame(IconViewSize);
            g.setColor(node.getFillColor());
            g.scale(ViewScaleDown, ViewScaleDown);
            g.fill(shape);
            g.setStroke(NodeIconBorder);
            g.setColor(Color.gray);
            g.draw(shape);
            g.scale(ViewScale, ViewScale);

            //node.setSize(IconSize);

            //             node.drawFit(new DrawContext(g.create(), node),
            //                          IconSize,
            //                          2);
            //             //node.drawFit(g, x, y);
        }

    }
}

//     // this type of node was only for intial prototype
//     private static LWComponent makeDataNodes(Schema schema, Field field)
//     {

//         Log.debug("PRODUCING KEY FIELD NODES " + field);
//         int i = 0;
//         for (DataRow row : schema.getRows()) {
//             n = new LWNode();
//             n.setClientData(Schema.class, schema);
//             n.getMetadataList().add(row.entries());
//             if (field != null) {
//                 final String value = row.getValue(field);
//                 n.setLabel(makeLabel(field, value));
//             } else {
//                 //n.setLabel(treeNode.getStyle().getLabel()); // applies initial style
//             }
//             nodes.add(n);
//             //Log.debug("setting meta-data for row " + (++i) + " [" + value + "]");
//             //                     for (Map.Entry<String,String> e : row.entries()) {
//             //                         // todo: this is slow: is updating UI components, setting cursors, etc, every time
//             //                         n.addMetaData(e.getKey(), e.getValue());
//             //                     }
//         }
//         Log.debug("PRODUCED META-DATA IN " + field);

//     }

//     private static LWComponent makeDataNode(Schema schema)
//     {
//         int i = 0;
//         LWNode node;
//         for (DataRow row : schema.getRows()) {
//             node = new LWNode();
//             node.setClientData(Schema.class, schema);
//             node.getMetadataList().add(row.entries());
//             node.setStyle(schema.getStyleNode()); // must have meta-data set first to pick up label template

//             nodes.add(n);
//             //Log.debug("setting meta-data for row " + (++i) + " [" + value + "]");
//             //                     for (Map.Entry<String,String> e : row.entries()) {
//             //                         // todo: this is slow: is updating UI components, setting cursors, etc, every time
//             //                         n.addMetaData(e.getKey(), e.getValue());
//             //                     }
//         }
//         Log.debug("PRODUCED META-DATA IN " + field);

//     }