At beginning I will say that I don't have in mind auto-complete combobox, but rather having a "setFilter(Set)" method in my combobox, so it displays what is in the set.
I was unable to achieve that effect, trying different approaches and I think it's the view responsibility to filter what it displays, so I should not extend ComboBoxModel.
This is what I have so far (main includes the case which doesn't work):
import java.awt.*;
import java.util.Set;
import javax.swing.*;
public class FilteredComboBox extends JComboBox {
private ComboBoxModel entireModel;
private final DefaultComboBoxModel filteredModel = new DefaultComboBoxModel();
private Set objectsToShow;
public FilteredComboBox(ComboBoxModel model) {
super(model);
this.entireModel = model;
}
public void setFilter(Set objectsToShow) {
if (objectsToShow != null) {
this.objectsToShow = objectsToShow;
filterModel();
} else {
removeFilter();
}
}
public void removeFilter() {
objectsToShow = null;
filteredModel.removeAllElements();
super.setModel(entireModel);
}
private void filterModel() {
filteredModel.removeAllElements();
for (int i = 0; i < entireModel.getSize(); ++i) {
Object element = entireModel.getElementAt(i);
addToFilteredModelIfShouldBeDisplayed(element);
}
super.setModel(filteredModel);
}
private void addToFilteredModelIfShouldBeDisplayed(Object element) {
if (objectsToShow.contains(element)) {
filteredModel.addElement(element);
}
}
@Override
public void setModel(ComboBoxModel model) {
entireModel = model;
super.setModel(entireModel);
if (objectsToShow != null) {
filterModel();
}
}
public static void main(String[] args) {
JFrame f = new JFrame();
f.setLayout(new BoxLayout(f.getContentPane(), BoxLayout.X_AXIS));
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
DefaultComboBoxModel model = new DefaultComboBoxModel();
FilteredComboBox cb = new FilteredComboBox(model);
cb.setPrototypeDisplayValue("XXXXXXXXXXXX");
f.add(cb);
f.pack();
Set objectsToShow = new HashSet();
objectsToShow.add("1");
objectsToShow.add("3");
objectsToShow.add("4");
cb.setFilter(objectsToShow); // if you set that filter after addElements it will work
model.addElement("1");
model.addElement("2");
model.addElement("3");
model.addElement("4");
model.addElement("5");
f.setVisible(true);
}
}
"I think it's the view responsibility to filter what it displays" - I'd argue that, the view displays what it's told, the model drives what it can show, but that's me...
This is an idea I wrote way back in Java 1.3 (with generic updates) which basically wraps a proxy
ComboBoxModel
around anotherComboBoxModel
. The proxy (orFilterableComboBoxModel
) then makes decisions about which elements from the original model match a filter and updates it's indices.Basically, all it does is generates an index map between itself and the original model, so it's not copying anything or generating new references to the original data.
The filtering is controlled via a "filterable" interface which simply passes the element to be checked and expects a
boolean
result in response. This makes the API highly flexible as filtering can be done any way you want without the need to change theFilterableComboBoxModel
in any way. It also means you can change the filter been used by simply applying a new one...If, like I usually do, you want to pass some value to the filter, you will need to inform the model that the filter has changed, via the
updateFilter
method...yeah, I know, aChangeListener
would probably be a better idea, but I was trying to keep it simple ;)For flexibility (and to maintain the current inheritance model), the core API is based on a
ListModel
, which means, you can also use the same concept with aJList
FilterableListModel
Filterable
FilterableComboBoxModel
It should be noted that it might actually be possible to use a
RowFilter
instead, but I've never really had the time to look at it (since I already had a working API)