/*******************************************************************************
* Breakout Cave Survey Visualizer
*
* Copyright (C) 2014 James Edwards
*
* jedwards8 at fastmail dot fm
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*******************************************************************************/
package org.andork.swing.selector;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.swing.JComboBox;
import org.andork.util.Java7;
/**
* An {@link ISelector} controlled by a {@link JComboBox}. When the user selects
* a different item in the combo box, {@link ISelectorListener}s will be
* notified. When the program sets the selection, the combo box will also be
* updated to that selection.<br>
* <br>
* If there are no available values in this selector, the
* {@link #setNothingAvailableItem(Object) nothingAvailableItem} will be
* temporarily displayed in the combo box if it is not null. <br>
* If there are values available, but the selection is null, the
* {@link #setNullItem(Object) nullItem} will be temporarily displayed if it is
* not null.<br>
* If the selection is not null, but not one of the available values, the
* {@link #setSelectionNotAvailableItem(Object) selectionNotAvailableItem} will
* be temporarily displayed if it is not null.<br>
* These items will not not affect the {@link #getSelection() selection} or the
* {@link #setAvailableValues(List) available values}.<br>
* <br>
* Also, whenever only one value is available, the {@link #comboBox()} will be
* disabled if {@code disableWhenOnlyOneAvailableItem} is
* {@link #setDisableWhenOnlyOneAvailableItem(boolean) set} to {@code true}.
*
* @author james.a.edwards
* @param <T>
* the selection type.
*/
public class DefaultSelector<T> implements ISelector<T> {
private boolean disableListeners;
private Object nullItem;
private Object nothingAvailableItem;
private Object selectionNotAvailableItem;
private JComboBox comboBox;
private final List<ISelectorListener<T>> listeners = new ArrayList<ISelectorListener<T>>();
private final List<T> availableValues = new ArrayList<T>();
private T selection;
private boolean allowSelectionNotAvailable = false;
private boolean enabled = true;
private boolean disableWhenOnlyOneAvailableItem = false;
public DefaultSelector() {
this(new JComboBox());
}
public DefaultSelector(JComboBox comboBox) {
this.comboBox = comboBox;
init();
}
public void addAvailableValue(int index, T value) {
availableValues.add(index, value);
updateComboBoxAvailableItems();
}
public void addAvailableValue(T value) {
availableValues.add(value);
updateComboBoxAvailableItems();
}
@Override
public void addSelectorListener(ISelectorListener<T> listener) {
if (!listeners.contains(listener)) {
listeners.add(listener);
}
}
/**
* Gets the {@link JComboBox} controlled by this {@code DefaultSelector}.
* You should not need to listen directly for ItemEvents or other user input
* from the combo box; use an {@link ISelectorListener} or this instead.
* Only use this method to put the combo box into a layout.
*
* @return the combo box controlled by this {@code DefaultSelector}.
*/
public JComboBox comboBox() {
return comboBox;
}
/**
* Gets the list of values available for user selection.
*/
public List<T> getAvailableValues() {
return Collections.unmodifiableList(availableValues);
}
/**
* Gets the item displayed in the {@link #comboBox()} when no values are
* {@link #getAvailableValues() available}.
*/
public Object getNothingAvailableItem() {
return nothingAvailableItem;
}
/**
* Gets the item displayed in the {@link #comboBox()} when the selection is
* {@link #setSelection(Object) set} to {@code null}.
*/
public Object getNullItem() {
return nullItem;
}
/**
* @return the currently selected value.
*/
@Override
public T getSelection() {
return selection;
}
/**
* Gets the item displayed in the {@link #comboBox()} when the selection is
* {@link #setSelection(Object) set} to a value not contained in the list of
* {@link #getAvailableValues() available} values. However, if
* {@link #setAllowSelectionNotAvailable(boolean)} to {@code true}, this
* item will not be used.
*/
public Object getSelectionNotAvailableItem() {
return selectionNotAvailableItem;
}
private void init() {
comboBox.setModel(new BetterComboBoxModel());
comboBox.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if (!disableListeners && e.getStateChange() == ItemEvent.SELECTED) {
if (!isTransientItem(e.getItem()) && availableValues.contains(e.getItem())) {
setSelection((T) e.getItem());
}
}
}
});
updateComboBoxAvailableItems();
}
/**
* Whether this {@link DefaultSelector} currently "allows" the
* {@link #getSelection() selection} to be a value not contained in the list
* of {@link #getAvailableValues() available} values. If allowed, such a
* value will be displayed in the {@link #comboBox()} as normal. Otherwise,
* the {@link #getSelectionNotAvailableItem()} will be displayed.
*/
public boolean isAllowSelectionNotAvailable() {
return allowSelectionNotAvailable;
}
public boolean isDisableWhenOnlyOneAvailableItem() {
return disableWhenOnlyOneAvailableItem;
}
/**
* @return whether this {@link DefaultSelector} is enabled. Even if it is
* enabled, the {@link #comboBox()} will be automatically disabled
* if there is only one available item and
* {@code disableWhenOnlyOneAvailableItem} is
* {@link #setDisableWhenOnlyOneAvailableItem(boolean) set} to
* {@code true}.
*/
public boolean isEnabled() {
return enabled;
}
protected boolean isTransientItem(Object o) {
return o != null && (o == nullItem || o == nothingAvailableItem || o == selectionNotAvailableItem ||
selection == o && allowSelectionNotAvailable && !availableValues.contains(selection));
}
protected void notifySelectionChanged(T oldSelection, T newSelection) {
for (ISelectorListener<T> listener : listeners) {
try {
listener.selectionChanged(this, oldSelection, newSelection);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
protected Object pickItemToSelect() {
Object newSelectedItem = null;
if (allowSelectionNotAvailable && selection != null && !availableValues.contains(selection)) {
newSelectedItem = selection;
} else if (availableValues.isEmpty()) {
newSelectedItem = nothingAvailableItem;
} else if (selection == null) {
newSelectedItem = nullItem;
} else if (!availableValues.contains(selection)) {
newSelectedItem = selectionNotAvailableItem;
} else {
newSelectedItem = selection;
if (nullItem != null) {
comboBox.removeItem(nullItem);
}
if (selectionNotAvailableItem != null) {
comboBox.removeItem(selectionNotAvailableItem);
}
if (nothingAvailableItem != null) {
comboBox.removeItem(nothingAvailableItem);
}
}
return newSelectedItem;
}
public T removeAvailableValue(int index) {
T result = availableValues.remove(index);
updateComboBoxAvailableItems();
return result;
}
@Override
public void removeSelectorListener(ISelectorListener<T> listener) {
listeners.remove(listener);
}
/**
* Sets whether to "allow" the {@link #getSelection() selection} to be a
* value not contained in the list of {@link #getAvailableValues()
* available} values. If allowed, such a value will be displayed in the
* {@link #comboBox()} as normal. Otherwise, the
* {@link #getSelectionNotAvailableItem()} will be displayed.
*/
public void setAllowSelectionNotAvailable(boolean useSelectionNotAvailableItem) {
if (this.allowSelectionNotAvailable != useSelectionNotAvailableItem) {
this.allowSelectionNotAvailable = useSelectionNotAvailableItem;
updateComboBoxSelectedItem();
}
}
public void setAvailableValue(int index, T value) {
availableValues.set(index, value);
updateComboBoxAvailableItems();
}
/**
* Sets the list of values available for user selection. If the selected
* value is not in the list, the selection will be cleared.
*
* @param newAvailableValues
* the new list of available values.
*/
public void setAvailableValues(Collection<? extends T> newAvailableValues) {
if (!availableValues.equals(newAvailableValues)) {
availableValues.clear();
availableValues.addAll(newAvailableValues);
updateComboBoxAvailableItems();
}
}
public <TT extends T> void setAvailableValues(TT... newAvailableValues) {
setAvailableValues(Arrays.asList(newAvailableValues));
}
public void setDisableWhenOnlyOneAvailableItem(boolean disableWhenOnlyOneAvailableItem) {
if (this.disableWhenOnlyOneAvailableItem != disableWhenOnlyOneAvailableItem) {
this.disableWhenOnlyOneAvailableItem = disableWhenOnlyOneAvailableItem;
updateComboBoxEnabled();
}
}
/**
* Sets whether this {@link DefaultSelector} is enabled. Even if it is
* enabled, the {@link #comboBox()} will be automatically disabled if there
* is only one available item and {@code disableWhenOnlyOneAvailableItem} is
* {@link #setDisableWhenOnlyOneAvailableItem(boolean) set} to {@code true}.
*/
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled;
updateComboBoxEnabled();
}
/**
* Sets the item displayed in the {@link #comboBox()} when no values are
* {@link #getAvailableValues() available}.
*/
public void setNothingAvailableItem(Object nothingAvailableItem) {
if (this.nothingAvailableItem != nothingAvailableItem) {
this.nothingAvailableItem = nothingAvailableItem;
updateComboBoxSelectedItem();
}
}
/**
* Sets the item displayed in the {@link #comboBox()} when the selection is
* {@link #setSelection(Object) set} to {@code null}.
*/
public void setNullItem(Object nullItem) {
if (this.nullItem != nullItem) {
this.nullItem = nullItem;
updateComboBoxSelectedItem();
}
}
/**
* Sets the selected value.
*
* @param newSelection
* the new desired selection. Has no effect if it is not in the
* list of available value.
*/
@Override
public void setSelection(T newSelection) {
if (!Java7.Objects.equals(newSelection, selection)) {
T oldSelection = selection;
selection = newSelection;
updateComboBoxSelectedItem();
notifySelectionChanged(oldSelection, newSelection);
}
}
/**
* Sets the item displayed in the {@link #comboBox()} when the selection is
* {@link #setSelection(Object) set} to a value not contained in the list of
* {@link #getAvailableValues() available} values. However, if
* {@link #setAllowSelectionNotAvailable(boolean)} to {@code true}, this
* item will not be used.
*/
public void setSelectionNotAvailableItem(Object selectionNotAvailableItem) {
if (this.selectionNotAvailableItem != selectionNotAvailableItem) {
this.selectionNotAvailableItem = selectionNotAvailableItem;
updateComboBoxSelectedItem();
}
}
protected void updateComboBoxAvailableItems() {
boolean listenersWereDisabled = disableListeners;
disableListeners = true;
try {
comboBox.removeAllItems();
for (T value : availableValues) {
comboBox.addItem(value);
}
updateComboBoxSelectedItem();
} finally {
disableListeners = listenersWereDisabled;
}
}
protected void updateComboBoxEnabled() {
comboBox.setEnabled(enabled && (availableValues.size() > 1 ||
availableValues.size() == 1 && !disableWhenOnlyOneAvailableItem));
}
protected void updateComboBoxSelectedItem() {
boolean listenersWereDisabled = disableListeners;
disableListeners = true;
try {
Object oldSelectedItem = comboBox.getSelectedItem();
Object newSelectedItem = pickItemToSelect();
if (oldSelectedItem != newSelectedItem) {
if (isTransientItem(newSelectedItem)) {
comboBox.insertItemAt(newSelectedItem, 0);
}
comboBox.setSelectedItem(newSelectedItem);
if (isTransientItem(oldSelectedItem)) {
comboBox.removeItem(oldSelectedItem);
}
}
updateComboBoxEnabled();
} finally {
disableListeners = listenersWereDisabled;
}
}
}