/* * 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 java.awt.Component; import java.awt.Dimension; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.Action; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import tufts.vue.DEBUG; import tufts.vue.LWPropertyChangeEvent; import tufts.vue.VueAction; import tufts.vue.VueResources; /** * A button that supports a drop-down menu and changes state based on the * currently selected drop-down menu item. * The menu can be initialized from either an array of property values, or actions. * Property change events or action events (depending on the initialization type) * are fired when the menu selection changes. * * Subclasses must implement LWEditor produce/display * * @version $Revision: 1.33 $ / $Date: 2010-02-03 19:15:47 $ / $Author: mike $ * @author Scott Fraize * */ // as this class is now specialized to handle vue LWKey properties, // it's no longer generic gui. Can create subclass, VuePopupMenu, // that does the LWPropertyHandler impl, and move that to the // forthcoming tool package. public abstract class MenuButton<T> extends JButton implements ActionListener, tufts.vue.LWEditor<T> // todo: cleaner to get this to subclass from JMenu, and then cross-menu drag-rollover // menu-popups would automatically work also. { protected Object mPropertyKey; protected T mCurrentValue; protected JPopupMenu mPopup; protected JMenuItem mEmptySelection; private Icon mButtonIcon; //protected static final String ArrowText = "v "; private static boolean isButtonActionable = false; private boolean actionAreaClicked = false; private Insets insets(int x) { return new Insets(x,x,x,x); } public MenuButton() { if (false) { //setBorder(BorderFactory.createEtchedBorder()); //setBorder(new CompoundBorder(BorderFactory.createRaisedBevelBorder(), new EmptyBorder(3,3,3,3))); //setBorder(new CompoundBorder(BorderFactory.createEtchedBorder(), new LineBorder(Color.blue, 6))); //setBorder(BorderFactory.createRaisedBevelBorder()); } else { //setOpaque(false); setContentAreaFilled(false); setBorder(null); //setBorder(new LineBorder(Color.blue, 2)); //setBorder(new EmptyBorder(2,2,2,2)); } //setBorderPainted(false); if (GUI.isMacAqua()) { // anything big makes them rounded & fit into their space. // We could make them square making them smaller, but will // need to force the toolbar row to small height, or compute // the insets needed for each button based on it's minimum size. setMargin(insets(22)); } setFocusable(false); addActionListener(this); final int borderIndent = 2; addMouseListener(new tufts.vue.MouseAdapter(toString()) { public void mousePressed(MouseEvent e) { if (!MenuButton.this.isEnabled()) return; /* if (//getText() == ArrowText && isButtonActionable && getIcon() != null && e.getX() < getIcon().getIconWidth() + borderIndent) { actionAreaClicked = true; } else { */ actionAreaClicked = false; Component c = e.getComponent(); getPopupMenu().show(c, 0, (int) c.getBounds().getHeight()); } }); } /** if the there's an immediate action area of the button pressed, fire the property setter right away instead of popping the menu (actionAreaClicked was determined in mousePressed) */ public void actionPerformed(ActionEvent e) { if (DEBUG.TOOL) System.out.println(this + " " + e); if (actionAreaClicked) firePropertySetter(); } /** * Set the raw icon used for displaying as a button (may be additionally decorated). * Intended for use during initialization. **/ public void setButtonIcon(Icon i) { if (DEBUG.BOXES||DEBUG.TOOL) System.out.println(this + " setButtonIcon " + i); //if (DEBUG.Enabled) new Throwable().printStackTrace(); _setIcon(mButtonIcon = i); } /*Intended for use during initialization OR then later for value changes. public void setOrResetButtonIcon(Icon i) { System.out.println("MenuButton " + this + " setButtonIcon " + i); new Throwable().printStackTrace(); if (mButtonIcon == null && i != null) { _setIcon(mButtonIcon = i); } if (i instanceof BlobIcon && mButtonIcon instanceof BlobIcon) { // in this case, just transfer color from the given icon to // our already existing color. ((BlobIcon)mButtonIcon).setColor(((BlobIcon)i).getColor()); } else { System.out.println("MenuButton " + this + " setButtonIcon: INITIALIZED"); mButtonIcon = i; } } */ protected Icon getButtonIcon() { return mButtonIcon; } /* override of JButton so can catch blob-icon public void XsetIcon(Icon i) { if (mButtonIcon != null) { if (i instanceof BlobIcon && mButtonIcon instanceof BlobIcon) ((BlobIcon)mButtonIcon).setColor(((BlobIcon)i).getColor()); } else { System.out.println("MenuButton " + this + " setIcon " + i); _setIcon(i); } } */ /** return the default button size for this type of button: subclasses can override */ protected Dimension getButtonSize() { return new Dimension(32,22); // better at 22, but get clipped 1 pix at top in VueToolbarController! todo: BUG } private void _setIcon(Icon i) { /* super.setIcon(i); super.setRolloverIcon(new VueButtonIcon(i, VueButtonIcon.ROLLOVER)); */ /* final int pad = 7; Dimension d = new Dimension(i.getIconWidth()+pad, i.getIconHeight()+pad); if (d.width < 21) d.width = 21; // todo: config if (d.height < 21) d.height = 21; // todo: config */ //if (DEBUG.BOXES||DEBUG.TOOL) System.out.println(this + " _setIcon " + i); Dimension d = getButtonSize(); if (true || !GUI.isMacAqua()) { if (false) VueButtonIcon.installGenerated(this, i, d); else VueButtonIcon.installGenerated(this, new MenuProxyIcon(i), d); //System.out.println(this + " *** installed generated, setPreferredSize " + d); } setPreferredSize(d); } protected JPopupMenu getPopupMenu() { return mPopup; } public void setPropertyKey(Object key) { mPropertyKey = key; } public Object getPropertyKey() { return mPropertyKey; } /** factory method for subclasses -- build's an icon for menu items */ protected Icon makeIcon(T value) { return null; } /** override if there is a custom menu item */ protected Object runCustomChooser() { return null; } /** @param values can be property values or actions (or even a mixture) */ protected void buildMenu(Object[] valuesOrActions) { buildMenu(valuesOrActions, null, false); } /** Key for JMenuItem's: a place to store a property value for this menu item */ public static final String ValueKey = "property.value"; /** * @param values can be property values or actions * @param names is optional * @param createCustom - add a "Custom" menu item that calls runCustomChooser * * The ability to pass in an array of actions is a convenience to create * the needed JMenuItem's using the Action.NAME & Action.SMALL_ICON & MenuButton.ValueKey * values stored in the action. The action is not actually fired when the menu * item is selected (this used to be the case, but no longer). * The action's will be expected to hava a value under the key * MenuButton.ValueKey representing the value of the object. (This only * works for actions that set specific values every time they fire). * * OLD: * If values are actions, the default handleValueSelection won't ever * do anything as a value wasn't set on the JMenuItem -- it's assumed * that the action is handling the value change. In this case override * handleMenuSelection to change the buttons appearance after a selection change. */ protected void buildMenu(Object[] values, String[] names, boolean createCustom) { mPopup = new JPopupMenu(); //final String valueKey = getPropertyName() + ".value"; // propertyName usually not set at this point! final ActionListener menuItemAction = new ActionListener() { public void actionPerformed(ActionEvent e) { handleMenuSelection(e); }}; for (int i = 0; i < values.length; i++) { JMenuItem item; T value; Icon icon = null; if (values[i] instanceof Action) { Action a = (Action) values[i]; item = new JMenuItem((String) a.getValue(Action.NAME)); value = (T) a.getValue(ValueKey); icon = (Icon) a.getValue(Action.SMALL_ICON); } else { item = new JMenuItem(); value = (T) values[i]; } item.putClientProperty(ValueKey, value); if (icon == null) icon = makeIcon(value); if (icon != null) item.setIcon(icon); if (names != null) item.setText(names[i]); item.addActionListener(menuItemAction); mPopup.add(item); } if (createCustom) { JMenuItem item = new JMenuItem(VueResources.getString("menubutton.custom")); // todo: more control over this item item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleValueSelection(runCustomChooser()); }}); mPopup.add(item); } mEmptySelection = new JMenuItem(); mEmptySelection.setVisible(false); mPopup.add(mEmptySelection); } protected void buildMenu(Class<T> enumType) { T[] values = enumType.getEnumConstants(); if (values == null) throw new Error("no enum constants for (not an enum?) " + enumType); String[] names = new String[values.length]; int i = 0; for (T e : values) names[i++] = e.toString(); buildMenu(values, names, false); } protected void handleMenuSelection(ActionEvent e) { // Note: based on the action event, if the source JMenuItem had an icon set, // we could automatically set the displayed button icon to the same here. // This doesn't help us tho, as MenuButtons need to be able to handle // property value changes happening outside of this component, and then // reflecting that value, which means in the LWPropertyProducer.setPropertyValue, // we have to provide a specific mapping from the given property value to the // selected menu item anyway. (Altho: if all our JMenuItems had a "property.value" // set in them, we could search thru them every time to figure out which icon // to set as a default...) if (DEBUG.TOOL) System.out.println("\n" + this + " handleMenuSelection " + e); handleValueSelection(((JComponent)e.getSource()).getClientProperty(ValueKey)); } /** Simulate a user value selection */ public void selectValue(Object value) { handleValueSelection(value); } protected void handleValueSelection(Object newPropertyValue) { if (DEBUG.TOOL) System.out.println(this + " handleValueSelection: newPropertyValue=" + newPropertyValue); // TODO: this is getting fired twice, once for ItemEvent stateChange=DESELECTED, and // then the one we really want, with itemState=SELECTED. We want to ignore the former, // as it's generating extra property sets on the selection that are immediately // overriden by the SELECTED value. This should actually be harmless, but // it's definitely unexpected internal behaviour. -- SMF 2007-05-01 14:55.37 if (newPropertyValue == null) // could be result of custom chooser return; // even if we were build from actions, in which case the LWComponents // have already been changed via that action, call setPropertyValue // here so any listening LWCToolPanels can update their held state, // and so subclasses can update their displayed selected icons // Okay, do NOT call this with the action? But what happens if nothing is selected? if (newPropertyValue instanceof Action) { System.out.println("Skipping setPropertyValue & firePropertyChanged for Action " + newPropertyValue); } else { Object oldValue = produceValue(); displayValue((T)newPropertyValue); firePropertyChanged(oldValue, newPropertyValue); } repaint(); } /** @return the currently selected value (interface LWEditor) */ public T produceValue() { if (DEBUG.TOOL) System.out.println(this + " produceValue " + mCurrentValue); return mCurrentValue; } /** Sub-classes can set the current value (mCurrentValue), and must update the display (icon, etc) and repaint (interface LWEditor) */ public abstract void displayValue(T value); /** fire a property change event even if old & new values are the same */ // COULD USE Component.firePropertyChange! all this is getting us is diagnostics! protected void firePropertyChanged(Object oldValue, Object newValue) { if (getPropertyKey() != null) { PropertyChangeListener[] listeners = getPropertyChangeListeners(); if (listeners.length > 0) { PropertyChangeEvent event = new LWPropertyChangeEvent(this, getPropertyKey(), oldValue, newValue); for (int i = 0; i< listeners.length; i++) { if (DEBUG.TOOL && (DEBUG.EVENTS || DEBUG.META)) System.out.println(this + " fires " + event + " to " + listeners[i]); listeners[i].propertyChange(event); } } } } protected void firePropertySetter() { Object o = produceValue(); if (DEBUG.TOOL) System.out.println(this + " firePropertySetter " + o); if (o instanceof Action) { if (o instanceof VueAction) ((VueAction)o).fire(this); else { Action a = (Action) o; a.actionPerformed(new ActionEvent(this, 0, (String) a.getValue(Action.NAME))); } } else { firePropertyChanged(o, o); } } public void paint(java.awt.Graphics g) { ((java.awt.Graphics2D)g).setRenderingHint (java.awt.RenderingHints.KEY_TEXT_ANTIALIASING, java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON); super.paint(g); /* if (true) setToolTipText(null);//tmp debug int w = getWidth(); int h = getHeight(); g.setColor(Color.black); final int arrowWidth = 5; // make sure is odd # int x = w - (arrowWidth + 3); int y = h / 2 - 1; for (int len = arrowWidth; len > 0; len -= 2) { g.drawLine(x,y,x+len,y); y++; x++; } */ } public String toString() { return getClass().getName() + "[" + getPropertyKey() + "]"; } } /* * paint( Graphics g) * Overrides paint method and renders an additional icon ontop of * of the normal rendering to indicate if this button contains * a popup handler. * * param Graphics g the Graphics. **/ /* // default offsets for drawing popup arrow via code public int mArrowSize = 3; public int mArrowHOffset = -9; public int mArrowVOffset = -7; // the popup overlay icons for up and down states public Icon mPopupIndicatorIconUp = null; public Icon mPopupIndicatorDownIcon = null; public void paint( java.awt.Graphics pGraphics) { super.paint( pGraphics); if (true) return; Dimension dim = getPreferredSize(); Insets insets = getInsets(); // now overlay the popup menu icon indicator // either from an icon or by brute painting if( (! false ) && (mPopup != null) && ( !mPopup.isVisible() ) ) { // draw popup arrow Color saveColor = pGraphics.getColor(); pGraphics.setColor( Color.black); int w = getWidth(); int h = getHeight(); int x1 = w + mArrowHOffset; int y = h + mArrowVOffset; int x2 = x1 + (mArrowSize * 2) -1; for(int i=0; i< mArrowSize; i++) { pGraphics.drawLine(x1,y,x2,y); x1++; x2--; y++; } pGraphics.setColor( saveColor); } else // Use provided popup overlay icons if( (mPopup != null) && ( !mPopup.isVisible() ) ) { Icon overlay = null; if( isSelected() ) { overlay = mPopupIndicatorDownIcon; } if( overlay != null) { overlay.paintIcon( this, pGraphics, insets.top, insets.left); } } } */