/* * 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.vue.DEBUG; import tufts.vue.VueAction; import tufts.vue.LWComponent; import tufts.vue.LWPropertyChangeEvent; import java.io.*; import java.awt.*; import java.awt.event.*; import java.beans.*; import java.util.*; import javax.swing.*; import javax.swing.border.*; /** * @version $Revision: 1.14 $ / $Date: 2010-02-03 19:15:46 $ / $Author: mike $ */ // 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 ComboBoxMenuButton<T> extends JComboBox implements ActionListener, tufts.vue.LWEditor<T> { /** Key for JMenuItem's: a place to store a property value for this menu item */ public static final String VALUE_KEY = "property.value"; protected Object mPropertyKey; protected T mCurrentValue; private Map<T,Icon> mIconCache; private static final Icon NO_ICON = new Icon() { public int getIconWidth() { return 0; } public int getIconHeight() { return 0; } public void paintIcon(Component c, Graphics g, int x, int y) {} }; public ComboBoxMenuButton() { setFocusable(false); if (false && tufts.Util.isMacPlatform()) { // disabled for now: for some reason, is not taking effect for LinkToolPanel subclass //putClientProperty("JComboBox.isPopDown", Boolean.TRUE); // can't see what difference this makes putClientProperty("JComboBox.isSquare", Boolean.TRUE); } } public void setPropertyKey(Object key) { mPropertyKey = key; } public Object getPropertyKey() { return mPropertyKey; } /** @return the currently selected value (interface LWEditor) */ public T produceValue() { if (DEBUG.TOOL) System.out.println(this + " produceValue " + mCurrentValue); return mCurrentValue; } public void displayValue(T newValue) { //if (DEBUG.TOOL && DEBUG.META) System.out.println(this + " displayValue " + newValue); if (DEBUG.TOOL) System.out.println(this + " displayValue " + newValue); mCurrentValue = newValue; setSelectedItem(newValue); /* //if (mCurrentValue == null || !mCurrentValue.equals(newValue)) { if (mCurrentValue != newValue) { mCurrentValue = newValue; setSelectedItem(newValue); } else { System.err.println("EQUALS: " + mCurrentValue + "=" + newValue); } */ } /** factory method for subclasses -- build's an icon for menu items */ protected Icon makeIcon(T value) { return null; } public T getMenuValueAt(int index) { Object item = getItemAt(index); if (item instanceof Action) return (T) ((Action)item).getValue(VALUE_KEY); else return (T) item; } protected Icon getIconForValue(Object value) { Icon icon = null; if (mIconCache != null) icon = mIconCache.get(value); if (icon == NO_ICON) return null; if (icon != null) return icon; if (value instanceof Action) { Action a = (Action) value; icon = (Icon) a.getValue(Action.SMALL_ICON); if (icon == null) { value = a.getValue(VALUE_KEY); icon = makeIcon((T) value); //a.putValue(Action.SMALL_ICON, icon); // warning: side effect } } else { icon = makeIcon((T) value); } if (mIconCache == null) mIconCache = new HashMap(); //System.out.println("Loading cache for [" + value + "] with " + icon); mIconCache.put((T) value, icon == null ? NO_ICON : icon); return icon; } /** 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); } /** * @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! this.addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent 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]); // ((Component)values[i]).addActionListener(menuItemAction); // this.addActionListener(menuItemAction); // mPopup.add(item); addItem(values[i]); } /* if (createCustom) { JMenuItem item = new JMenuItem("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(ItemEvent 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 (e.getStateChange() != ItemEvent.SELECTED) {// ignore de-selections if (DEBUG.TOOL) System.out.println("\n" + this + " handleMenuSelection " + e + " (IGNORED)"); return; } if (DEBUG.TOOL) System.out.println(this + " handleMenuSelection " + e); if (e.getItem() instanceof Action) handleValueSelection((T) ((Action)e.getItem()).getValue(VALUE_KEY)); else handleValueSelection((T) e.getItem()); } protected void handleValueSelection(T newPropertyValue) { if (DEBUG.TOOL) System.out.println(this + " handleValueSelection: newPropertyValue=" + newPropertyValue); 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 { T oldValue = produceValue(); displayValue(newPropertyValue); firePropertyChanged(oldValue, newPropertyValue); } repaint(); } /** 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); } public String toString() { return getClass().getName() + "[" + getPropertyKey() + "]"; } }