/*
Copyright (c) 2003-2009 ITerative Consulting Pty Ltd. All Rights Reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:
o Redistributions of source code must retain the above copyright notice, this list of conditions and
the following disclaimer.
o Redistributions in binary form must reproduce the above copyright notice, this list of conditions
and the following disclaimer in the documentation and/or other materials provided with the distribution.
o This jcTOOL Helper Class software, whether in binary or source form may not be used within,
or to derive, any other product without the specific prior written permission of the copyright holder
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package DisplayProject.controls;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Window;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import javax.swing.ComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.text.JTextComponent;
import org.apache.log4j.Logger;
import DisplayProject.Constants;
import DisplayProject.DropListModel;
import DisplayProject.ListField;
import DisplayProject.StatusTextListener;
import DisplayProject.UIutils;
import DisplayProject.actions.Editable;
import DisplayProject.actions.ElementList;
import DisplayProject.actions.WidthPolicy;
import DisplayProject.table.ArrayFieldCellHelper;
import Framework.Array_Of_ListElement;
import Framework.EventManager;
import Framework.ListElement;
import Framework.TextData;
/**
* A combo box that can automatically resize when its underlying model
* changes
*/
public class AutoResizingComboBox extends JComboBox implements ListField
{
// TF:28/07/2009:Changed this to implement ListField, instead of the subclasses implementing list field.
private static Logger logger = Logger.getLogger(AutoResizingComboBox.class);
private static final long serialVersionUID = 6898659606998961409L;
private ComboBoxResizer cbr;
private Dimension sizeForModel = new Dimension(0, 0);
/**
* A flag to indicate whether this combobox should be read only, that is the user
* can drop the list of values down and have a look at them, but not change the selection
*/
private boolean readOnly = false;
public AutoResizingComboBox() {
// TF:24/07/2008:Set the default background colour to inherit
this.setBackground(null);
// TF:07/05/2009:Added a mouse wheel listener
this.addMouseWheelListener(new ComboBoxMouseWheelHandler());
// TF:13/11/2009:DET-130:Added status line listener
this.addMouseListener(StatusTextListener.sharedInstance());
// TF:15/12/2009:DET-141:Set this up to allow inheriting of popup menu
this.setInheritsPopupMenu(true);
}
public void setModel(ComboBoxModel aModel) {
super.setModel(aModel);
sizeForModel = new Dimension(0, 0);
cbr = new ComboBoxResizer();
aModel.addListDataListener(cbr);
//PM:12/10/07 - removed at OTAGO because it causes the cell size to be incorrect if the first element
// has has an empty string "" as its value
// this.setRenderer( new DefaultListCellRenderer() {
// @Override
// public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
// if (list.getFixedCellHeight() <= 0) {
// FontMetrics fm = this.getFontMetrics(this.getFont());
// list.setFixedCellHeight(fm.getHeight());
// }
// return super.getListCellRendererComponent(list, value, index, isSelected,
// cellHasFocus);
// }
// });
}
public void sizeToNatural()
{
cbr.resize();
}
public Dimension getMinimumSize() {
// CraigM:02/10/2008 - If we have an explicit size, then ignore the size for model
if (WidthPolicy.get(this) == Constants.SP_EXPLICIT) {
return super.getMinimumSize();
}
Dimension minimum = super.getMinimumSize();
if (minimum != null) {
if (minimum.height < getSizeForModel().height
|| minimum.width < getSizeForModel().width) {
double width = Math.max(minimum.width, getSizeForModel().width);
double height = Math.max(minimum.height, getSizeForModel().height);
minimum = new Dimension((int)width, (int)height);
}
}
else {
minimum = getSizeForModel();
}
return minimum;
}
private Object oldVal = null;
public void setSelectedItemWithoutPosting(Object anObject) {
// Programmatically setting the selected item -- remove the action listeners then add them back
ActionListener[] listeners = this.getActionListeners();
for (int i = 0; i < listeners.length; i++) {
this.removeActionListener(listeners[i]);
}
oldVal = super.getSelectedItem();
super.setSelectedItem(anObject);
for (int i = 0; i < listeners.length; i++) {
this.addActionListener(listeners[i]);
}
}
public void setSelectedItem(Object anObject) {
// CraigM:15/12/2008 - Allow changes if modifying the value through code (Ie: Event posting is disabled)
if (!readOnly || EventManager.isPostingEnabled() == false) {
Object oldVal = super.getSelectedItem();
// TF:30/07/2009:DET-107:If we're in an array field and the popup is visible, then when the new item is selected
// we should not allow the editor to close. Without this, using the mouse to change an item in a combo box
// would result in focus being lost as the editor was removed. Doing the code this way will also allow the
// proper AfterValueChange events to be fired, but remove extraneous beforeFocusLoss events
// See setAllowEditorRemoval for more details.
// Also, don't set the value to the existing value if they're the same, otherwise we'll get spurious
// AfterValueChange events. Note that the use of reference comparison rather than .equals is deliberate to
// ensure correct behaviour in cases where there are 2 elements in the list which are different but have the same content
if (oldVal != anObject) {
this.oldVal = oldVal;
boolean flagSet = false;
ArrayField t = ArrayFieldCellHelper.getArrayField(AutoResizingComboBox.this);
if (t!=null && this.isPopupVisible()) {
t.setAllowEditorRemoval(false);
flagSet = true;
}
super.setSelectedItem(anObject);
if (flagSet) {
t.setAllowEditorRemoval(true);
}
}
}
}
/**
* @return the readOnly
*/
public boolean isReadOnly() {
return this.readOnly;
}
/**
* @param pReadOnly the readOnly to set
*/
public void setReadOnly(boolean pReadOnly) {
this.readOnly = pReadOnly;
// CraigM:03/07/2008 - Enable the editor
Editable.set((JTextComponent)getEditor().getEditorComponent(), !readOnly);
}
public Object getPreviousValue() {
return oldVal;
}
private Dimension getSizeForModel() {
return sizeForModel;
}
private void setSizeForModel(Dimension d) {
sizeForModel = d;
}
/**
* This class allows the combobox to be resized based on the data in it.
*/
private class ComboBoxResizer implements ListDataListener {
public void contentsChanged(ListDataEvent e) {
resize();
}
public void intervalAdded(ListDataEvent e) {
resize();
}
public void intervalRemoved(ListDataEvent e) {
resize();
}
public void resize()
{
logger.debug("Data Changed. Checking whether we need to resize");
Dimension preferredDimension = getPreferredSize();
int currentHeight = getHeight();
// for some reason in swing, the preferred height and the actual height never
// seem to agree, but are one pixel out. If this is the case, we need to take
// the preferred height
int height;
if (preferredDimension.height == currentHeight - 1) {
height = preferredDimension.height;
}
else if (preferredDimension.height > currentHeight) {
height = preferredDimension.height;
}
else {
height = currentHeight;
}
Component but = getComponent(0);
int butWidth = but.getWidth();
FontMetrics fm = getFontMetrics(getFont());
double preferredWidth = preferredDimension.getWidth();
// The current minimum width is the lower bound for any new minimum width.
double minWidth = Math.max(butWidth + 15, AutoResizingComboBox.super.getMinimumSize().width);
int numItems = getModel().getSize();
boolean doResize = false;
DropListModel model = null;
if (AutoResizingComboBox.this.getModel() != null && AutoResizingComboBox.this.getModel() instanceof DropListModel) {
model = (DropListModel)AutoResizingComboBox.this.getModel();
}
if (model == null || WidthPolicy.get(AutoResizingComboBox.this) != Constants.SP_EXPLICIT) {
for (int i = 0; i < numItems; i++)
{
// Set the minimum and preferred sizes based on the largest element. Note that the minimum size
// is not always the same as the preferred size, as extra space could have been allocated to this
// combobox based on it's parentage, but this needs to be able to shrink back if needed.
Object element = getModel().getElementAt(i);
int minSize = SwingUtilities.computeStringWidth(fm, element.toString()) + butWidth + 10;
if (minSize > minWidth) {
minWidth = minSize;
}
if (minSize > preferredWidth)
{
preferredWidth = minSize;
logger.debug("Updating preferred width");
doResize = true;
}
}
}
else {
minWidth = preferredWidth;
}
logger.debug("Preferred Width [" + preferredWidth + "] Height [" + height + "]");
int newMinWidth = (int)minWidth;
int newPrefWidth = (int)preferredWidth;
Dimension d = getMinimumSize();
boolean sizeSet = false;
if (d.width != newMinWidth || d.height != height) {
setSizeForModel(new Dimension(newMinWidth, height));
sizeSet = true;
}
if (preferredDimension.width != newPrefWidth || preferredDimension.height != height) {
setPreferredSize(new Dimension(newPrefWidth, height));
sizeSet = true;
}
//if ((comboBox.getSelectedIndex() == -1) && (numItems > 0))
//comboBox.setSelectedIndex(0);
if (sizeSet || doResize) {
// TF:21/8/07:Changed to validate
if (getParent() != null) {
// Force this widget to be laid out again
getParent().invalidate();
getParent().validate();
}
}
}
}
public Array_Of_ListElement<ListElement> getElementList() {
// return ((DropListModel)getModel()).getElementList();
// CraigM:08/07/2008 - Use the pending action
return ElementList.get(this);
}
public int getIntegerValue() {
return ((DropListModel)getModel()).getIntegerValue();
}
public Object getObjectValue() {
return ((DropListModel)getModel()).getObjectValue();
}
public TextData getTextValue() {
return ((DropListModel)getModel()).getTextValue();
}
public void setElementList(final Array_Of_ListElement<ListElement> listElems) {
// CraigM:08/07/2008 - Use the pending action
ElementList.set(this, listElems);
}
public void setIntegerValue(int value) {
((DropListModel)getModel()).setIntegerValue(value);
}
public void setObjectValue(Object value) {
((DropListModel)getModel()).setObjectValue(value);
}
public void setTextValue(TextData value) {
((DropListModel)getModel()).setTextValue(value);
}
// TF:07/05/2009:Handled the scroll wheel of the mouse. (Thanks to DQ!)
protected class ComboBoxMouseWheelHandler implements MouseWheelListener {
public void mouseWheelMoved(MouseWheelEvent e) {
if (e.getComponent().isEnabled() && e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
boolean scrollDown = true;
if (e.getWheelRotation() < 0) {
scrollDown = false; // scroll up!
}
int selectedIndex = AutoResizingComboBox.this.getSelectedIndex();
// See if there's any value starting with what's in the field currently
Array_Of_ListElement<ListElement> elements = AutoResizingComboBox.this.getElementList();
int elementToSet = 0;
if (elements.size() > 0) {
// Find the first element in the list that matches the current value
// (case insensitive!) and then match the next one in the list
if (selectedIndex == -1) {
String currentText = editor.getItem().toString();
for (int i = 0; i < elements.size(); i++) {
if (((ListElement) elements.get(i)).getTextValue().compare(currentText.length(), currentText, true) == 0) {
elementToSet = i;
break;
}
}
}
else {
if (scrollDown){
elementToSet = selectedIndex + 1;
}
else {
elementToSet = selectedIndex - 1;
}
}
if (elementToSet >= elements.size()) {
elementToSet = elements.size()-1;
}
else if (elementToSet < 0) {
elementToSet = 0;
}
if (elementToSet < elements.size() && elementToSet != selectedIndex) {
// TF:9/10/07: If we're a child of an array field, calling setSelectedItem will cause us to stop
// editing. If we were to supress the actionListeners on this Combo box, we would not have committed
// the edit properly prior to firing the AfterValueChange events. Hence, we allow the table to
// leave editing mode, updating things properly, and then re-edit the same cell. We must be in a
// table as that's the premise of the outer IF test of this method.
ArrayField t = ArrayFieldCellHelper.getArrayField(AutoResizingComboBox.this);
// Disable the removal of the editor from the array, otherwise setSelectedItem will remove the editor
// See setAllowEditorRemoval for more details.
if (t != null) t.setAllowEditorRemoval(false);
setSelectedItem(elements.get(elementToSet));
if (t != null) t.setAllowEditorRemoval(true);
((JTextComponent) getEditor().getEditorComponent()).selectAll();
}
}
e.consume();
}
}
}
protected class ComboBoxKeyHandler extends KeyAdapter {
/*
* CraigM:25/09/2008 - Removed check for ArrayField as we always want to select the correct item (even if we arn't in an ArrayField)
*
* @see java.awt.event.KeyAdapter#keyPressed(java.awt.event.KeyEvent)
*/
@Override
public void keyPressed(KeyEvent e) {
int selectedIndex = AutoResizingComboBox.this.getSelectedIndex();
if (e.getKeyCode() == KeyEvent.VK_DOWN ||
e.getKeyCode() == KeyEvent.VK_UP ||
e.getKeyCode() == KeyEvent.VK_PAGE_DOWN ||
e.getKeyCode() == KeyEvent.VK_PAGE_UP) {
// See if there's any value starting with what's in the field currently
Array_Of_ListElement<ListElement> elements = AutoResizingComboBox.this.getElementList();
int elementToSet = 0;
if (elements.size() > 0) {
// Find the first element in the list that matches the current value
// (case insensitive!) and then match the next one in the list
if (selectedIndex == -1) {
// TF:07/05/2009:Moved this here for efficiency
String currentText = editor.getItem().toString();
for (int i = 0; i < elements.size(); i++) {
if (((ListElement)elements.get(i)).getTextValue().compare(currentText.length(), currentText, true) == 0) {
elementToSet = i;
break;
}
}
}
else {
switch (e.getKeyCode()) {
case KeyEvent.VK_DOWN: elementToSet = selectedIndex + 1; break;
case KeyEvent.VK_UP: elementToSet = selectedIndex - 1; break;
case KeyEvent.VK_PAGE_DOWN: elementToSet = selectedIndex + (AutoResizingComboBox.this.getMaximumRowCount()-1); break;
case KeyEvent.VK_PAGE_UP: elementToSet = selectedIndex - (AutoResizingComboBox.this.getMaximumRowCount()-1); break;
}
}
if (elementToSet >= elements.size()) {
elementToSet = elements.size() - 1;
}
else if (elementToSet < 0) {
elementToSet = 0;
}
if (elementToSet < elements.size() && elementToSet != selectedIndex) {
// TF:9/10/07: If we're a child of an array field, calling setSelectedItem will cause us to stop
// editing. If we were to supress the actionListeners on this Combo box, we would not have committed
// the edit properly prior to firing the AfterValueChange events. Hence, we allow the table to
// leave editing mode, updating things properly, and then re-edit the same cell. We must be in a
// table as that's the premise of the outer IF test of this method.
ArrayField t = ArrayFieldCellHelper.getArrayField(AutoResizingComboBox.this);
// Disable the removal of the editor from the array, otherwise setSelectedItem will remove the editor
// See setAllowEditorRemoval for more details.
if (t!=null) t.setAllowEditorRemoval(false);
setSelectedItem(elements.get(elementToSet));
if (t!=null) t.setAllowEditorRemoval(true);
((JTextComponent)getEditor().getEditorComponent()).selectAll();
}
}
AutoResizingComboBox.this.hidePopup();
e.consume();
}
}
}
/* (non-Javadoc)
* @see DisplayProject.ListField#setElementSelected(int, boolean)
* @author AD:26/5/2008
*/
public void setElementSelected(int index, boolean isSelected) {
if (isSelected) {
this.setSelectedIndex(index);
} else {
// Set no item to be selected
this.setSelectedIndex(-1);
}
}
/**
* Get the background colour. This obeys the forte-style rules for background colour:<p>
* <ul>
* <li>If an explicit background colour has been set, use that background colour</li>
* <li>Otherwise, if any of our parents have background colour set, use that colour</li>
* <li>Otherwise, use the default colour (white)
* </ul>
*/
@Override
public Color getBackground() {
if (!isBackgroundSet()) {
// We've been set to have a colour of inherit
Color parentColor = UIutils.getParentBackgroundColor(this);
if (parentColor != null) {
return parentColor;
}
else {
// There's no parent that's valid, put the default colour of white, but only if there's a border
return Color.white;
}
}
return super.getBackground();
}
/**
* Get the foreground colour. This obeys the forte-style rules for background colour:<p>
* <ul>
* <li>If an explicit foreground colour has been set, use that foreground colour</li>
* <li>Otherwise, if any of our parents have foreground colour set, use that colour</li>
* <li>Otherwise, use the default colour (white)
* </ul>
*/
@Override
public Color getForeground() {
if (!isForegroundSet()) {
// We've been set to have a colour of inherit
Container parent = this.getParent();
// Keep going up until we find a parent with the background set. We don't care about
// JScrollPanes or JViewports, because these should derive their colour from the control,
// not the other way around
while (parent != null && (!parent.isForegroundSet() || parent instanceof JScrollPane || parent instanceof JViewport)) {
parent = parent.getParent();
}
if (parent != null && !(parent instanceof Window)) {
return parent.getForeground();
}
else {
// There's no parent that's valid, return the default colour of black
return Color.black;
}
}
return super.getForeground();
}
}