package org.appwork.swing.components.searchcombo;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Rectangle;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.ArrayList;
import javax.swing.ComboBoxEditor;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.Icon;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.plaf.basic.ComboPopup;
import org.appwork.app.gui.MigPanel;
import org.appwork.scheduler.SingleSchedule;
import org.appwork.utils.logging.Log;
import org.appwork.utils.swing.EDTRunner;
/**
* this component extends a normal combobox and implements a editable
* filter/autocompletion feature. <b> make sure that you model is sorted</b>
*
* @author thomas
*
* @param <T>
*/
public abstract class SearchComboBox<T> extends JComboBox {
class Editor implements ComboBoxEditor {
private final JTextField tf;
private final MigPanel panel;
private final JLabel icon;
private T value;
private final SingleSchedule sheduler;
private final Color defaultForeground;
private boolean setting;
public Editor() {
this.tf = new JTextField();
this.icon = new JLabel();
// editor panel
this.panel = new MigPanel("ins 0", "[][grow,fill]", "[grow,fill]");
this.panel.add(this.icon);
this.panel.setOpaque(true);
this.panel.setBackground(this.tf.getBackground());
this.tf.setBackground(null);
this.tf.setOpaque(false);
this.tf.putClientProperty("Synthetica.opaque", Boolean.FALSE);
this.defaultForeground = this.tf.getForeground();
this.panel.add(this.tf);
this.sheduler = new SingleSchedule(50);
// this.panel.setBorder(this.tf.getBorder());
this.tf.setBorder(null);
this.tf.addFocusListener(new FocusListener() {
@Override
public void focusGained(final FocusEvent e) {
Editor.this.tf.selectAll();
}
@Override
public void focusLost(final FocusEvent e) {
if (!Editor.this.autoComplete(false)) {
Editor.this.tf.setText(Editor.this.value == null ? "" : SearchComboBox.this.getText(Editor.this.value));
Editor.this.autoComplete(false);
}
}
});
this.tf.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void changedUpdate(final DocumentEvent e) {
Editor.this.auto();
}
@Override
public void insertUpdate(final DocumentEvent e) {
Editor.this.auto();
}
@Override
public void removeUpdate(final DocumentEvent e) {
// this would avoid usuage of backspace
// Editor.this.auto();
}
});
}
/*
* (non-Javadoc)
*
* @see javax.swing.ComboBoxEditor#addActionListener(java.awt.event.
* ActionListener)
*/
@Override
public void addActionListener(final ActionListener l) {
this.tf.addActionListener(l);
}
private void auto() {
if (this.setting) { return; }
// scheduler executes at least 50 ms after this submit.
this.sheduler.submit(new Runnable() {
@Override
public void run() {
Editor.this.autoComplete(true);
}
});
}
/**
* finds all possible matches of the entered text and sets the selected
* object
*/
protected boolean autoComplete(final boolean showPopup) {
final String txt = Editor.this.tf.getText();
if (this.value != null && SearchComboBox.this.getText(this.value).equals(txt)) { return true; }
String text = null;
final ArrayList<T> found = new ArrayList<T>();
for (int i = 0; i < SearchComboBox.this.getModel().getSize(); i++) {
text = SearchComboBox.this.getText((T) SearchComboBox.this.getModel().getElementAt(i));
if (text != null && text.startsWith(txt)) {
found.add((T) SearchComboBox.this.getModel().getElementAt(i));
}
}
new EDTRunner() {
@Override
protected void runInEDT() {
final int pos = Editor.this.tf.getCaretPosition();
if (found.size() == 0) {
Editor.this.tf.setForeground(SearchComboBox.this.getForegroundBad());
SearchComboBox.this.hidePopup();
} else {
Editor.this.tf.setForeground(Editor.this.defaultForeground);
// Editor.this.setItem(found.get(0));
SearchComboBox.this.setSelectedItem(found.get(0));
Editor.this.setItem(found.get(0));
Editor.this.tf.setCaretPosition(pos);
Editor.this.tf.select(txt.length(), Editor.this.tf.getText().length());
// Show popup, and scroll to correct position
if (found.size() > 1 && showPopup) {
// limit popup rows
SearchComboBox.this.setMaximumRowCount(found.size());
SearchComboBox.this.setPopupVisible(true);
// Scroll popup list, so that found[0] is the first
// entry. This is a bit "dirty", so we put it in a
// try catch...just to avoid EDT Exceptions
try {
final Object popup = SearchComboBox.this.getUI().getAccessibleChild(SearchComboBox.this, 0);
if (popup instanceof Container) {
final Component scrollPane = ((Container) popup).getComponent(0);
if (popup instanceof ComboPopup) {
final JList jlist = ((ComboPopup) popup).getList();
if (scrollPane instanceof JScrollPane) {
final Rectangle cellBounds = jlist.getCellBounds(SearchComboBox.this.getSelectedIndex(), SearchComboBox.this.getSelectedIndex() + found.size() - 1);
if (cellBounds != null) {
jlist.scrollRectToVisible(cellBounds);
}
}
}
}
} catch (final Throwable e) {
Log.exception(e);
}
} else {
SearchComboBox.this.hidePopup();
}
}
}
};
return found.size() > 0;
}
/*
* (non-Javadoc)
*
* @see javax.swing.ComboBoxEditor#getEditorComponent()
*/
@Override
public Component getEditorComponent() {
// TODO Auto-generated method stub
return this.panel;
}
/*
* (non-Javadoc)
*
* @see javax.swing.ComboBoxEditor#getItem()
*/
@Override
public Object getItem() {
// TODO Auto-generated method stub
return this.value;
}
/*
* (non-Javadoc)
*
* @see javax.swing.ComboBoxEditor#removeActionListener(java.awt.event.
* ActionListener)
*/
@Override
public void removeActionListener(final ActionListener l) {
this.tf.removeActionListener(l);
}
/*
* (non-Javadoc)
*
* @see javax.swing.ComboBoxEditor#selectAll()
*/
@Override
public void selectAll() {
this.tf.selectAll();
}
/*
* (non-Javadoc)
*
* @see javax.swing.ComboBoxEditor#setItem(java.lang.Object)
*/
@SuppressWarnings("unchecked")
@Override
public void setItem(final Object anObject) {
// if (this.value == anObject) { return; }
this.setting = true;
this.tf.setText(SearchComboBox.this.getText((T) anObject));
this.icon.setIcon(SearchComboBox.this.getIcon((T) anObject));
this.value = (T) anObject;
this.setting = false;
}
}
private Color foregroundBad = Color.red;
public SearchComboBox() {
super((ComboBoxModel) null);
this.setEditor(new Editor());
this.setEditable(true);
// we extends the existing renderer. this avoids LAF incompatibilities
final ListCellRenderer org = this.getRenderer();
this.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuCanceled(final PopupMenuEvent e) {
// TODO Auto-generated method stub
}
@Override
public void popupMenuWillBecomeInvisible(final PopupMenuEvent e) {
SearchComboBox.this.setMaximumRowCount(8);
}
@Override
public void popupMenuWillBecomeVisible(final PopupMenuEvent e) {
}
});
this.setRenderer(new ListCellRenderer() {
@SuppressWarnings("unchecked")
public Component getListCellRendererComponent(final JList list, final Object value, final int index, final boolean isSelected, final boolean cellHasFocus) {
try {
final JLabel ret = (JLabel) org.getListCellRendererComponent(list, SearchComboBox.this.getText((T) value), index, isSelected, cellHasFocus);
ret.setIcon(SearchComboBox.this.getIcon((T) value));
// ret.setOpaque(false);
return ret;
} catch (final Throwable e) {
// org might not be a JLabel (depending on the LAF)
// fallback here
return org.getListCellRendererComponent(list, SearchComboBox.this.getText((T) value), index, isSelected, cellHasFocus);
}
}
});
}
public Color getForegroundBad() {
return this.foregroundBad;
}
/**
* @param value
* @return
*/
abstract protected Icon getIcon(T value);
/**
* @param value
* @return
*/
abstract protected String getText(T value);
public void setForegroundBad(final Color forgroundGood) {
this.foregroundBad = forgroundGood;
}
/**
* Sets the Model for this combobox
*
* @param listModel
*/
public void setList(final ArrayList<T> listModel) {
super.setModel(new DefaultComboBoxModel(listModel.toArray(new Object[] {})));
}
/**
* Do not use this method. For Type Safty, please use
* {@link #setList(ArrayList)} instead
*
* @deprecated use {@link #setList(ArrayList)}
*/
@Deprecated
public void setModel(final ComboBoxModel aModel) {
if (aModel == null) {
super.setModel(new DefaultComboBoxModel());
return;
}
throw new RuntimeException("Use setList()");
}
}