/*
* 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.
 */


package tufts.vue.gui;

import tufts.Util;
import tufts.vue.DEBUG;
import tufts.vue.VueResources;
import tufts.vue.VueUtil;
import java.beans.PropertyChangeEvent;
import java.util.*;
import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

//import com.sun.tools.xjc.generator.unmarshaller.automaton.Alphabet.SuperClass;

/**
 * A vertical stack of collapsable/expandable regions containing arbitrary JComponent's.
 *
 * Note that the ultimate behaviour of the stack will be very dependent on the
 * the preferredSize/maximumSize/minimumSize settings on the contained JComponent's.
 *
 * @version $Revision: 1.58 $ / $Date: 2010-02-03 19:15:47 $ / $Author: mike $
 * @author Scott Fraize
 */
public class WidgetStack extends Widget
    implements Scrollable
{
    private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(WidgetStack.class);
    
    private final JPanel mGridBag;
    private final GridBagConstraints _gbc = new GridBagConstraints();
    private final Insets ExpandedTitleBarInsets = GUI.EmptyInsets;
    private final Insets CollapsedTitleBarInsets = new Insets(0,0,1,0);
    private final JComponent mDefaultExpander;
    private final Collection mWidgets = new ArrayList();
    private final GridBagLayout mLayout;

    private WidgetTitle mLockedWidget = null;
    private int mExpanderCount = 0;
    private int mExpandersOpen = 0;

    //private Dimension mMinSize = new Dimension();

    public WidgetStack() {
        this("<>");
    }        

    public WidgetStack(String name) {
        super(name);
        mGridBag = this;
        mLayout = new GridBagLayout();
        setLayout(mLayout);
        init();
        
        if (DEBUG.BOXES) setBorder(new javax.swing.border.LineBorder(Color.cyan, 4));

        // We now add a component "guaranteed" to be at the bottom of the stack
        // (gridy=64), that starts invisible, but when all other vertical expanders
        // become invisible (are closed), this component will be set to visible (tho it
        // will display nothing), and will eat up any leftover vertical space at the
        // bottom of gridbag (it has a non-zero weighty), so if everything is collapsed,
        // the titleBar's all go to the top, instead of the middle.  (In a grid bag,
        // there must always be at least one visible component with a non-zero gridy
        // value, or everything is clumped in the center of the display area).
        
        GridBagConstraints c = (GridBagConstraints) _gbc.clone();
        c.fill = GridBagConstraints.BOTH;
        c.anchor = GridBagConstraints.SOUTH;
        c.weighty = 1;
        c.gridy = 64; 
        mDefaultExpander = new JPanel(new BorderLayout());
        mDefaultExpander.setMinimumSize(new Dimension(0,0));
        mDefaultExpander.setPreferredSize(new Dimension(0,0));
        if (DEBUG.BOXES) {
            mDefaultExpander.setOpaque(true);
            mDefaultExpander.setBackground(Color.darkGray);
            JLabel l = new JLabel(VueResources.getString("jlabel.widgetstack"), JLabel.CENTER);
            l.setForeground(Color.white);
            mDefaultExpander.add(l);
        }
        mDefaultExpander.setVisible(false);
        add(mDefaultExpander, c);

        //mMinSize.width = 100;

        // todo: need to set min size on whole stack (nitems * title height)
        // and have DockWindow respect this.
    }

    private void init() {
        _gbc.gridwidth = GridBagConstraints.REMAINDER; // last in line as only one column
        _gbc.anchor = GridBagConstraints.NORTH;
        _gbc.weightx = 1;                              // always expand horizontally
        _gbc.gridx = 0;
        _gbc.gridy = 0;

    }

    @Override
    public void removeAll() {
        super.removeAll();
        mWidgets.clear();
        mExpanderCount = 0;
        mExpandersOpen = 0;
        mLockedWidget = null;
        init();
    }

    public void setTitleItem(String s) {
        putClientProperty("TITLE-ITEM", s);
    }
    


    /**
     * At least one pane MUST have a non-zero vertical expansion
     * weight (usually values between 0.0 and 1.0: they are meaningful only
     * relative to each other), otherwise the panes will all clump
     * together in the middle.
     */
    
    public void addPane(String title, JComponent widget, float verticalExpansionWeight) {
        //verticalExpansionWeight=0.0f;
        boolean isExpander = (verticalExpansionWeight != 0.0f);

        if (isExpander)
            mExpanderCount++;
        
        WidgetTitle titleBar = new WidgetTitle(title, widget, isExpander);        
        mWidgets.add(titleBar);
        //mMinSize.height = mWidgets.size() * (TitleHeight + 1);
        //setMinimumSize(mMinSize);
        
        if (DEBUG.WIDGET) {
            out("addPane:"
                + " expansionWeight=" + verticalExpansionWeight
                //+ " expanderCnt=" + mExpanderCount
                //+ " isExpander=" + isExpander
                + " [" + title + "] containing " + GUI.name(widget));
            GUI.dumpSizes(widget, "WidgetStack.addPane");
        }


        _gbc.weighty = 0;
        _gbc.fill = GridBagConstraints.HORIZONTAL;
        _gbc.insets = ExpandedTitleBarInsets;
        if (!isBooleanTrue(widget, TITLE_HIDDEN_KEY) && !title.startsWith("_"))
            mGridBag.add(titleBar, _gbc);
        
        _gbc.gridy++;
        _gbc.fill = GridBagConstraints.BOTH;
        _gbc.weighty = verticalExpansionWeight;
        _gbc.insets = GUI.EmptyInsets;

        if (false) {

            // if component has no border, add the default one
            // Actually, this also no good: what if a scroll-pane?
            if (widget.getBorder() == null && !(widget instanceof JScrollPane))
                widget.setBorder(GUI.WidgetInsetBorder);
            mGridBag.add(widget, _gbc);

            /*
            // Enforced white-space border
            JPanel widgetPanel = new JPanel(new BorderLayout());
            widgetPanel.setOpaque(false);
            widgetPanel.add(widget);
            widgetPanel.setBorder(GUI.WidgetInsetBorder);
            mGridBag.add(widgetPanel, _gbc);
            */
        } else {
            if (false && Widget.wantsScroller(widget)) {
                // Would need to handle seeing up through the contained
                // widget for property changes: e.g. hidden would
                // need to hide the scroller, not the widget.
                JScrollPane scroller = new JScrollPane(widget,
                                                       JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                                                       JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
                mGridBag.add(scroller, _gbc);
            } else {
                mGridBag.add(widget, _gbc);
            }
        }
            

        _gbc.gridy++;

        //if (!widget.isPreferredSizeSet()) {// note: a java 1.5 api call only
        //if (!isExpander && !widget.isMinimumSizeSet())
        //    widget.setMinimumSize(widget.getPreferredSize());                
        
    }

    @Override
    public void addNotify() {
        //if (DEBUG.WIDGET) out("minSize " + mMinSize);
        updateDefaultExpander();
        super.addNotify();
        if (mExpanderCount == 0)
            if (DEBUG.WIDGET) out("no vertical expanders");
            //tufts.Util.printStackTrace("warning: no vertical expanding panes; WidgetStack will not layout properly");
        setName("in " + GUI.name(getParent()));

        //if (DEBUG.Enabled) Log.debug("addNotify: " + GUI.name(this) + "; PARENT=" + getParent());
        if (DEBUG.DOCK || DEBUG.WIDGET) Log.debug("addNotify: " + GUI.name(this));
    }
    
//     @Override
//     public Dimension getMaximumSize() {
//         return getPreferredSize();
//     }
    
//     @Override
//     public final Dimension getMinimumSize() {
//         if (isMinimumSizeSet())
//             return super.getMinimumSize();
//         else
//             return sizeTrack("getMinimumSize");
//     }
    
    @Override
    public final Dimension getPreferredSize() {
        return sizeTrack("getPreferredSize");
    }

    /**
     * If we've been requested to track the size of an object (normally for scroll-pane viewport auto-size purposes),
     * Enforce reporting our preferred with as the width of what's being tracked, and our preferred height as
     * no less than what is being tracked.  This is so that when in a JScrollPane with a vertical scroll bar, and
     * no horizontal scrollbar, we fill to the exact width (no more, no less), and we can use up all available
     * visible vertical space (so any auto-expanding Widget's can take up the slack).
     */
    private Dimension sizeTrack(String src)
    {
        final JComponent sizeTrack = (JComponent) getClientProperty("VUE.sizeTrack");
        if (sizeTrack != null) {
            if (DEBUG.WIDGET) out(src + "; TRACKING SIZE OF: " + GUI.name(sizeTrack));
            Dimension ps;
            try {
                ps = super.getPreferredSize();
            } catch (Throwable t) {
                // can get: Exception in thread "*AWT*" java.lang.ArrayIndexOutOfBoundsException: 512
                // if too many items (>=512) are loaded into a GridBag in Java prior 1.6.
                
                // TODO:
                // Also, have seen java.lang.ArrayIndexOutOfBoundsException: 3
                //
                // Seen scrolling to bottom of search results
                //
                //  Stack dump showed: at javax.swing.text.CompositeView.getView(CompositeView.java:143)
                // Thread dump showed: at sun.font.AppleNativeStrike.getNativeGlyphImageBounds(Native Method)

                //Log.warn(String.format("%s; sizeTrack(%s); %s", getName(), src, t));

                Log.warn(String.format("%s; sizeTrack(%s);", getName(), src), t);
                ps = new Dimension(200, 1024);
            }
                
            final int th = sizeTrack.getHeight();
            final int tw = sizeTrack.getWidth();
            if (th > ps.height) {
                if (DEBUG.WIDGET) out(src + "; OVERRIDE PREF-HEIGHT " + ps.height + " WITH TRACK HEIGHT " + th);
                ps.height = th;
            }
            if (tw != ps.width) {
                // if (DEBUG.WIDGET) out(src + "; OVERRIDE  PREF-WIDTH " + ps.width +  " WITH TRACK  WIDTH " + tw);
                ps.width = tw;
            }
            return ps;
        } else {
            return super.getPreferredSize();
        }
    }
    
    /** interface Scrollable */
    public Dimension getPreferredScrollableViewportSize() {
        Dimension d = getPreferredSize();
        if (DEBUG.WIDGET) out("GPSVS " + Util.fmt(d));
        return d;
    }

    /** interface Scrollable -- clicking on the up/down arrows of the scroll bar use this */
    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
        //return 128;
        return 16;
    }
    /** interface Scrollable */
    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
        return 32;
        //return 128;
    }
    
    /** interface Scrollable */
    public boolean getScrollableTracksViewportWidth() { return false; }
    /** interface Scrollable */
    public boolean getScrollableTracksViewportHeight() { return false; }
    

    /**

     * The given widget *must* already have it's name set to be used as the title.

     * Note that if verticalExpansionWeight is zero, it is also important that the given
     * JComponent provides a reasonable preferredSize or minimumSize. Most Swing
     * components provide a reasonable preferredSize automatically, but pay attention to
     * layout managers that might not do such a good job of passing up this information.
     * Also of particular note are JScrollPanes, which by default usually will collapse
     * down to about nothing unless you manually set the pref or min sizes them (or, say a
     * container they're laid out in that they're expanding to fill).
     
     **/
    public void addPane(JComponent widget, float verticalExpansionWeight) {
        addPane(widget.getName(), widget, verticalExpansionWeight);
    }
    

    public void addPane(String title, JComponent widget) {
        addPane(title, widget, 0.0f);
    }

    public Widget addPane(String title) {
        Widget w = new Widget(title);
        addPane(w, 1.0f);
        return w;
    }

    /** @param c must have name set (setName) */
    public void addPane(JComponent c) {
        addPane(c, 1f);
    }

    private void updateDefaultExpander() {    	
        //System.out.println("EXPANDERS OPEN: " + mExpandersOpen);
        if (mExpandersOpen == 0){        	
            mDefaultExpander.setVisible(true);
        }
        else
            mDefaultExpander.setVisible(false);
    }

    private void updateLockingState() {
        /*
        if (DEBUG.WIDGET) out("updateLockingState: expanders open = " + mExpandersOpen);
        if (mExpandersOpen == 1) {
            findFirstOpenExpander().setLocked(true);
        } else if (mLockedWidget != null) {
            mLockedWidget.setLocked(false);
        }
        */
    }

    private WidgetTitle findFirstOpenExpander() {
        WidgetTitle w;
        Iterator i = mWidgets.iterator();
        while (i.hasNext()) {
            w = (WidgetTitle) i.next();
            if (w.isExpander && w.mExpanded && w.isVisible())
                return w;
        }
        return null;
    }

    public static final int TitleHeight = VueResources.getInt("gui.widget.title.height", 18);
    public static final Color
        TopGradient = VueResources.getColor("gui.widget.title.background.top", 108,149,221),
        BottomGradient = VueResources.getColor("gui.widget.title.background.bottom", 80,123,197);
    
    // Mac Finder top blue: Color(79,154,240);
    // Mac Finder bottom blue: Color(0,133,246);
    private static final GradientPaint Gradient
        = new GradientPaint(0,           0, TopGradient,
                            0, TitleHeight, BottomGradient);
    private static final GradientPaint GradientEmbedded
        = new GradientPaint(0,           0, new Color(79,154,240),
                            0, TitleHeight, new Color(0,133,246));

    //private static final char RightArrowChar = 0x25B6; // unicode "black right pointing triangle"
    //private static final char DownArrowChar = 0x25BC; // unicode "black down pointing triangle"
    private static final char RightArrowChar = DockWindow.RightArrowChar;
    private static final char DownArrowChar = DockWindow.DownArrowChar;

    private static final boolean isMac = tufts.Util.isMacPlatform();

    //private static final char Chevron = 0xBB; // unicode "right-pointing double angle quotation mark"

    private boolean _forcePrefSize = false;
    @Override
    public Dimension getSize() {
        if (DEBUG.WIDGET) {
            Dimension d;
            if (_forcePrefSize) {
                d = getPreferredSize();
                out("getSize; OVERRIDE SIZE w/PREF " + Util.fmt(d));
            } else {
                d =  super.getSize();
                out("getSize " + Util.fmt(d));
            }
            return d;
        } else {
            return super.getSize();
        }
    }

    class WidgetTitle extends Box implements java.beans.PropertyChangeListener {

        private final JLabel mTitle;
        private final MenuButton mMenuButton;
        private final JComponent mWidget;
        private final GUI.IconicLabel mIcon;
        private final RefreshButton refreshButton;
        private final HelpButton helpButton;
        private final MiscActionButton miscActionButton;
        
        private final boolean isExpander;

        private boolean isLocked = false;
        private boolean mExpanded = true;
        private boolean mEmbeddedStack = false;
        private boolean mTitleVisible = true; // if false, display widget, but not title (implies no user control)

        private Color mTopColor;
        private final GradientPaint mGradient;

        public WidgetTitle(String label, JComponent widget, boolean isExpander) {
            super(BoxLayout.X_AXIS);
            this.isExpander = isExpander;
            setName(label);
            setOpaque(true);
            mWidget = widget;
            mTitle = new JLabel(label);
            GUI.init(mTitle, "gui.widget.title");
            
            String localName = widget.getName();
            if (localName == null)
                localName = label;
            
            //All the results Panes have different names so to group the category
            //I did this, maybe Widget should be rewritten so it has a type? instead of using
            //name for this.
            if (localName.startsWith("Searching ") || localName.startsWith("Loading "))
            	localName = "resultsPane";
            
            if (localName != null) {
            	//if the name contains spaces use the type instead
            	
                String localKey = "gui.widget.title." + localName;                
                GUI.init(mTitle, localKey);
                mTopColor = VueResources.getColor(localKey + ".background.top");
               
                if (mTopColor != null) {
                    Color botColor = VueResources.getColor(localKey + ".background.bottom", BottomGradient);
                    mGradient = new GradientPaint(0,           0, mTopColor,
                                                  0, TitleHeight, botColor);
                } else {
                    mGradient = Gradient;
                }
                mTitle.setText(VueResources.getString(localKey + ".text", label));
            } else
                mGradient = Gradient;

            if (mTopColor == null)
                mTopColor = TopGradient;
            
            // TODO: merge with DockWindow for offset / std property
            add(Box.createHorizontalStrut(isMac ? 17 : 6));
//             int iconHeight = 10;
//             int iconWidth = 9;
//             int fontSize = 9;
//             mIcon = new GUI.IconicLabel(DownArrowChar, fontSize, Color.white, iconWidth, iconHeight);

            // TODO: merge with DockWindow code for same
            mIcon = new GUI.IconicLabel(DownArrowChar,
                                        16, // point-size
                                        Color.white,
                                        15, // fixed width
                                        16); // fixed height
            //if (isMac)
                 mIcon.setBorder(GUI.makeSpace(0,0,1,0)); // t,l,b,r
             
            add(mIcon);
            
            add(Box.createHorizontalStrut(isMac ? 1 : 2));
            add(mTitle);

            add(Box.createGlue());
            
//             if (true) {
//                 //final AbstractButton addAll = new VueButton("add");
//                 final AbstractButton addAll = new JButton(); 
//                 addAll.setIcon(VueResources.getIcon("add.raw"));
//                 addAll.setOpaque(false);
//                 addAll.setBorder(null);
//                 //addAll.setBorderPainted(false);
//                 addAll.setAlignmentY(0.5f);                
//                 addAll.setBorder(GUI.makeSpace(0,0,1,0));
//                 add(addAll);
//                 add(Box.createHorizontalStrut(2));
//             }
            
            refreshButton = new RefreshButton("refreshButton",null);
            add(refreshButton);
            
            
            miscActionButton = new MiscActionButton();
            add(miscActionButton);
        
            helpButton = new HelpButton(null);
            add(helpButton);
            
            //    add(Box.createHorizontalStrut(isMac ? 1 : 2));        
            
            mMenuButton = new MenuButton(null);
            mMenuButton.setOpaque(false);           
            add(mMenuButton);            
            setPreferredSize(new Dimension(50, TitleHeight));
            setMaximumSize(new Dimension(Short.MAX_VALUE, TitleHeight));
            setMinimumSize(new Dimension(50, TitleHeight));

            addMouseListener(new tufts.vue.MouseAdapter(label) {
                    //public void mouseClicked(MouseEvent e) { if (!isLocked) Widget.setExpanded(mWidget, !mExpanded); }
                    public void mouseClicked(MouseEvent e) { handleMouseClicked(); }
                    public void mousePressed(MouseEvent e) { if (!isLocked) mIcon.setForeground(mTopColor.brighter()); }
                    public void mouseReleased(MouseEvent e) { if (!isLocked) mIcon.setForeground(Color.white); }
                });

            // Check for property values set on the JComponent before being added to the
            // WidgetStack.  Important to handle EXPANSION_KEY before HIDDEN_KEY
            // (changing the expansion of something that is hidden is currently
            // undefined).  If the property is already set, we handle it via a fake
            // propertyChangeEvent.  If it isn't set, we set the default, which won't
            // trigger a recursive propertyChangeEvent as we haven't added us as a
            // property change listener yet.

            // todo: title-hidden currently only takes effect at init-time -- no dynamic update
            mTitleVisible = !isBooleanTrue(widget, TITLE_HIDDEN_KEY);
            
//             final Object expanded = widget.getClientProperty(EXPANSION_KEY);
//             if (expanded != null) {
//                 propertyChange(new PropertyChangeEvent(this, EXPANSION_KEY, null, expanded));
//             } else {
//                 // expanded by default
//                 setBoolean(widget, EXPANSION_KEY, true);
//                 handleWidgetDisplayChange(true); 
//             }
//             final Object hidden = widget.getClientProperty(Widget.HIDDEN_KEY);
//             if (hidden != null) {
//                 propertyChange(new PropertyChangeEvent(this, HIDDEN_KEY, null, hidden));
//             } else {
//                 // not hidden by default
//                 setBoolean(widget, HIDDEN_KEY, false);
//             }

            if (!handlePresetProperty(EXPANSION_KEY)) {
                // expanded by default
                setBoolean(widget, EXPANSION_KEY, true);
                handleWidgetDisplayChange(true); 
            }
                
            if (!handlePresetProperty(HIDDEN_KEY)) {
                // not hidden by default
                setBoolean(widget, HIDDEN_KEY, false);
            }

            handlePresetProperty(REFRESH_ACTION_KEY);
            // shouldn't we be handling all of these?  DataSourceViewer current sets help & misc actions,
            // but I don't think those property sets could ever have effect...
            //handlePresetProperty(MISC_ACTION_KEY);
            //handlePresetProperty(HELP_ACTION_KEY);

            widget.addPropertyChangeListener(this);
            
        }  

      
        /** @return true if the given property key was found with an already set value
         * (if found, will simulate a property change event to handle the property) */
        private boolean handlePresetProperty(String key) {

            final Object value = mWidget.getClientProperty(key);
            if (value != null) {
                propertyChange(new PropertyChangeEvent(this, key, null, value));
                return true;
            } else {
                return false;
            }
            
        }

        public void addNotify() {
            super.addNotify();
            mEmbeddedStack = (SwingUtilities.getAncestorOfClass(WidgetStack.class, getParent()) != null);
        }

        private void setLocked(boolean locked) {
            if (isLocked == locked)
                return;
            isLocked = locked;
            if (locked) {
                mIcon.setForeground(mTopColor.darker());
                mLockedWidget = this;
            } else {
                mIcon.setForeground(Color.white);
                mLockedWidget = null;
            }
        }

        private void handleMouseClicked() {
            if (isLocked)
                return;
            boolean doExpand = !mExpanded;
            Widget.setExpanded(mWidget, doExpand);
            
            if (doExpand || DEBUG.WIDGET) {
                final Rectangle bounds = getBounds();
                final int titleHeight = bounds.height;
                final int wh = mWidget.getPreferredSize().height;
                
                if (DEBUG.WIDGET) {
                    out("TITLE BOUNDS", Util.fmt(bounds) + "; height=" + titleHeight);
                    out("WIDGT HEIGHT", wh);
                }

                // Ideally, we'd add in both the widget height, and the another title
                // height below, meaning we'd scroll to see the full widget, plus the
                // next widget title bar below.  But there's some bug with
                // scrollRectToVisible, presumably related to the scroll pane not
                // adjusting in time, where we actually get better results right now if
                // the region we request scrolling to is simply the widget title.
                
                //bounds.height += wh;

                // Add the height if the title in again, so that in case there's another
                // widget below us, we at least can see it's title.
                // Technically, don't need to add if last one, but can't scroll off bottom.
                
                //bounds.height += titleHeight;
                
                if (DEBUG.WIDGET) out("TOTAL BOUNDS", Util.fmt(bounds));

                //_forcePrefSize=true;

                final JScrollPane s = (JScrollPane) SwingUtilities.getAncestorOfClass(JScrollPane.class, this);
                if (s != null) {
                    final JViewport vp = s.getViewport();
                    final Component view = vp.getView();

                    // Validating/invalidating lets it work when expanding *down* a
                    // widget w/out messing with the scroll bar / scroll region, but
                    // non-validating works when we want to expand something "up" at
                    // bottom and then scroll down to see it (making it look like it's
                    // "opening upwards")
                    
                    // both invalidate then validate on the view required for above hack to work
                    view.invalidate();
                    // is working best at the moment letting the validate happen in JViewport.scrollRectToVisible:
                    //view.validate(); 
                    
                    if (DEBUG.WIDGET){
                        out("VIEWPORT SIZE",        Util.fmt(vp.getSize()));
                        out("EXTENT SIZE",          Util.fmt(vp.getExtentSize()));
                        out("VIEW POSITION",        Util.fmt(vp.getViewPosition()));
                        out("VIEW SIZE",            Util.fmt(view.getSize()));
                        out("PrefVIEW SIZE",        Util.fmt(view.getPreferredSize()));
                        out("VIEW", GUI.name(view));
                        //vp.setViewSize(view.getPreferredSize()); // hack
                    }
                }


                if (!doExpand) 
                    return; // only get here if DEBUG.WIDGET

                if (wantsScrollerAlways(WidgetStack.this)) {
                    
                    // this works best if scroll-bar is a constant.  this also produces
                    // the least jumpy results (no flashing) in cases where we
                    // transition from no-scroll bar to having a scroll bar, although
                    // this method can be less accurate -- it's probably missing changes
                    // to the viewport size that haven't completed yet

                    if (DEBUG.WIDGET) out("IMMEDIATE SCROLL");
                    scrollRectToVisible(bounds);
                    //_forcePrefSize=false;
                    
                } else {

                    // This produces the most accurate results, tho is pretty much
                    // always going to show some flashing in the scroll-bar.
                    
                    if (DEBUG.WIDGET) out("DELAYED SCROLL");
                    final JScrollPane sp = (JScrollPane) SwingUtilities.getAncestorOfClass(JScrollPane.class, this);
                    if (sp != null) {
                        sp.getVerticalScrollBar().setValueIsAdjusting(true);
                        GUI.invokeAfterAWT(new Runnable() { public void run() {
                            // Wait for scroll-bar to appear and/or adjust after
                            // we're made visible before doing this
                            scrollRectToVisible(bounds);
                            sp.getVerticalScrollBar().setValueIsAdjusting(false);
                            //_forcePrefSize=false;
                            if (DEBUG.WIDGET) out("DELAYED SCROLL COMPLETE");
                        }});
                    }
                }
            }
        }


        /** interface java.beans.PropertyChangeListener for contained component */
        public void propertyChange(java.beans.PropertyChangeEvent e) {
            final String key = e.getPropertyName();
        
            if (DEBUG.WIDGET && (true || !key.equals("ancestor")))
                out(GUI.name(e.getSource()) + " property \"" + key + "\" newValue=[" + GUI.name(e.getNewValue()) + "]");

            if (key == EXPANSION_KEY) {
                setWidgetExpanded( ((Boolean)e.getNewValue()).booleanValue() );
                
            } else if (key == HIDDEN_KEY) {
                setWidgetHidden( ((Boolean)e.getNewValue()).booleanValue() );

            } else if (key == MENU_ACTIONS_KEY) {
                mMenuButton.setMenuActions((Action[]) e.getNewValue());
                
            } else if (key == MISC_ACTION_KEY) {
                miscActionButton.setAction((MouseListener)e.getNewValue());
                                
            } else if (key == MISC_ICON_KEY) {
                miscActionButton.setIconPrefix((String)e.getNewValue());                
                
            } else if (key == REFRESH_ACTION_KEY) {
                refreshButton.setAction((MouseListener)e.getNewValue());
                
            } else if (key == HELP_ACTION_KEY) {                
                helpButton.setHelpText((String)e.getNewValue());
            } else if (key.equals("name")) {
                mTitle.setText((String) e.getNewValue());
                
            } else if (key.endsWith("Size")) {

                Component src = (Component) e.getSource();
            
                if (DEBUG.WIDGET) GUI.dumpSizes(src);
                if (true||DEBUG.WIDGET) out("revalidate on size property change");
                revalidate();
                if (DEBUG.WIDGET) GUI.dumpSizes(src);
            }
        }

        /**
         * This method only does something if isExpander is true: track how many
         * expanding Widgets (in the GridBagLayout) are visible, beacuse when we get
         * down to 0, we need to add a default expander to take up the remaining space.
         */
        private void handleWidgetDisplayChange(boolean visible) {
        	
            if (!isExpander)
                return;

            if (visible)
                mExpandersOpen++;
            else
                mExpandersOpen--;

            if (DEBUG.WIDGET) out("VISIBLE EXPANDER COUNT: " + mExpandersOpen);
            
            if (mExpandersOpen < 0 || mExpandersOpen > mExpanderCount)
                Log.warn(this + "; handleWidgetDisplayChange=" + visible,
                         new IllegalStateException("WidgetStack: expanders claimed open: "
                                                   + mExpandersOpen
                                                   + ", expander count=" + mExpanderCount));

            // Could do: if only one widget open, change constraints on THAT guy to
            // expand...  Or, as soon as all expanders close, set remaining ones to
            // expand equally?  That could just get ugly tho (titles keep moving down
            // out from under your mouse -- this currently happens only on our last item
            // in the stack which isn't so bad the way we're using it, but for
            // everything?) Really need second tier expander marks for stuff
            // w/scroll-panes (use negative expansion weights?)  Subclassing
            // GridBagLayout might make this easier also.

            // new: if last expander open, and we close it,
            // open the next visible expander below, or if none, the
            // one above, or none, don't allow this to collapse.

            WidgetStack.this.updateDefaultExpander();
                
        }

        private void updateLockingStateLater() {
            GUI.invokeAfterAWT(new Runnable() { public void run() {
                updateLockingState();
            }});
        }

        private void setWidgetHidden(boolean hide) {
            if (DEBUG.WIDGET) out("setWidgetHidden " + hide);

            if (mTitleVisible && isVisible() == !hide)
                return;

            setVisible(mTitleVisible && !hide);
            
            if (hide) {
                if (mExpanded) {
                    mWidget.setVisible(false);
                    handleWidgetDisplayChange(false);
                }
            } else if (mExpanded) {
                mWidget.setVisible(true);
                handleWidgetDisplayChange(true);
            }

            updateLockingStateLater();
        }
        

        private void setWidgetExpanded(boolean expanded) {
            if (DEBUG.WIDGET) out("setWidgetExpanded", expanded);
            if (mExpanded == expanded)
                return;
            mExpanded = expanded;
            if (mExpanded) {
                mIcon.setText("" + DownArrowChar);
            } else {
                mIcon.setText("" + RightArrowChar);
            }

            handleWidgetDisplayChange(mExpanded);

            GridBagConstraints c = mLayout.getConstraints(this);            
            c.insets = expanded ? ExpandedTitleBarInsets : CollapsedTitleBarInsets;
            mLayout.setConstraints(this, c);           
            mWidget.setVisible(expanded);

            revalidate();
            
            updateLockingStateLater();
            
        }

        public void paint(Graphics g) {
            if (!isMac) {
                // this is on by default on the mac
                Graphics2D g2 = (Graphics2D) g;
                g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            }
            super.paint(g);
        }

        public void paintComponent(Graphics g) {
            paintGradient((Graphics2D)g);
            //In Java 6 calling super.paintComponent() here prevents the Gradient from painting,
            //since all widgetTitles have gradients I think this should be OK will need to back test in Java 5.
         //   super.paintComponent(g);
        }

        private void paintGradient(Graphics2D g)
        {
        	//System.out.println("paint gradient " + mGradient.toString());
            if (false && mEmbeddedStack)
                g.setPaint(GradientEmbedded);
            else
                g.setPaint(mGradient);
            g.fillRect(0, 0, getWidth(), TitleHeight);
        }


        private void out(Object o) {
            Log.debug(GUI.name(this) + " " + (o==null?"null":o.toString()));
            //System.err.println(GUI.name(this) + " " + (o==null?"null":o.toString()));
        }
        private void out(String s, Object o) {
            Log.debug(String.format("%s %17s: %s", GUI.name(this), s, o));
        }

    
    }
    
    private void out(Object o) {
        Log.debug(GUI.name(this) + " " + (o==null?"null":o.toString()));
        //System.err.println(GUI.name(this) + " " + (o==null?"null":o.toString()));
    }

   
    class MenuButton extends JButton {
    	private boolean mouse_over = false;
    	MenuButton thisbutton;

    	MenuButton(Action[] actions) {
            //super(iconChar, 18, defaultColor, TitleHeight, TitleHeight);
        	super();
        	thisbutton = this;        	
            setName(WidgetStack.this.getName());   
            //setSize(new Dimension(100,25));
            setText(" "+VueResources.getString("option")+" ");           
        	//setIcon(VueResources.getImageIcon("dockWindow.menuIcon.raw"));
        	setRolloverEnabled(true);
        	//setRolloverIcon(VueResources.getImageIcon("dockWindow.menuIcon.hover"));
        	Insets noInsets=new Insets(5,5,5,5);
        	//store the icon you want to display in imageIcon		        		
        	setMargin(noInsets);			
        //	setBorder(BorderFactory.createEmptyBorder());
        	setContentAreaFilled(false);
        //  setAlignmentY(0.5f);
            // todo: to keep manually picking a height and a bottom pad to get this
            // middle aligned is no good: will eventually want to use a TextLayout to
            // get precise bounds for center, and create as a real Icon
            //setBorder(GUI.makeSpace(3,3,3,3));
            setMenuActions(actions);
            Font macFont = new Font("Lucinda Grande", Font.BOLD, 11);
    		Font windowsFont = new Font("Lucida Sans Unicode", Font.BOLD, 11);
    		boolean isWindows = VueUtil.isWindowsPlatform();
    		setForeground(Color.white);	    		
    		if(isWindows){
    			setFont(windowsFont);
    		}else{
    			setFont(macFont);
    		}

    		addMouseListener(new MouseAdapter() 
    	      {
    	      public void mouseEntered(MouseEvent e) {
    	    	  mouse_over=true;
    	    	  thisbutton.repaint();
    	      }
    	      public void mouseExited(MouseEvent e) {
    	    	  mouse_over=false;
    	    	  thisbutton.repaint();}
    	      });
    		setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
        }
        void setMenuActions(Action[] actions) {
            clearMenuActions();

            //if (DEBUG.Enabled) setName(WidgetStack.this.getName() + " " + Arrays.asList(actions));

            if (actions == null) {
                setVisible(false);
                return;
            }
            setVisible(true);

            new GUI.PopupMenuHandler(this, GUI.buildMenu(actions)) {
                public void mouseEntered(MouseEvent e) { 
                 	setForeground(Color.white);                  	
                }
                public void mouseExited(MouseEvent e) { 
                	setForeground(Color.white);                	               	
                }
                public int getMenuX(Component c) { return c.getWidth(); }
                public int getMenuY(Component c) { return -getY(); } // 0 in parent
            };

            repaint();
        }

        private void clearMenuActions() {
            MouseListener[] ml = getMouseListeners();
            for (int i = 0; i < ml.length; i++) {
                if (ml[i] instanceof GUI.PopupMenuHandler)
                    removeMouseListener(ml[i]);
            }
        } 
        protected void paintComponent(Graphics g) {
            // Paint the default look of the button.
            super.paintComponent(g);
            // Your custom painting here.
            int w = getWidth();
            int h = getHeight(); 
            Image centerImg = VueResources.getImageIcon("dockWindow.panner.menu.raw.center").getImage();
            Image leftImg = VueResources.getImageIcon("dockWindow.panner.menu.raw.leftcap").getImage();   
            Image rightImg = VueResources.getImageIcon("dockWindow.panner.menu.raw.rightcap").getImage();
            Font macFont = new Font("Lucinda Grande", Font.BOLD, 11);
    		Font windowsFont = new Font("Lucida Sans Unicode", Font.BOLD, 11);
    		boolean isWindows = VueUtil.isWindowsPlatform();
    		///setForeground(Color.white);	
    		Font font = null;
    		if(isWindows){
    			font = windowsFont;
    		}else{
    			font = macFont;
    		}    		
            if(mouse_over)
            {
            	centerImg = VueResources.getImageIcon("dockWindow.panner.menu.raw.center_ov").getImage();
            	leftImg = VueResources.getImageIcon("dockWindow.panner.menu.raw.leftcap.hover").getImage();
            	rightImg = VueResources.getImageIcon("dockWindow.panner.menu.raw.rightcap.hover").getImage();      	 
      	
            }            
            if(isWindows){
            	g.drawImage(leftImg,0,1, 10 , h-1, this);
            	g.drawImage(centerImg,10,1, w - (rightImg.getWidth(null)+13) , h-1, this);
            	g.drawImage(rightImg,w-11,h/2-7, rightImg.getWidth(null) , rightImg.getHeight(null), this);            	
                g.setFont(font);
            	g.drawString(VueResources.getString("option"),10,h-5);            	
            }else{
            	g.drawImage(leftImg,0,0, 10 , h+1, this);
            	g.drawImage(centerImg,10,0, w-(rightImg.getWidth(null)+13) , h+1, this);
            	g.drawImage(rightImg,w-8,h/2-7, rightImg.getWidth(null) , rightImg.getHeight(null)+1, this);            	
                g.setFont(font);            	  
            	g.drawString(VueResources.getString("option"),10,h-3);             	
            }
        }
    }   


    static class HelpButton extends VueLabel implements MouseListener {
        

        HelpButton(Action[] actions) {
            //super(iconChar, 18, defaultColor, TitleHeight, TitleHeight);
        	super();
        	setIcon(VueResources.getImageIcon("dockWindow.helpIcon.raw"));
        	this.setFocusable(true);
        	Insets noInsets=new Insets(5,0,0,0);

        	// todo: to keep manually picking a height and a bottom pad to get this
            // middle aligned is no good: will eventually want to use a TextLayout to
            // get precise bounds for center, and create as a real Icon
        	addMouseListener(this);
            setBorder(GUI.makeSpace(3,3,3,3));
            setHelpText(null);
        }    
        
        public void setHelpText(String text) 
        {            
            if (text == null) {
            	this.setToolTipText(null);
                setVisible(false);
                return;
            }
            else
            {
            	this.setToolTipText(text);
            	setVisible(true);
            }
        }        
        
        public void mouseClicked(MouseEvent arg0) {
			// TODO Auto-generated method stub
			
		}

		public void mousePressed(MouseEvent arg0) {
			// TODO Auto-generated method stub
			
		}

		public void mouseReleased(MouseEvent arg0) {
			// TODO Auto-generated method stub
			
		}

		public void mouseEntered(MouseEvent arg0) {			
			setIcon(VueResources.getImageIcon("dockWindow.helpIcon.hover"));
			repaint();
			
		}

		public void mouseExited(MouseEvent arg0) {
			setIcon(VueResources.getImageIcon("dockWindow.helpIcon.raw"));
			repaint();
			
		}                

    }
    
    static class RefreshButton extends JLabel implements MouseListener {
    	
// Would make more sense as extending AbstractButton, but is having trouble being visible that way.
//         RefreshButton(String icon, MouseListener action) {
//             setIcon(VueResources.getImageIcon(icon+".raw"));        	     
//             setRolloverIcon(VueResources.getImageIcon(icon+".hover"));
//             setText("bang");
//             setBorderPainted(false);
//             setRolloverEnabled(true);
//             setAction(action);           
//         }
        
    	private final String iconChar;
    
        RefreshButton(String icon, MouseListener action) {
            iconChar = icon;
            setIcon(VueResources.getImageIcon(iconChar+".raw"));        	     
            setAlignmentY(0.5f);
            setBorder(GUI.makeSpace(3,0,3,5));
            addMouseListener(this);
            setAction(action);           
        }

        public void setAction(MouseListener action) 
        {
            clearAction();

            if (action == null) {
                setVisible(false);
                return;
            }
            else
            {            	
            	addMouseListener(this);
            	addMouseListener(action);
            	setVisible(true);
            }

        }

        private void clearAction() {
            MouseListener[] ml = getMouseListeners();
            for (int i = 0; i < ml.length; i++) {                
                    removeMouseListener(ml[i]);
            }
        }

		public void mouseClicked(MouseEvent arg0) {
			// TODO Auto-generated method stub
			
		}

		public void mousePressed(MouseEvent arg0) {
			// TODO Auto-generated method stub
			
		}

		public void mouseReleased(MouseEvent arg0) {
			// TODO Auto-generated method stub
			
		}

		public void mouseEntered(MouseEvent arg0) {			
			setIcon(VueResources.getImageIcon(iconChar+".hover"));
			
		}

		public void mouseExited(MouseEvent arg0) {
			setIcon(VueResources.getImageIcon(iconChar+".raw"));
			
		}                
    }

    static class MiscActionButton extends JLabel implements MouseListener{
    	
    	private String iconChar;
    	
        MiscActionButton() {
        	super();
        	
        	        	     
        	Insets noInsets=new Insets(0,0,0,0);
            setAlignmentY(0.5f);
            setBorder(GUI.makeSpace(3,0,3,5));
            addMouseListener(this);
            setAction(null);         
            
        }

        public void setIconPrefix(String prefix)
        {
        	iconChar = prefix;
        	if (iconChar != null)
        		setIcon(VueResources.getImageIcon(iconChar+".raw")); 
        }
        public void setAction(MouseListener action) 
        {
            clearAction();

            if (action == null) {
                setVisible(false);
                return;
            }
            else
            {            	
            	addMouseListener(this);
            	addMouseListener(action);
            	setVisible(true);
            }

        }

        private void clearAction() {
            MouseListener[] ml = getMouseListeners();
            for (int i = 0; i < ml.length; i++) {                
                    removeMouseListener(ml[i]);
            }
        }

		public void mouseClicked(MouseEvent arg0) {
			if (iconChar != null)
			{
				GUI.invokeAfterAWT(new Runnable()
	    		{
	    			public void run()
	    			{
	    				setIcon(VueResources.getImageIcon(iconChar+".raw"));		
	    			}
	    		});
			}
			
		}

		public void mousePressed(MouseEvent arg0) {
			// TODO Auto-generated method stub
			
		}

		public void mouseReleased(MouseEvent arg0) {
			// TODO Auto-generated method stub
			
		}

		public void mouseEntered(MouseEvent arg0) {		
			if (iconChar != null)
			{
				GUI.invokeAfterAWT(new Runnable()
	    		{
	    			public void run()
	    			{
	    				setIcon(VueResources.getImageIcon(iconChar+".hover"));		
	    			}
	    		});
			}
				
			
			
		}

		public void mouseExited(MouseEvent arg0) {
			if (iconChar != null)
			{
				GUI.invokeAfterAWT(new Runnable()
	    		{
	    			public void run()
	    			{
	    				setIcon(VueResources.getImageIcon(iconChar+".raw"));		
	    			}
	    		});
			}
			
		}                
    }

    public static void main(String args[])
    {
        // todo: appears to be a bug in GridBagLayout where if ALL
        // components are expanders, in can sometimes add a pixel
        // at the top of the freakin layout.  This example
        // was all weights of 1.0.  The pixel can come in and
        // out even during resizes: some sizes just trigger it...
        // Okay, even if NOT all are expanders it can fail.
        // Christ.  Yet our ResourcePanel stack and DRBrowser
        // stack work fine... Okay, thoes have only ONE expander...
        
        tufts.vue.VUE.init(args);
        
        WidgetStack s = new WidgetStack("Test");

        String[] names = new String[] { "One",
                                        "contentPreview",
                                        "contentInfo",
                                        "Four",
        };

        for (int i = 0; i < names.length; i++) {
            s.addPane(names[i], new JLabel(names[i], SwingConstants.CENTER), 1f);
        }
        //s.addPane("Fixed", new JLabel("fixed"), 0f);
        
        GUI.createDockWindow(VueResources.getString("dockWindow.WidgetStackTest.title"), s).setVisible(true);
    }

}    

