/*
* Copyright (c) 2010, Michael Grossmann, Nikolaus Moll
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the jo-widgets.org nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*/
package org.jowidgets.spi.impl.swing.common.widgets;
import java.awt.Component;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.ComboBoxEditor;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import org.jowidgets.common.color.IColorConstant;
import org.jowidgets.common.mask.TextMaskMode;
import org.jowidgets.common.types.InputChangeEventPolicy;
import org.jowidgets.common.types.Markup;
import org.jowidgets.common.verify.IInputVerifier;
import org.jowidgets.common.widgets.controller.IFocusListener;
import org.jowidgets.common.widgets.controller.IKeyListener;
import org.jowidgets.spi.impl.controller.FocusObservable;
import org.jowidgets.spi.impl.controller.InputObservable;
import org.jowidgets.spi.impl.controller.KeyObservable;
import org.jowidgets.spi.impl.mask.TextMaskVerifierFactory;
import org.jowidgets.spi.impl.swing.common.util.ColorConvert;
import org.jowidgets.spi.impl.swing.common.util.FontProvider;
import org.jowidgets.spi.impl.swing.common.widgets.event.LazyKeyEventContentFactory;
import org.jowidgets.spi.impl.swing.common.widgets.util.InputModifierDocument;
import org.jowidgets.spi.impl.verify.InputVerifierHelper;
import org.jowidgets.spi.widgets.IComboBoxSelectionSpi;
import org.jowidgets.spi.widgets.IComboBoxSpi;
import org.jowidgets.spi.widgets.setup.IComboBoxSelectionSetupSpi;
import org.jowidgets.spi.widgets.setup.IComboBoxSetupSpi;
import org.jowidgets.util.Tuple;
import org.jowidgets.util.event.IObservableCallback;
public class ComboBoxImpl extends AbstractInputControl implements IComboBoxSelectionSpi, IComboBoxSpi {
private final ComboBoxEditorImpl comboBoxEditor;
private Integer maxLength;
private final KeyObservable keyObservable;
private final FocusObservable focusObservable;
private final KeyListener keyListener;
private final boolean isAutoCompletionMode;
private final boolean isSelectionMode;
private boolean inputEventsEnabled;
private AutoCompletionModel autoCompletionModel;
private final IInputVerifier inputVerifier;
public ComboBoxImpl(final IComboBoxSelectionSetupSpi setup) {
super(new JComboBox());
this.inputEventsEnabled = true;
this.isAutoCompletionMode = setup.isAutoCompletion();
this.isSelectionMode = !(setup instanceof IComboBoxSetupSpi);
final boolean hasEditor = isAutoCompletionMode || !isSelectionMode;
getUiReference().setModel(createComboBoxModel(setup.getElements()));
final IInputVerifier maskVerifier;
String initialText = null;
if (setup instanceof IComboBoxSetupSpi) {
final IComboBoxSetupSpi comboBoxSetup = (IComboBoxSetupSpi) setup;
this.maxLength = comboBoxSetup.getMaxLength();
maskVerifier = TextMaskVerifierFactory.create(this, comboBoxSetup.getMask());
inputVerifier = InputVerifierHelper.getInputVerifier(maskVerifier, comboBoxSetup);
if (comboBoxSetup.getMask() != null && TextMaskMode.FULL_MASK == comboBoxSetup.getMask().getMode()) {
initialText = comboBoxSetup.getMask().getPlaceholder();
}
}
else {
inputVerifier = null;
maskVerifier = null;
maxLength = null;
}
if (hasEditor) {
getUiReference().setEditable(true);
this.comboBoxEditor = new ComboBoxEditorImpl(inputVerifier, setup.getInputChangeEventPolicy());
getUiReference().setEditor(comboBoxEditor);
if (initialText != null) {
setText(initialText);
}
this.keyListener = new KeyAdapter() {
@Override
public void keyReleased(final KeyEvent e) {
keyObservable.fireKeyReleased(new LazyKeyEventContentFactory(e));
}
@Override
public void keyPressed(final KeyEvent e) {
keyObservable.fireKeyPressed(new LazyKeyEventContentFactory(e));
}
};
final IObservableCallback keyObservableCallback = new IObservableCallback() {
@Override
public void onLastUnregistered() {
comboBoxEditor.removeKeyListener(keyListener);
}
@Override
public void onFirstRegistered() {
comboBoxEditor.addKeyListener(keyListener);
}
};
this.keyObservable = new KeyObservable(keyObservableCallback);
this.focusObservable = new FocusObservable();
comboBoxEditor.addFocusListener(new FocusListener() {
@Override
public void focusLost(final FocusEvent e) {
focusObservable.focusLost();
}
@Override
public void focusGained(final FocusEvent e) {
focusObservable.focusGained();
}
});
if (isAutoCompletionMode) {
getUiReference().addPopupMenuListener(new PopupMenuListener() {
private boolean canceled;
@Override
public void popupMenuWillBecomeVisible(final PopupMenuEvent e) {
canceled = false;
}
@Override
public void popupMenuWillBecomeInvisible(final PopupMenuEvent e) {
if (!canceled) {
comboBoxEditor.setCaretPosition(comboBoxEditor.getItem().length());
}
}
@Override
public void popupMenuCanceled(final PopupMenuEvent e) {
canceled = true;
}
});
}
}
else {
this.keyListener = new KeyAdapter() {
@Override
public void keyReleased(final KeyEvent e) {
keyObservable.fireKeyReleased(new LazyKeyEventContentFactory(e));
}
@Override
public void keyPressed(final KeyEvent e) {
keyObservable.fireKeyPressed(new LazyKeyEventContentFactory(e));
}
};
final IObservableCallback keyObservableCallback = new IObservableCallback() {
@Override
public void onLastUnregistered() {
getUiReference().removeKeyListener(keyListener);
}
@Override
public void onFirstRegistered() {
getUiReference().addKeyListener(keyListener);
}
};
this.keyObservable = new KeyObservable(keyObservableCallback);
this.focusObservable = new FocusObservable();
getUiReference().addFocusListener(new FocusListener() {
@Override
public void focusLost(final FocusEvent e) {
focusObservable.focusLost();
}
@Override
public void focusGained(final FocusEvent e) {
focusObservable.focusGained();
}
});
this.comboBoxEditor = null;
}
if (setup.getInputChangeEventPolicy() == InputChangeEventPolicy.ANY_CHANGE) {
getUiReference().addItemListener(new ItemListener() {
@Override
public void itemStateChanged(final ItemEvent e) {
if (e.getID() == ItemEvent.ITEM_STATE_CHANGED && e.getStateChange() == ItemEvent.SELECTED) {
fireInputChanged();
}
}
});
}
else if (setup.getInputChangeEventPolicy() == InputChangeEventPolicy.EDIT_FINISHED) {
getUiReference().addFocusListener(new FocusAdapter() {
@Override
public void focusLost(final FocusEvent e) {
fireInputChanged();
}
});
}
else {
throw new IllegalArgumentException(
"InputChangeEventPolicy '" + setup.getInputChangeEventPolicy() + "' is not known.");
}
}
private ComboBoxModel createComboBoxModel(final String[] elements) {
final ComboBoxModel result;
if (isAutoCompletionMode) {
if (isSelectionMode) {
int max = 0;
for (final String item : elements) {
max = Math.max(item.length(), max);
}
maxLength = max;
}
autoCompletionModel = new AutoCompletionModel(elements);
result = autoCompletionModel;
}
else {
result = new DefaultComboBoxModel(elements);
autoCompletionModel = null;
}
return result;
}
@Override
public JComboBox getUiReference() {
return (JComboBox) super.getUiReference();
}
@Override
public void setEditable(final boolean editable) {
getUiReference().setEnabled(editable);
if (comboBoxEditor != null) {
getUiReference().setEditable(editable);
}
}
@Override
public int getSelectedIndex() {
return getUiReference().getSelectedIndex();
}
@Override
public void setSelectedIndex(final int index) {
getUiReference().setSelectedIndex(index);
}
@Override
public String[] getElements() {
final ComboBoxModel model = getUiReference().getModel();
final String[] result = new String[model.getSize()];
for (int i = 0; i < result.length; i++) {
result[i] = (String) model.getElementAt(i);
}
return result;
}
@Override
public void setElements(final String[] elements) {
getUiReference().setModel(createComboBoxModel(elements));
}
@Override
public String getText() {
if (comboBoxEditor != null) {
return comboBoxEditor.getItem();
}
else {
return null;
}
}
@Override
public void setText(final String text) {
if (comboBoxEditor != null) {
comboBoxEditor.setItem(text);
}
}
@Override
public void setFontSize(final int size) {
if (comboBoxEditor != null) {
comboBoxEditor.setFontSize(size);
}
}
@Override
public void setFontName(final String fontName) {
if (comboBoxEditor != null) {
comboBoxEditor.setFontName(fontName);
}
}
@Override
public void setMarkup(final Markup markup) {
if (comboBoxEditor != null) {
comboBoxEditor.setMarkup(markup);
}
}
@Override
public void setSelection(final int start, final int end) {
if (comboBoxEditor != null) {
comboBoxEditor.setSelection(start, end);
}
}
@Override
public void select() {
if (comboBoxEditor != null) {
comboBoxEditor.selectAll();
}
}
@Override
public void setCaretPosition(final int pos) {
if (comboBoxEditor != null) {
comboBoxEditor.setCaretPosition(pos);
}
}
@Override
public int getCaretPosition() {
if (comboBoxEditor != null) {
return comboBoxEditor.getCaretPosition();
}
else {
return 0;
}
}
@Override
public void addKeyListener(final IKeyListener listener) {
keyObservable.addKeyListener(listener);
}
@Override
public void removeKeyListener(final IKeyListener listener) {
keyObservable.removeKeyListener(listener);
}
@Override
public void addFocusListener(final IFocusListener listener) {
focusObservable.addFocusListener(listener);
}
@Override
public void removeFocusListener(final IFocusListener listener) {
focusObservable.removeFocusListener(listener);
}
@Override
public void setForegroundColor(final IColorConstant colorValue) {
getUiReference().getEditor().getEditorComponent().setForeground(ColorConvert.convert(colorValue));
super.setForegroundColor(colorValue);
}
@Override
public void setBackgroundColor(final IColorConstant colorValue) {
getUiReference().getEditor().getEditorComponent().setBackground(ColorConvert.convert(colorValue));
super.setBackgroundColor(colorValue);
}
@Override
public void setPopupVisible(final boolean visible) {
getUiReference().setPopupVisible(true);
}
@Override
public boolean isPopupVisible() {
return getUiReference().isPopupVisible();
}
private void fireInputChanged() {
if (inputEventsEnabled) {
super.fireInputChanged(new Tuple<Integer, String>(getSelectedIndex(), getText()));
}
}
// Input Observable method
@Override
public void fireInputChanged(final Object value) {
if (inputEventsEnabled) {
super.fireInputChanged(new Tuple<Integer, String>(getSelectedIndex(), getText()));
}
}
private class ComboBoxEditorImpl implements ComboBoxEditor {
private boolean setItemInvoked;
private final JTextField textField;
private final InputModifierDocument modifierDocument;
private final InputObservable inputObservable;
private boolean keyPressedBackspace;
private boolean keyPressedDelete;
private boolean deleteOnSelection;
ComboBoxEditorImpl(final IInputVerifier inputVerifier, final InputChangeEventPolicy inputChangeEventPolicy) {
super();
this.textField = new JTextField();
final Border border = (Border) UIManager.get("ComboBox.editorBorder");
if (border != null) {
textField.setBorder(border);
}
this.setItemInvoked = false;
if (inputChangeEventPolicy == InputChangeEventPolicy.ANY_CHANGE) {
inputObservable = ComboBoxImpl.this;
}
else if (inputChangeEventPolicy == InputChangeEventPolicy.EDIT_FINISHED) {
inputObservable = null;
textField.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(final FocusEvent e) {
fireInputChanged();
}
});
}
else {
throw new IllegalArgumentException("InputChangeEventPolicy '" + inputChangeEventPolicy + "' is not known.");
}
if (isAutoCompletionMode) {
if (isSelectionMode) {
this.modifierDocument = new AutoCompletionSelectionDocument(
textField,
inputVerifier,
inputObservable,
maxLength);
}
else {
this.modifierDocument = new AutoCompletionDocument(textField, inputVerifier, inputObservable, maxLength);
}
}
else {
this.modifierDocument = new InputModifierDocument(textField, inputVerifier, inputObservable, maxLength);
}
this.textField.setDocument(modifierDocument);
this.textField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent e) {
if (e.getKeyChar() != KeyEvent.CHAR_UNDEFINED && getUiReference().isDisplayable()) {
if (isAutoCompletionMode && !getUiReference().isPopupVisible()) {
getUiReference().setPopupVisible(true);
}
if (!isAutoCompletionMode && getSelectedIndex() != -1) {
setSelectedIndex(-1);
}
}
keyPressedBackspace = (e.getKeyCode() == KeyEvent.VK_BACK_SPACE);
keyPressedDelete = (e.getKeyCode() == KeyEvent.VK_DELETE);
if (keyPressedBackspace || keyPressedDelete) {
deleteOnSelection = textField.getSelectionStart() != textField.getSelectionEnd();
if (!isAutoCompletionMode && getSelectedIndex() != -1) {
setSelectedIndex(-1);
}
}
}
});
}
public void addKeyListener(final KeyListener keyListener) {
textField.addKeyListener(keyListener);
}
public void removeKeyListener(final KeyListener keyListener) {
textField.removeKeyListener(keyListener);
}
public void addFocusListener(final FocusListener focusListener) {
textField.addFocusListener(focusListener);
}
public int getCaretPosition() {
return textField.getCaretPosition();
}
public void setCaretPosition(final int pos) {
textField.setCaretPosition(pos);
}
public void setSelection(final int start, final int end) {
textField.setSelectionStart(start);
textField.setSelectionEnd(end);
}
@Override
public Component getEditorComponent() {
return textField;
}
@Override
public void setItem(final Object anObject) {
if (!setItemInvoked) {
modifierDocument.setInputObservable(null);
setItemInvoked = true;
textField.setText((String) anObject);
setItemInvoked = false;
modifierDocument.setInputObservable(inputObservable);
}
}
@Override
public String getItem() {
return textField.getText();
}
@Override
public void selectAll() {
textField.selectAll();
}
public void setFontSize(final int size) {
textField.setFont(FontProvider.deriveFont(getUiReference().getFont(), size));
}
public void setFontName(final String fontName) {
textField.setFont(FontProvider.deriveFont(getUiReference().getFont(), fontName));
}
public void setMarkup(final Markup markup) {
textField.setFont(FontProvider.deriveFont(getUiReference().getFont(), markup));
}
@Override
public void addActionListener(final ActionListener listener) {}
@Override
public void removeActionListener(final ActionListener listener) {}
public void setSelectionStart(final int start) {
textField.setSelectionStart(start);
}
public void setSelectionEnd(final int end) {
textField.setSelectionEnd(end);
}
public void moveCaretPosition(final int pos) {
textField.moveCaretPosition(pos);
}
}
private final class AutoCompletionModel extends DefaultComboBoxModel {
private static final long serialVersionUID = 7679165913730011187L;
private final String[] elements;
AutoCompletionModel(final String[] elements) {
// TODO MG,NM review: copy instead?
this.elements = elements;
}
@Override
public int getSize() {
return elements.length;
}
@Override
public Object getElementAt(final int index) {
return elements[index];
}
}
private abstract class AbstractAutoCompletionDocument extends InputModifierDocument {
private static final long serialVersionUID = 1L;
private boolean programmaticModelUpdate;
//private final DocumentListener[] listenerList;
private int disableCount;
AbstractAutoCompletionDocument(
final JTextComponent textComponent,
final IInputVerifier inputVerifier,
final InputObservable inputObservable,
final Integer maxLength) {
super(textComponent, inputVerifier, inputObservable, maxLength);
disableCount = 0;
}
protected boolean isValidPrefix(final String text) {
final String lowerText = text.toLowerCase();
for (final String item : autoCompletionModel.elements) {
if (item.toLowerCase().startsWith(lowerText)) {
return true;
}
}
return false;
}
protected String getFirstMatch(final String text) {
final String lowerText = text.toLowerCase();
String firstMatch = null;
for (final String item : autoCompletionModel.elements) {
final String lowerItem = item.toLowerCase();
if (lowerItem.equals(lowerText)) {
return item;
}
if (firstMatch == null && (lowerItem.startsWith(lowerText))) {
firstMatch = item;
}
}
if (firstMatch != null) {
return firstMatch;
}
else if (isSelectionMode) {
return "";
}
else {
return text;
}
}
protected String getStringAfterReplacing(final int offset, final int length, final String text)
throws BadLocationException {
String result = getText(0, offset) + text;
if (offset + length < getLength()) {
result = result + getText(offset + length + 1, getLength() - offset - length);
}
return result;
}
protected void beginProgrammaticModelUpdate() {
programmaticModelUpdate = true;
}
protected void endProgrammaticModelUpdate() {
programmaticModelUpdate = false;
}
protected boolean isProgrammaticModelUpdate() {
return programmaticModelUpdate;
}
protected void disableEvents() {
if (disableCount == 0) {
inputEventsEnabled = false;
}
disableCount++;
}
protected void enableEvents() throws BadLocationException {
disableCount--;
if (disableCount == 0) {
inputEventsEnabled = true;
fireInputChanged();
}
}
}
private final class AutoCompletionDocument extends AbstractAutoCompletionDocument {
private static final long serialVersionUID = -745323548478120663L;
AutoCompletionDocument(
final JTextComponent textComponent,
final IInputVerifier inputVerifier,
final InputObservable inputObservable,
final Integer maxLength) {
super(textComponent, inputVerifier, inputObservable, maxLength);
}
@Override
public void insertString(final int offs, final String str, final AttributeSet a) throws BadLocationException {
// all changes are allowed
if (isProgrammaticModelUpdate()) {
return;
}
// check if changes are valid
final String text = getStringAfterReplacing(offs, 0, str);
if (!isValidPrefix(text)) {
// no auto completion
super.insertString(offs, str, a);
}
else {
disableEvents();
// do auto completion
final String autoCompletion = getFirstMatch(text);
beginProgrammaticModelUpdate();
autoCompletionModel.setSelectedItem(autoCompletion);
endProgrammaticModelUpdate();
super.remove(0, getLength());
super.insertString(0, autoCompletion, a);
if (comboBoxEditor != null) {
comboBoxEditor.setSelectionEnd(getLength());
comboBoxEditor.moveCaretPosition(offs + str.length());
}
enableEvents();
}
}
@Override
public void replace(final int offset, final int length, final String text, final AttributeSet attrs)
throws BadLocationException {
disableEvents();
super.replace(offset, length, text, attrs);
enableEvents();
}
}
private final class AutoCompletionSelectionDocument extends AbstractAutoCompletionDocument {
private static final long serialVersionUID = -745323548478120664L;
AutoCompletionSelectionDocument(
final JTextComponent textComponent,
final IInputVerifier inputVerifier,
final InputObservable inputObservable,
final Integer maxLength) {
super(textComponent, inputVerifier, inputObservable, maxLength);
}
@Override
public void insertString(int offs, final String str, final AttributeSet a) throws BadLocationException {
if (isProgrammaticModelUpdate()) {
return;
}
// check if changes are valid
String text = getStringAfterReplacing(offs, 0, str);
if (!isValidPrefix(text)) {
final Object item = getUiReference().getSelectedItem();
if (item == null) {
text = "";
}
else {
text = item.toString();
}
offs = offs - str.length();
UIManager.getLookAndFeel().provideErrorFeedback(getUiReference());
}
final String autoCompletion = getFirstMatch(text);
beginProgrammaticModelUpdate();
autoCompletionModel.setSelectedItem(autoCompletion);
endProgrammaticModelUpdate();
disableEvents();
super.remove(0, getLength());
enableEvents();
super.insertString(0, autoCompletion, a);
if (comboBoxEditor != null) {
comboBoxEditor.setSelectionEnd(getLength());
comboBoxEditor.setSelectionStart(offs + str.length());
}
}
@Override
public void remove(int offs, int len) throws BadLocationException {
if (isProgrammaticModelUpdate()) {
return;
}
if (isValidPrefix("") && offs == 0 && getLength() == len) {
beginProgrammaticModelUpdate();
autoCompletionModel.setSelectedItem("");
endProgrammaticModelUpdate();
super.remove(0, len);
}
else if (comboBoxEditor.keyPressedBackspace || comboBoxEditor.keyPressedDelete) {
if (comboBoxEditor.keyPressedBackspace && offs > 0 && comboBoxEditor.deleteOnSelection) {
offs--;
len++;
}
final String newText = getStringAfterReplacing(offs, len, "");
if (!isValidPrefix(newText)) {
len = getLength() - offs;
}
if (comboBoxEditor.keyPressedBackspace
|| (comboBoxEditor.keyPressedDelete && !comboBoxEditor.deleteOnSelection)) {
comboBoxEditor.setCaretPosition(getLength());
comboBoxEditor.moveCaretPosition(offs);
}
}
else {
super.remove(offs, len);
}
}
}
}