Implement #4 Set ComboBox AutoReducible - Thanks to Yan and Derek

http://hg.idempiere.com/idempiere/issue/4/set-combobox-autoreducible
http://www.red1.org/adempiere/viewtopic.php?f=31&t=1203
This commit is contained in:
Carlos Ruiz 2011-05-04 21:48:04 -05:00
parent 98464c0934
commit 8d447eebe1
3 changed files with 653 additions and 5 deletions

View File

@ -39,7 +39,7 @@ public class VComboBox extends CComboBox
/**
*
*/
private static final long serialVersionUID = 2024662772161020317L;
private static final long serialVersionUID = 7632613004262943867L;
/**
* Constructor
@ -156,7 +156,7 @@ public class VComboBox extends CComboBox
*/
public String getDisplay()
{
if (getSelectedIndex() == -1)
if (getSelectedItem() == null)
return "";
//
NamePair p = (NamePair)getSelectedItem();
@ -164,5 +164,13 @@ public class VComboBox extends CComboBox
return "";
return p.getName();
} // getDisplay
@Override
protected boolean isMatchingFilter(Object element)
{
if (element instanceof NamePair)
element = ((NamePair)element).getName();
return super.isMatchingFilter(element);
}
} // VComboBox

View File

@ -274,7 +274,7 @@ public class VLookup extends JComponent
m_lookup.fillComboBox (isMandatory(), true, true, false);
m_combo.setModel(m_lookup);
//
AutoCompletion.enable(m_combo);
// AutoCompletion.enable(m_combo);
m_combo.addActionListener(this); // Selection
m_combo.getEditor().getEditorComponent().addMouseListener(mouseAdapter); // popup
// FocusListener to refresh selection before opening

View File

