I have following editor class, and I'm curious what's wrong with it. When running, it does correctly set the right radio button as selected. However, when flushing the top level editor, getValue is never called, and my object's property never get updated. Here's the code (hint - modified ValueListBox):
public class ValueRadioList<T> extends FlowPanel implements
HasConstrainedValue<T>, LeafValueEditor<T>, ValueChangeHandler<Boolean> {
private final List<T> values = new ArrayList<T>();
private final Map<Object, Integer> valueKeyToIndex =
new HashMap<Object, Integer>();
private final String name;
private final Renderer<T> renderer;
private final ProvidesKey<T> keyProvider;
private T value;
public ValueRadioList(Renderer<T> renderer) {
this(renderer, new SimpleKeyProvider<T>());
}
public ValueRadioList(Renderer<T> renderer, ProvidesKey<T> keyProvider) {
super();
this.name = DOM.createUniqueId();
this.keyProvider = keyProvider;
this.renderer = renderer;
}
private void addValue(T value) {
Object key = keyProvider.getKey(value);
if (valueKeyToIndex.containsKey(key)) {
throw new IllegalArgumentException("Duplicate value: " + value);
}
valueKeyToIndex.put(key, values.size());
values.add(value);
RadioButton radio = new RadioButton(name, renderer.render(value));
radio.addValueChangeHandler(this);
add(radio);
assert values.size() == getWidgetCount();
}
#Override public HandlerRegistration addValueChangeHandler(
ValueChangeHandler<T> handler) {
return addHandler(handler, ValueChangeEvent.getType());
}
#Override public T getValue() {
return value;
}
#Override public void onValueChange(ValueChangeEvent<Boolean> event) {
int selectedIndex = -1;
for (int i = 0, l = getWidgetCount(); i < l; i++) {
if (((RadioButton) getWidget(i)).getValue()) {
selectedIndex = i;
break;
}
}
if (selectedIndex < 0) {
return; // Not sure why this happens during addValue
}
T newValue = values.get(selectedIndex);
setValue(newValue, true);
}
#Override public void setAcceptableValues(Collection<T> newValues) {
values.clear();
valueKeyToIndex.clear();
clear();
for (T nextNewValue : newValues) {
addValue(nextNewValue);
}
updateRadioList();
}
#Override public void setValue(T value) {
setValue(value, false);
}
#Override public void setValue(T value, boolean fireEvents) {
if (value == this.value
|| (this.value != null && this.value.equals(value))) {
return;
}
T before = this.value;
this.value = value;
updateRadioList();
if (fireEvents) {
ValueChangeEvent.fireIfNotEqual(this, before, value);
}
}
private void updateRadioList() {
Object key = keyProvider.getKey(value);
Integer index = valueKeyToIndex.get(key);
if (index == null) {
addValue(value);
}
index = valueKeyToIndex.get(key);
((RadioButton) getWidget(index)).setValue(true);
}
}
Solved it, my POJO missed a setter for that field.
Related
I've got a listview where I can select different panes that will show up in the main pane. Now for a particular pane I need some objects to use every function. On the listview there is a listener on the selectedItemProperty. I want to check if the objects I need are available, otherwise the objects oldValue and newValue should keep their value. Is there a way of doing that?
listView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Object>()
{
#Override
public void changed(ObservableValue<? extends Object> observable, Object oldValue,
Object newValue)
{
if(obj == null)
{
// keep the oldValues for both
}
});
}
Here is a runnable example in the first code snippet the second snippet is what it would look like in your program
public class MainNoFXML extends Application {
public static void main(String[] args) { launch(args); }
#Override
public void start(Stage primaryStage) {
ListView<Object> listView = new ListView<>(FXCollections.observableArrayList(new String[]{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"}));
listView.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal)->{
if(newVal != null && (!newVal.equals("Monday") && !newVal.equals("Tuesday")))
Platform.runLater(() -> listView.getSelectionModel().select(oldVal));
});
Scene scene = new Scene(listView);
primaryStage.setScene(scene);
primaryStage.show();
}
}
Your Program(I think this is correct not exactly sure what your working with)
listView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Object>() {
#Override
public void changed(ObservableValue<? extends Object> observable, Object oldValue,
Object newValue)
{
if(obj == null)
{
Platform.runLater(() -> listView.getSelectionModel().select(oldValue));
// keep the oldValues for both
}
});
});
In this case you should use a custom selection model to prevent some items from being selected.
The following implementation is simplified. It doesn't handle changes of the item list. Furthermore it implements single selection only, but hopefully it shows the general approach:
public class CustomSelectionModel<T> extends MultipleSelectionModel<T> {
private final ListView<T> listView;
private final ObservableList<Integer> selectedIndices = FXCollections.observableArrayList();
private final ObservableList<Integer> selectedIndicesUnmodifiable = FXCollections.unmodifiableObservableList(selectedIndices);
private final ObservableList<T> selectedItems;
private BiPredicate<Integer, T> leavePredicate;
private BiPredicate<Integer, T> enterPredicate;
public BiPredicate<Integer, T> getLeavePredicate() {
return leavePredicate;
}
public void setLeavePredicate(BiPredicate<Integer, T> leavePredicate) {
this.leavePredicate = leavePredicate;
}
public BiPredicate<Integer, T> getEnterPredicate() {
return enterPredicate;
}
public void setEnterPredicate(BiPredicate<Integer, T> enterPredicate) {
this.enterPredicate = enterPredicate;
}
public CustomSelectionModel(ListView<T> listView) {
if (listView == null) {
throw new IllegalArgumentException();
}
this.listView = listView;
this.selectedItems = new TransformationList<T, Integer>(selectedIndices) {
#Override
protected void sourceChanged(ListChangeListener.Change<? extends Integer> c) {
beginChange();
while (c.next()) {
if (c.wasReplaced()) {
nextReplace(c.getFrom(), c.getTo(), c.getRemoved().stream().map(listView.getItems()::get).collect(Collectors.toList()));
} else if (c.wasRemoved()) {
nextRemove(c.getFrom(), c.getRemoved().stream().map(listView.getItems()::get).collect(Collectors.toList()));
} else if (c.wasAdded()) {
nextAdd(c.getFrom(), c.getTo());
}
}
endChange();
}
#Override
public int getSourceIndex(int index) {
return index;
}
#Override
public T get(int index) {
return listView.getItems().get(getSource().get(index));
}
#Override
public int size() {
return getSource().size();
}
};
}
#Override
public ObservableList<Integer> getSelectedIndices() {
return selectedIndicesUnmodifiable;
}
#Override
public ObservableList<T> getSelectedItems() {
return selectedItems;
}
#Override
public void selectIndices(int index, int... indices) {
clearAndSelect(index); // ignore all indices but the first
}
#Override
public void selectAll() {
selectFirst();
}
#Override
public void selectFirst() {
if (!listView.getItems().isEmpty()) {
clearAndSelect(0);
}
}
#Override
public void selectLast() {
if (!listView.getItems().isEmpty()) {
clearAndSelect(listView.getItems().size() - 1);
}
}
private void moveSelection(int oldIndex, int newIndex) {
if ((leavePredicate == null || leavePredicate.test(oldIndex, oldIndex == -1 ? null : listView.getItems().get(oldIndex)))
&& (enterPredicate == null || enterPredicate.test(newIndex, newIndex == -1 ? null : listView.getItems().get(newIndex)))) {
setSelectedIndex(newIndex);
if (newIndex == -1) {
selectedItems.clear();
setSelectedItem(null);
} else {
setSelectedItem(listView.getItems().get(newIndex));
if (isEmpty()) {
selectedIndices.add(newIndex);
} else {
selectedIndices.set(0, newIndex);
}
}
listView.getFocusModel().focus(newIndex);
}
}
#Override
public void clearAndSelect(int index) {
moveSelection(getSelectedIndex(), index);
}
#Override
public void select(int index) {
clearAndSelect(index);
}
#Override
public void select(T obj) {
int index = listView.getItems().indexOf(obj);
if (index >= 0) {
clearAndSelect(index);
}
}
#Override
public void clearSelection(int index) {
if (getSelectedIndex() == index) {
clearSelection();
}
}
#Override
public void clearSelection() {
moveSelection(getSelectedIndex(), -1);
}
#Override
public boolean isSelected(int index) {
return selectedIndices.contains(index);
}
#Override
public boolean isEmpty() {
return selectedIndices.isEmpty();
}
#Override
public void selectPrevious() {
int index = getSelectedIndex() - 1;
if (index >= 0) {
clearAndSelect(index);
}
}
#Override
public void selectNext() {
int index = getSelectedIndex() + 1;
if (index < listView.getItems().size()) {
clearAndSelect(index);
}
}
}
#Override
public void start(Stage primaryStage) throws Exception {
ListView<Integer> listView = new ListView<>();
for (int i = 0; i < 100; i++) {
listView.getItems().add(i);
}
CustomSelectionModel<Integer> selectionModel = new CustomSelectionModel<>(listView);
listView.setSelectionModel(selectionModel);
CheckBox moveAllowed = new CheckBox("disallow movement");
moveAllowed.selectedProperty().addListener((o, oldValue, newValue) -> selectionModel.setLeavePredicate(newValue ? (index, item) -> false : null));
CheckBox enterOdd = new CheckBox("enter odd items disallowed");
enterOdd.selectedProperty().addListener((o, oldValue, newValue) -> selectionModel.setEnterPredicate(newValue ? (index, item) -> (index & 1) == 0 : null));
Scene scene = new Scene(new ScrollPane(new HBox(listView, new VBox(moveAllowed, enterOdd))));
primaryStage.setScene(scene);
primaryStage.show();
}
I am trying allready to make custome model for JSpinner but it doesnt work.
the code looks like:
public class ModelJSpinner implements SpinnerModel
{
private long value;
private long min;
private long max;
private long increment;
private ChangeListener l;
private ArrayList<ChangeListener> listeners;
#Override
public Object getValue()
{
return null;
}
public ModelJSpinner(long min, long max, long increment)
{
super();
this.min = min;
this.max = max;
this.increment = increment;
setValue(min);
listeners = new ArrayList<>();
}
#Override
public void setValue(Object value)
{
if (value == null)
{
}else {
this.value = (Long) value;
}
//fireStateChanged();
}
private void fireStateChanged()
{
if (listeners == null)
return;
for (int a = 0; a < listeners.size(); a++)
{
ChangeListener l = (ChangeListener) listeners.get(a);
try
{
l.stateChanged(new ChangeEvent(this));
}
catch (RuntimeException e)
{
e.printStackTrace();
}
}
}
#Override
public Object getNextValue()
{
Long nextValue = value + increment;
if (nextValue > max)
{
return null;
}
else
{
return nextValue;
}
}
#Override
public Object getPreviousValue()
{
Long previousValue = value - increment;
if (previousValue < min)
{
return null;
}
else
{
return previousValue;
}
}
#Override
public void addChangeListener(javax.swing.event.ChangeListener l)
{
this.l = l;
listeners.add(l);
}
#Override
public void removeChangeListener(javax.swing.event.ChangeListener l)
{
if (this.l == l)
{
l = null;
}
listeners.add(l);
}
}
However when i run the following code i get...nothing much except JSpinner that doesnt do much...
public class Test
{
public static void main(String[] args)
{
ModelJSpinner model = new ModelJSpinner(10L, 20L, 5L);
JSpinner spinner = new JSpinner(model);
spinner.setModel(model);
spinner.setValue(15L);
JFrame frame = new JFrame("adasasd");
frame.setSize(350, 150);
frame.add(spinner);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
}
At the beginning, all i wanted was just to see that JSpinner can do something with above mentioned Model and later i wanted to implement Changelisteners.
As of now, i am not even able to have it drawn properly. Is there a chance that somebody could help me?
I need to use instance of Class implementing SpinnerModel as model for JSpinner and i just cannot make it work.
BR
DK
This is a guarantee to fail:
#Override
public Object getValue()
{
return null;
}
since this is the method that the JSpinner uses to determine what value to display.
Instead have this return the value held by your value field. Also, don't return null for the getNextValue() if next value is above max. Instead return the max. Similarly for the getPreviousValue(), return the min value if the calculated previous value is less than min.
For example,
public class SpinnerModel3 implements SpinnerModel {
private long value;
private long min;
private long max;
private long increment;
// using a set to avoid allowing addition of duplicate listeners
private Set<ChangeListener> listenerSet = new HashSet<>();
public SpinnerModel3(long value, long min, long max, long increment) {
super();
this.value = value;
this.min = min;
this.max = max;
this.increment = increment;
}
#Override
public void addChangeListener(ChangeListener l) {
listenerSet.add(l);
}
#Override
public Object getNextValue() {
long nextValue = value + increment;
nextValue = Math.min(nextValue, max);
return nextValue;
}
#Override
public Object getPreviousValue() {
long prevValue = value - increment;
prevValue = Math.max(prevValue, min);
return prevValue;
}
#Override
public Object getValue() {
return value;
}
#Override
public void removeChangeListener(ChangeListener l) {
listenerSet.remove(l);
}
#Override
public void setValue(Object value) {
this.value = (long) value;
fireStateChanged();
}
protected void fireStateChanged() {
// create a ChangeEvent object
ChangeEvent e = new ChangeEvent(this);
for (ChangeListener l : listenerSet) {
l.stateChanged(e); // notify all listeners
}
}
}
Note that it is usually better to use the extend the abstract model class if one is available (or even better, the default model class, but none is available for spinner model). So better still:
#SuppressWarnings("serial")
public class SpinnerModel2 extends AbstractSpinnerModel {
private long value;
private long min;
private long max;
private long increment;
public SpinnerModel2(long value, long min, long max, long increment) {
super();
this.value = value;
this.min = min;
this.max = max;
this.increment = increment;
}
#Override
public Object getNextValue() {
long nextValue = value + increment;
nextValue = Math.min(nextValue, max);
return nextValue;
}
#Override
public Object getPreviousValue() {
long prevValue = value - increment;
prevValue = Math.max(prevValue, min);
return prevValue;
}
#Override
public Object getValue() {
return value;
}
#Override
public void setValue(Object value) {
this.value = (long) value;
fireStateChanged();
}
}
I have create my own SpreadsheetCellEditor to show a ColorPicker, but I don't want to return back the ColorPicker.getValue().toString, I want to return a label with
Background color of selected value. I have searched for setContentDisplay(ContentDisplay.GRAPHIC_ONLY) but it seems not exist on SpreadsheetCell.
So how I can achieve this?
Here is my implementation so far,
public class comboboxCellEditor extends SpreadsheetCellEditor {
private final ColorPicker colorPicker = new ColorPicker();
private EventHandler<KeyEvent> eh;
private ChangeListener<Color> cl;
private boolean ending = false;
public comboboxCellEditor(SpreadsheetView view) {
super(view);
}
#Override
public void startEdit(Object value) {
if (value instanceof Color) {
this.colorPicker.setValue((Color) value);
}
attachEnterEscapeEventHandler();
this.colorPicker.show();
this.colorPicker.requestFocus();
}
#Override
public Control getEditor() {
return colorPicker;
}
public String getControlValue() {
return this.colorPicker.getValue().toString();
}
#Override
public void end() {
if (this.colorPicker.isShowing()) {
this.colorPicker.hide();
}
this.colorPicker.removeEventFilter(KeyEvent.KEY_PRESSED, this.eh);
this.colorPicker.valueProperty().removeListener(this.cl);
}
private void attachEnterEscapeEventHandler() {
this.eh =
new EventHandler<KeyEvent>() {
public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
comboboxCellEditor.this.ending = true;
comboboxCellEditor.this.endEdit(true);
comboboxCellEditor.this.ending = false;
} else if (t.getCode() == KeyCode.ESCAPE) {
comboboxCellEditor.this.endEdit(false);
}
}
};
this.colorPicker.addEventFilter(KeyEvent.KEY_PRESSED, this.eh);
this.cl = new ChangeListener<Color>() {
public void changed(ObservableValue<? extends Color> observable, Color oldValue, Color newValue) {
if (!comboboxCellEditor.this.ending)
comboboxCellEditor.this.endEdit(true);
}
};
this.colorPicker.valueProperty().addListener(this.cl);
}
}
public class SpreadSheetComboboxCellType extends SpreadsheetCellType<Color> {
#Override
public SpreadsheetCellEditor createEditor(SpreadsheetView spreadsheetView) {
return new comboboxCellEditor(spreadsheetView);
}
#Override
public String toString(Color color) {
return color.toString();
}
#Override
public boolean match(Object o) {
return true;
}
#Override
public Color convertValue(Object o) {
if (o instanceof Color)
return (Color) o;
else {
return Color.valueOf((String) o);
}
}
public SpreadsheetCell createCell(int row, int column, int rowSpan, int columnSpan, Color value) {
SpreadsheetCellBase cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
cell.setItem(value);
Label label = new Label();
label.setGraphic(null);
label.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
cell.setGraphic(label);
return cell;
}
}
The SpreadsheetCell has an updateText method each time you modify the item. Therefore it's meant to display text first.
You have several options.
The easiest one is to add a listener to the cell and modify your graphic at that moment :
The text shown in the cell will be the result of the conversion of your SpreadsheetCellType. Since you don't want text, return nothing in your SpreadSheetComboboxCellType :
#Override
public String toString(Color color) {
return "";
}
Then simply add a Listener on your SpreadsheetCell and modify the graphic :
cell.itemProperty().addListener(new ChangeListener<Object>() {
#Override
public void changed(ObservableValue<? extends Object> ov, Object t, Object newValue) {
if (newValue != null && newValue instanceof Color ) {
//Do something
cell.setGraphic(My_Graphic_Object);
} else {
cell.setGraphic(null);
}
}
});
This should do the trick. Now you can implement your own SpreadsheetCell in order to have a total control on the display. It will be more efficient. Basically copy all the code for SpreadsheetCellBase (here : https://bitbucket.org/controlsfx/controlsfx/src/755c59605e623476ff0a6c860e2c218488776aec/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCellBase.java?at=default&fileviewer=file-view-default )
And simply modify the updateText method :
private void updateText() {
if(getItem() == null){
text.setValue(""); //$NON-NLS-1$
}else if (!("").equals(getFormat())) { //$NON-NLS-1$
text.setValue(type.toString(getItem(), getFormat()));
} else {
text.setValue(type.toString(getItem()));
}
}
Instead of modifying the text, you would be modifying the graphic with the new color.
I tried to implement my own JSpinner model to accept an enumeration (including I18N), so I did like that:
searchSpinner.setModel(new AbstractSpinnerModel() {
int index = 0;
int minIndex = 0;
int maxIndex = MY_ENUM.values().length - 1;
Object selected = MY_ENUM.values()[index];
#Override
public Object getValue() {
return selected;
}
#Override
public void setValue(Object value) {
selected = value;
fireStateChanged();
}
#Override
public Object getNextValue() {
if (index < maxIndex) {
index++;
}
fireStateChanged();
return MY_ENUM.values()[index];
}
#Override
public Object getPreviousValue() {
if (index > minIndex) {
index--;
}
fireStateChanged();
return MY_ENUM.values()[index];
}
#Override
public void addChangeListener(ChangeListener l) {
}
#Override
public void removeChangeListener(ChangeListener l) {
}
});
The problem is that did not work, and even the spinner list looks like disabled. What am I doing wrong?
UPDATE: Based on first answer
You should extend from AbstractSpinnerModel (note to folks new to his question -- note that his original question had the class implementing the SpinnerModel interface. He later changed his code to reflect my recommendation) and be sure to call the fireStateChanged() method when appropriately. Also you've not taken into account edge cases and beyond edge cases.
e.g.,
import javax.swing.*;
import javax.swing.JSpinner.DefaultEditor;
public class MySpinnerPanel extends JPanel {
public static void main(String[] args) {
JSpinner spinner = new JSpinner(new MyEnumSpinnerModel());
JSpinner.DefaultEditor editor = (DefaultEditor) spinner.getEditor();
editor.getTextField().setColumns(5);
JPanel panel = new JPanel();
panel.add(spinner);
JOptionPane.showMessageDialog(null, panel);
}
}
enum MyEnum {
FE, FI, FO, FUM, FOO, FUBAR, SPAM
}
class MyEnumSpinnerModel extends AbstractSpinnerModel {
private int index = 0;
#Override
public Object getValue() {
return MyEnum.values()[index];
}
#Override
public void setValue(Object value) {
if (value instanceof MyEnum) {
index = ((MyEnum) value).ordinal();
fireStateChanged();
} else {
String text = value.toString() + " is not a valid enum item";
throw new IllegalArgumentException(text);
}
}
#Override
public Object getNextValue() {
if (index >= MyEnum.values().length - 1) {
return null;
} else {
return MyEnum.values()[index + 1];
}
}
#Override
public Object getPreviousValue() {
if (index <= 0) {
return null;
} else {
return MyEnum.values()[index - 1 ];
}
}
}
Edit
Note that the model itself should not require a listener to notify the view (as per the other answer to this question) as that's what the AbstractSpinnerModel does internally. It's fireStateChange() method is what the model itself should call to trigger this notification, same as most all other similar model structures in Swing such as any TableModel object that you create that derives from the AbstractTableModel. For details, please see the source code for the SpinnerListModel. Your code should emulate this class.
You should use ChangeListener to notify the view of changes in the model.
spinner = new JSpinner(new SpinnerModel() {
private ChangeListener l;
#Override
public void setValue(Object value) {
...
if(l != null) {
l.stateChanged(new ChangeEvent(this));
}
}
...
#Override
public void addChangeListener(ChangeListener l) {
this.l = l;
}
#Override
public void removeChangeListener(ChangeListener l) {
if(this.l == l) {
this.l = null;
}
}
});
Edit: You can use List to register many listeners.
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 another ComboBoxModel. The proxy (or FilterableComboBoxModel) 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 the FilterableComboBoxModel 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, a ChangeListener 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 a JList
FilterableListModel
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractListModel;
import javax.swing.ListModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
public class FilterableListModel<E> extends AbstractListModel<E> implements ListDataListener {
private ListModel<E> peer;
private List<Integer> lstFilteredIndicies;
private IFilterable filter;
public FilterableListModel() {
lstFilteredIndicies = new ArrayList<Integer>(25);
}
public FilterableListModel(ListModel<E> model) {
this();
setModel(model);
}
public FilterableListModel(ListModel<E> model, IFilterable filter) {
this();
setModel(model);
setFilter(filter);
}
public void setModel(ListModel<E> parent) {
if (peer == null || !peer.equals(parent)) {
if (peer != null) {
fireIntervalRemoved(this, 0, peer.getSize() - 1);
peer.removeListDataListener(this);
}
peer = parent;
lstFilteredIndicies.clear();
if (peer != null) {
peer.addListDataListener(this);
}
filterModel(true);
}
}
public ListModel<E> getModel() {
return peer;
}
#Override
public int getSize() {
IFilterable filter = getFilter();
return filter == null ? getModel() == null ? 0 : getModel().getSize() : lstFilteredIndicies.size();
}
#Override
public E getElementAt(int index) {
IFilterable filter = getFilter();
ListModel<E> model = getModel();
E value = null;
if (filter == null) {
if (model != null) {
value = model.getElementAt(index);
}
} else {
int filterIndex = lstFilteredIndicies.get(index);
value = model.getElementAt(filterIndex);
}
return value;
}
public int indexOf(Object value) {
int index = -1;
for (int loop = 0; loop < getSize(); loop++) {
Object at = getElementAt(loop);
if (at == value) {
index = loop;
break;
}
}
return index;
}
#Override
public void intervalAdded(ListDataEvent e) {
IFilterable filter = getFilter();
ListModel model = getModel();
if (model != null) {
if (filter != null) {
int startIndex = Math.min(e.getIndex0(), e.getIndex1());
int endIndex = Math.max(e.getIndex0(), e.getIndex1());
for (int index = startIndex; index <= endIndex; index++) {
Object value = model.getElementAt(index);
if (filter.include(value)) {
lstFilteredIndicies.add(index);
int modelIndex = lstFilteredIndicies.indexOf(index);
fireIntervalAdded(this, modelIndex, modelIndex);
}
}
} else {
fireIntervalAdded(this, e.getIndex0(), e.getIndex1());
}
}
}
#Override
public void intervalRemoved(ListDataEvent e) {
IFilterable filter = getFilter();
ListModel model = getModel();
if (model != null) {
if (filter != null) {
int oldRange = lstFilteredIndicies.size();
filterModel(false);
fireIntervalRemoved(this, 0, oldRange);
if (lstFilteredIndicies.size() > 0) {
fireIntervalAdded(this, 0, lstFilteredIndicies.size());
}
} else {
fireIntervalRemoved(this, e.getIndex0(), e.getIndex1());
}
}
}
#Override
public void contentsChanged(ListDataEvent e) {
filterModel(true);
}
public void setFilter(IFilterable<E> value) {
if (filter == null || !filter.equals(value)) {
filter = value;
if (getModel() != null) {
if (getModel().getSize() > 0) {
fireIntervalRemoved(this, 0, getModel().getSize() - 1);
}
}
filterModel(true);
}
}
public IFilterable<E> getFilter() {
return filter;
}
protected void filterModel(boolean fireEvent) {
if (getSize() > 0 && fireEvent) {
fireIntervalRemoved(this, 0, getSize() - 1);
}
lstFilteredIndicies.clear();
IFilterable<E> filter = getFilter();
ListModel<E> model = getModel();
if (filter != null && model != null) {
for (int index = 0; index < model.getSize(); index++) {
E value = model.getElementAt(index);
if (filter.include(value)) {
lstFilteredIndicies.add(index);
if (fireEvent) {
fireIntervalAdded(this, getSize() - 1, getSize() - 1);
}
}
}
}
}
public void updateFilter() {
IFilterable filter = getFilter();
ListModel model = getModel();
if (filter != null && model != null) {
for (int index = 0; index < model.getSize(); index++) {
Object value = model.getElementAt(index);
if (filter.include(value)) {
if (!lstFilteredIndicies.contains(index)) {
lstFilteredIndicies.add(index);
fireIntervalAdded(this, getSize() - 1, getSize() - 1);
}
} else if (lstFilteredIndicies.contains(index)) {
int oldIndex = lstFilteredIndicies.indexOf(index);
lstFilteredIndicies.remove(oldIndex);
fireIntervalRemoved(this, oldIndex, oldIndex);
}
}
}
}
}
Filterable
public interface IFilterable<O> {
public boolean include(O value);
}
FilterableComboBoxModel
import javax.swing.ComboBoxModel;
public class FilterableComboBoxModel<E> extends FilterableListModel<E> implements ComboBoxModel<E> {
private FilterableComboBoxModel(ComboBoxModel<E> model) {
super(model);
}
public ComboBoxModel<E> getComboBoxModel() {
return (ComboBoxModel) getModel();
}
#Override
public void setSelectedItem(Object anItem) {
getComboBoxModel().setSelectedItem(anItem);
}
#Override
public Object getSelectedItem() {
return getComboBoxModel().getSelectedItem();
}
}
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)