From 8d447eebe1e16c5f22586c4cf7942f6a32493e27 Mon Sep 17 00:00:00 2001 From: Carlos Ruiz Date: Wed, 4 May 2011 21:48:04 -0500 Subject: [PATCH] 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 --- .../src/org/compiere/grid/ed/VComboBox.java | 14 +- .../src/org/compiere/grid/ed/VLookup.java | 2 +- .../src/org/compiere/swing/CComboBox.java | 642 +++++++++++++++++- 3 files changed, 653 insertions(+), 5 deletions(-) diff --git a/org.adempiere.ui.swing/src/org/compiere/grid/ed/VComboBox.java b/org.adempiere.ui.swing/src/org/compiere/grid/ed/VComboBox.java index cb2042ccfd..dda11f0f62 100644 --- a/org.adempiere.ui.swing/src/org/compiere/grid/ed/VComboBox.java +++ b/org.adempiere.ui.swing/src/org/compiere/grid/ed/VComboBox.java @@ -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 diff --git a/org.adempiere.ui.swing/src/org/compiere/grid/ed/VLookup.java b/org.adempiere.ui.swing/src/org/compiere/grid/ed/VLookup.java index a8f580be81..d9ba2defbb 100644 --- a/org.adempiere.ui.swing/src/org/compiere/grid/ed/VLookup.java +++ b/org.adempiere.ui.swing/src/org/compiere/grid/ed/VLookup.java @@ -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 diff --git a/org.adempiere.ui.swing/src/org/compiere/swing/CComboBox.java b/org.adempiere.ui.swing/src/org/compiere/swing/CComboBox.java index f26a103959..c9f8e58ab2 100644 --- a/org.adempiere.ui.swing/src/org/compiere/swing/CComboBox.java +++ b/org.adempiere.ui.swing/src/org/compiere/swing/CComboBox.java @@ -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 JComboBox 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 m_visibleData = new ArrayList(); + + /** The filtered data. */ + private ArrayList m_modelData = new ArrayList(); + + /** 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 dataModel is an instance of + * MutableComboBoxModel. If not, it throws an exception. + * + * @exception RuntimeException if dataModel is not an + * instance of MutableComboBoxModel. + */ + 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