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