@ -17,16 +17,32 @@
package org.compiere.swing;
import java.awt.Color;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Vector;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.FocusManager;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JTextField;
import javax.swing.MutableComboBoxModel;
import javax.swing.SwingUtilities;
import javax.swing.event.EventListenerList;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.plaf.ComboBoxUI;
import javax.swing.text.JTextComponent;
import org.adempiere.plaf.AdempierePLAF;
import org.compiere.plaf.CompiereComboBoxUI;
@ -45,7 +61,7 @@ public class CComboBox extends JComboBox
/**
*
*/
private static final long serialVersionUID = 4605625077881909766L;
private static final long serialVersionUID = 5918151626085721856L;
/**
* Creates a <code>JComboBox</code> that takes it's items from an
@ -140,6 +156,21 @@ public class CComboBox extends JComboBox
/** Field Height */
public static int FIELD_HIGHT = 0;
/** Property key for auto-reduction. */
public static final String AUTO_REDUCIBLE_PROPERTY = "autoReducible";
/** Property key for case sensitive auto-reduction. */
public static final String CASE_SENSITIVE_PROPERTY = "caseSensitive";
/** View model for hiding showing only filtered data */
ReducibleModel m_reducibleModel;
/** Key listener for triggering an update the filtering model . */
private ReducibleKeyListener reducibleKeyListener = new ReducibleKeyListener();
/** Reference Field */
private static JTextField s_text = new JTextField(15);
/**
* Common Init
@ -147,6 +178,67 @@ public class CComboBox extends JComboBox
private void init()
{
FIELD_HIGHT = getPreferredSize().height;
setEditable(true);
setAutoReducible(true);
addMouseListener(new MouseAdapter()
{
public void mousePressed(MouseEvent me) {
if (SwingUtilities.isLeftMouseButton(me) && isAutoReducible())
updateReducibleModel(false);
}
});
// when auto-reducing, the focus listener will ensure all data choices
// are shown on initial focus, and that a valid selection is in place
// when focus is lost
final JTextComponent textComponent =
(JTextComponent)getEditor().getEditorComponent();
textComponent.addFocusListener(new FocusListener()
{
public void focusGained(FocusEvent fe)
{
if (isEditable())
textComponent.selectAll();
textComponent.repaint();
}
public void focusLost(FocusEvent fe)
{
if (isAutoReducible())
{
Object item = m_reducibleModel.getSelectedItem();
item = (item == null && m_reducibleModel.getSize() != 0) ?
m_reducibleModel.getElementAt(0) : item;
if (item == null)
{
updateReducibleModel(false);
if (m_reducibleModel.getSize() != 0)
item = m_reducibleModel.getElementAt(0);
else
return;
}
m_reducibleModel.setSelectedItem(item);
}
textComponent.setCaretPosition(0);
hidePopup();
textComponent.repaint();
}
});
textComponent.addMouseListener(new MouseAdapter()
{
public void mouseClicked(MouseEvent me) {
if (SwingUtilities.isLeftMouseButton(me) &&
isAutoReducible() &&
!isPopupVisible())
{
updateReducibleModel(false);
showPopup();
}
}
});
} // init
@ -166,6 +258,23 @@ public class CComboBox extends JComboBox
m_icon = defaultIcon;
} // setIcon
public ComboBoxModel getCompleteComboBoxModel()
{
return m_reducibleModel.getModel();
} // getCompleteComboBoxModel
/**
* @see javax.swing.JComboBox#setModel(javax.swing.ComboBoxModel)
*/
public void setModel(ComboBoxModel aModel)
{
m_reducibleModel = (m_reducibleModel == null) ? new ReducibleModel() : m_reducibleModel;
m_reducibleModel.setModel(aModel);
super.setModel(m_reducibleModel);
} // setModel
/**
* Set UI and re-set Icon for arrow button
* @param ui
@ -345,4 +454,535 @@ public class CComboBox extends JComboBox
setName(actionCommand);
} // setActionCommand
/**
* Called only when auto-reducing. By default, does a case insensitive
* string search for a match in the string representation of the given
* element.
*
* @param element an element in the combo box model
*
* @return true if the choice is to be displayed in the popup menu
*/
protected boolean isMatchingFilter(Object element)
{
String str = (element != null) ? element.toString().trim() : "";
str = isCaseSensitive() ? str : str.toLowerCase();
return str.indexOf(m_reducibleModel.getMatchingFilter()) > -1;
}
/**
* Is the combo box auto-reducible?
*
* @return true if isAutoReducible()
*/
public boolean isAutoReducible()
{
Boolean b = (Boolean)getClientProperty(AUTO_REDUCIBLE_PROPERTY);
return (b != null) && b.booleanValue();
}
/**
* Set whether the combo box is auto-reducible. The combo box must also be editable
* for auto-reduction to fully functional. Auto-reduction of data will preclude
* the ability for users to enter in their own choices.
*
* @param autoreducible true will activate auto-reduction of choices when user enters text
*/
public void setAutoReducible(boolean autoreducible)
{
if (isAutoReducible() != autoreducible)
{
putClientProperty(AUTO_REDUCIBLE_PROPERTY, Boolean.valueOf(autoreducible));
updateReducibleModel(false);
JTextComponent textComponent =
(JTextComponent)getEditor().getEditorComponent();
if (autoreducible)
textComponent.addKeyListener(reducibleKeyListener);
else
textComponent.removeKeyListener(reducibleKeyListener);
}
}
/**
* Is the auto-reduction case sensitive?
*
* @return true if case sensitive
*/
public boolean isCaseSensitive()
{
Boolean b = (Boolean)getClientProperty(CASE_SENSITIVE_PROPERTY);
return (b != null) && b.booleanValue();
}
/* (non-Javadoc)
* @see javax.swing.JComboBox#removeAllItems()
*/
public void removeAllItems()
{
m_reducibleModel.removeAllElements();
}
/**
* Set whether auto-reduction is case sensitive.
*
* @param caseSensitive true will make auto-reduction is case sensitive
*/
public void setCaseSensitive(boolean caseSensitive)
{
putClientProperty(CASE_SENSITIVE_PROPERTY, Boolean.valueOf(caseSensitive));
}
/**
* Updates the auto-reduction model.
*
* @param filtering true if the underlying data model should be filtered
*/
void updateReducibleModel(boolean filtering)
{
if (filtering ||
m_reducibleModel.getSize() != m_reducibleModel.getModel().getSize())
{
if (getParent() != null)
hidePopup();
// remember to caret position
JTextComponent textComponent =
(JTextComponent)getEditor().getEditorComponent();
int pos = textComponent.getCaretPosition();
m_reducibleModel.setFilter(textComponent.getText());
// update the model
m_reducibleModel.updateModel(filtering);
// reset the caret
textComponent.setText(m_reducibleModel.getFilter());
textComponent.setCaretPosition(pos);
// ensure the combo box is resized to match the popup, if necessary
if (getParent() != null)
{
getParent().validate();
getParent().repaint();
if (isShowing() && m_reducibleModel.getSize() > 0) {
// only show the popup if there is something to show
showPopup();
}
}
}
}
/**
* A view adapter model to hide filtered choices in the underlying combo box model.
*/
private class ReducibleModel implements MutableComboBoxModel, ListDataListener
{
/**
* Default constructor. Creates a ReducibleModel.
*/
public ReducibleModel()
{
}
/** The wrapped data model. */
private ComboBoxModel m_model;
/** The wrapped data model. */
private EventListenerList m_listenerList = new EventListenerList();
/** The filtered data. */
private ArrayList<Object> m_visibleData = new ArrayList<Object>();
/** The filtered data. */
private ArrayList<Object> m_modelData = new ArrayList<Object>();
/** The current filter. */
private String m_filter = "";
/** The cached filter for case insensitive filtering. */
private String m_lcFilter = "";
/**
* Pass through to the wrapped model if underlying model is MutableComboBoxModel.
*
* @see javax.swing.DefaultComboBoxModel#addElement(java.lang.Object)
*/
public void addElement(Object anObject)
{
checkMutableComboBoxModel();
m_modelData.add(anObject);
((MutableComboBoxModel)m_model).addElement(anObject);
}
/* (non-Javadoc)
* @see javax.swing.ListModel#addListDataListener(javax.swing.event.ListDataListener)
*/
public void addListDataListener(ListDataListener ldl)
{
m_listenerList.remove(ListDataListener.class, ldl);
m_listenerList.add(ListDataListener.class, ldl);
}
/**
* Checks that the <code>dataModel</code> is an instance of
* <code>MutableComboBoxModel</code>. If not, it throws an exception.
*
* @exception RuntimeException if <code>dataModel</code> is not an
* instance of <code>MutableComboBoxModel</code>.
*/
void checkMutableComboBoxModel()
{
if ( !(m_model instanceof MutableComboBoxModel) )
throw new RuntimeException("Cannot use this method with a non-Mutable data model.");
}
/* (non-Javadoc)
* @see javax.swing.event.ListDataListener#contentsChanged(javax.swing.event.ListDataEvent)
*/
public void contentsChanged(ListDataEvent lde)
{
updateDataModel();
updateModel(false);
if (isPopupVisible())
{
hidePopup();
showPopup();
}
}
/**
*
*/
private void fireContentsChanged()
{
ListDataEvent lde = null;
for (ListDataListener ldl : getListDataListeners())
{
lde = (lde == null) ?
new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, 0, getSize()) : lde;
ldl.contentsChanged(lde);
}
}
/* (non-Javadoc)
* @see javax.swing.ListModel#getElementAt(int)
*/
public Object getElementAt(int index)
{
return m_visibleData.get(index);
}
/**
* Return the current filter.
*
* @return the filter
*/
public String getFilter()
{
return m_filter;
}
/**
*
*/
public ListDataListener[] getListDataListeners()
{
return (ListDataListener[])m_listenerList.getListeners(ListDataListener.class);
}
/**
* @return the filter to use for matching; hecks case sensistivity
*/
protected String getMatchingFilter()
{
return isCaseSensitive() ? m_filter : m_lcFilter;
}
/**
* @return the wrapped model
*/
public ComboBoxModel getModel()
{
return m_model;
}
/**
* @return the selected item in the wrapped model
*
* @see javax.swing.DefaultComboBoxModel#getSelectedItem()
*/
public Object getSelectedItem()
{
return m_model.getSelectedItem();
}
/* (non-Javadoc)
* @see javax.swing.ListModel#getSize()
*/
public int getSize()
{
return m_visibleData.size();
}
/**
* Pass through to the wrapped model if underlying model is MutableComboBoxModel.
*
* @see javax.swing.DefaultComboBoxModel#insertElementAt(java.lang.Object, int)
*/
public void insertElementAt(Object anObject, int index)
{
checkMutableComboBoxModel();
m_modelData.add(index, anObject);
((MutableComboBoxModel)m_model).insertElementAt(anObject, index);
}
/**
* Pass through to the wrapped model if underlying model is MutableComboBoxModel.
*
* @see javax.swing.event.ListDataListener#intervalAdded(javax.swing.event.ListDataEvent)
*/
public void intervalAdded(ListDataEvent lde)
{
updateDataModel();
updateModel(false);
}
/**
* Pass through to the wrapped model if underlying model is MutableComboBoxModel.
*
* @see javax.swing.event.ListDataListener#intervalRemoved(javax.swing.event.ListDataEvent)
*/
public void intervalRemoved(ListDataEvent lde)
{
updateDataModel();
updateModel(false);
}
/**
*
*/
public void removeAllElements()
{
checkMutableComboBoxModel();
ListDataListener[] listeners = getListDataListeners();
for (int i = 0; i < listeners.length; i++)
removeListDataListener(listeners[i]);
m_model.removeListDataListener(this);
m_modelData.clear();
m_visibleData.clear();
while (m_model.getSize() > 0)
((MutableComboBoxModel)m_model).removeElementAt(0);
for (ListDataListener ldl : listeners)
addListDataListener(ldl);
m_model.addListDataListener(this);
updateModel(false);
}
/**
* Pass through to the wrapped model if underlying model is MutableComboBoxModel.
*
* @see javax.swing.DefaultComboBoxModel#removeElement(java.lang.Object)
*/
public void removeElement(Object anObject)
{
checkMutableComboBoxModel();
m_modelData.remove(anObject);
m_visibleData.clear();
((MutableComboBoxModel)m_model).removeElement(anObject);
}
/**
* Pass through to the wrapped model if underlying model is MutableComboBoxModel.
*
* @see javax.swing.DefaultComboBoxModel#removeElementAt(int)
*/
public void removeElementAt(int index)
{
checkMutableComboBoxModel();
m_modelData.remove(index);
m_visibleData.clear();
((MutableComboBoxModel)m_model).removeElementAt(index);
}
/* (non-Javadoc)
* @see javax.swing.ListModel#removeListDataListener(javax.swing.event.ListDataListener)
*/
public void removeListDataListener(ListDataListener ldl)
{
m_listenerList.remove(ListDataListener.class, ldl);
}
/**
* @param filter the filter to set
*/
public void setFilter(String filter)
{
this.m_filter = (filter != null) ? filter : "";
m_lcFilter = filter.trim().toLowerCase();
}
/**
* Set the wrapped combo box model.
*
* @param model the model to set
*/
public void setModel(ComboBoxModel model)
{
if (this.m_model != null)
this.m_model.removeListDataListener(this);
this.m_model = model;
updateDataModel();
m_filter = "";
model.addListDataListener(this);
updateModel(false);
}
/**
* Set the selected item in the wrapped model.
*
* @see javax.swing.DefaultComboBoxModel#setSelectedItem(java.lang.Object)
*/
public void setSelectedItem(Object anObject)
{
if (anObject == null || m_modelData.contains(anObject))
m_model.setSelectedItem(anObject);
}
/**
* Updates the view model based on whether filtering or not.
*
* @param filtering true if the underlying model is to be filtered
*/
public void updateDataModel()
{
m_modelData.clear();
int size = m_model.getSize();
for (int i = 0; i < size; i++)
m_modelData.add(m_model.getElementAt(i));
}
/**
* Updates the view model based on whether filtering or not.
*
* @param filtering true if the underlying model is to be filtered
*/
public void updateModel(boolean filtering)
{
boolean includeAll = !filtering || !isAutoReducible() || "".equals(m_lcFilter);
if (includeAll)
{
m_visibleData.clear();
m_visibleData.addAll(m_modelData);
}
else
{
m_visibleData.clear();
Object selected = getSelectedItem();
ListDataListener[] listeners = getListDataListeners();
for (int i = 0; i < listeners.length; i++)
removeListDataListener(listeners[i]);
m_model.removeListDataListener(this);
int size = m_model.getSize();
for (int i = 0; i < size; i++)
{
Object element = m_model.getElementAt(i);
if (element == null || isMatchingFilter(element))
{
m_visibleData.add(element);
}
}
if (m_visibleData.contains(selected) || selected == null)
setSelectedItem(selected);
for (ListDataListener ldl : listeners)
addListDataListener(ldl);
m_model.addListDataListener(this);
}
fireContentsChanged();
}
} // ReducibleModel
/**
* Key listener for editor's text compontent to trigger auto-reduction. Only
* used when auto-reduction is enabled.
*/
class ReducibleKeyListener extends KeyAdapter
{
/** Invokes autoreduction. */
private Runnable m_invoker = new Runnable()
{
public void run()
{
updateReducibleModel(true);
}
};
/** Visibly updates the popup menu. */
private Runnable m_updateMenu = new Runnable()
{
public void run()
{
hidePopup();
getParent().validate();
getParent().repaint();
showPopup();
}
};
/* (non-Javadoc)
* @see java.awt.event.KeyAdapter#keyPressed(java.awt.event.KeyEvent)
*/
public void keyPressed(KeyEvent ke)
{
if (ke.getKeyCode() != KeyEvent.VK_CONTROL &&
ke.getKeyCode() != KeyEvent.VK_ALT &&
ke.getKeyCode() != KeyEvent.VK_SHIFT &&
( ke.getModifiersEx() & InputEvent.ALT_DOWN_MASK ) == 0 )
{
if (ke.getKeyCode() == KeyEvent.VK_ENTER ||
ke.getKeyCode() == KeyEvent.VK_TAB)
{
// enter key pressed, so complete editing and select item
Object selObject = getSelectedItem();
selObject = (selObject == null && getItemCount() > 0) ? getItemAt(0) : selObject;
setSelectedItem(selObject);
getEditor().setItem(getSelectedItem());
}
else if (ke.getKeyCode() == KeyEvent.VK_ESCAPE)
{
// escape key ends editing and rejects focus of text editor
FocusManager.getCurrentManager().upFocusCycle();
}
else if (ke.getKeyCode() == KeyEvent.VK_UP ||
ke.getKeyCode() == KeyEvent.VK_KP_UP ||
ke.getKeyCode() == KeyEvent.VK_DOWN ||
ke.getKeyCode() == KeyEvent.VK_KP_DOWN)
{
// up or down selects new value
SwingUtilities.invokeLater(m_updateMenu);
}
else
{
// key typed, so filter
SwingUtilities.invokeLater(m_invoker);
setSelectedItem(null);
}
}
}
} // ReducibleKeyListener
} // CComboBox