/**
* Copyright (C) 2015 Valkyrie RCP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.valkyriercp.list;
import org.springframework.util.Assert;
import javax.swing.*;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainDocument;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.HashMap;
import java.util.Map;
/**
* Provides AutoCompletion to a combobox. Works with the editor of the JComboBox
* to make the conversion between strings and the objects of the JComboBox
* model. <br>
* Based on code contributed to the public domain by Thomas Bierhance
* (http://www.orbital-computer.de/JComboBox/)
*
* @author Peter De Bruycker
* @author Thomas Bierhance
*/
public class ComboBoxAutoCompletion extends PlainDocument {
private final ChangeHandler changeHandler = new ChangeHandler();
private final JComboBox comboBox;
private final JTextComponent editor;
boolean hitBackspace;
boolean hitBackspaceOnSelection;
private Map item2string = new HashMap();
private ComboBoxModel model;
private boolean selectingValue;
/**
* Adds autocompletion support to the given <code>JComboBox</code>.
*
* @param comboBox
* the combobox
*/
public ComboBoxAutoCompletion(final JComboBox comboBox) {
Assert.notNull(comboBox, "The ComboBox cannot be null.");
Assert.isTrue(!comboBox.isEditable(), "The ComboBox must not be editable.");
Assert.isTrue(comboBox.getEditor().getEditorComponent() instanceof JTextComponent,
"Only ComboBoxes with JTextComponent as editor are supported.");
this.comboBox = comboBox;
comboBox.setEditable(true);
model = comboBox.getModel();
model.addListDataListener(changeHandler);
editor = (JTextComponent)comboBox.getEditor().getEditorComponent();
editor.setDocument(this);
editor.addFocusListener(changeHandler);
editor.addKeyListener(changeHandler);
fillItem2StringMap();
// Handle initially selected object
Object selected = comboBox.getSelectedItem();
comboBox.getEditor().setItem(selected);
}
private void fillItem2StringMap() {
editor.setDocument(new PlainDocument());
item2string.clear();
JTextComponent editor = (JTextComponent)comboBox.getEditor().getEditorComponent();
// get current item of editor
Object currentItem = comboBox.getEditor().getItem();
for (int i = 0; i < comboBox.getItemCount(); i++) {
Object item = comboBox.getItemAt(i);
comboBox.getEditor().setItem(item);
item2string.put(item, editor.getText());
}
// reset item in editor
comboBox.getEditor().setItem(currentItem);
editor.setDocument(this);
}
private String getStringFor(Object item) {
return (String)item2string.get(item);
}
private void highlightCompletedText(int start) {
editor.setCaretPosition(getLength());
editor.moveCaretPosition(start);
}
/**
* @see javax.swing.text.Document#insertString(int, java.lang.String,
* javax.swing.text.AttributeSet)
*/
public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
// ignore empty insert
if (str == null || str.length() == 0)
return;
if(selectingValue)
return;
// check offset position
if (offs < 0 || offs > getLength())
throw new BadLocationException("Invalid offset - must be >= 0 and <= " + getLength(), offs);
// construct the resulting string
String currentText = getText(0, getLength());
String beforeOffset = currentText.substring(0, offs);
String afterOffset = currentText.substring(offs, currentText.length());
String futureText = beforeOffset + str + afterOffset;
// lookup and select a matching item
Object item = lookupItem(futureText);
if (item != null) {
selectingValue = true;
try {
comboBox.setSelectedItem(item);
} finally {
selectingValue = false;
}
}
else {
// keep old item selected if there is no match
item = comboBox.getSelectedItem();
// imitate no insert (later on offs will be incremented by
// str.length(): selection won't move forward)
offs = offs - str.length();
// provide feedback to the user that his input has been received but
// can not be accepted
// comboBox.getToolkit().beep();
// when available use:
UIManager.getLookAndFeel().provideErrorFeedback(comboBox);
}
// display the completed string
String itemString = item == null ? "" : getStringFor(item);
setText(itemString);
// if the user selects an item via mouse the the whole string will be
// inserted.
// highlight the entire text if this happens.
if (itemString != null) {
if (itemString.equals(str) && offs == 0) {
highlightCompletedText(0);
}
else {
highlightCompletedText(offs + str.length());
// show popup when the user types
if (comboBox.isShowing()) {
comboBox.setPopupVisible(true);
}
}
}
}
private Object lookupItem(String pattern) {
Object selectedItem = model.getSelectedItem();
// only search for a different item if the currently selected does not
// match
if (selectedItem != null && startsWithIgnoreCase(getStringFor(selectedItem), pattern)) {
return selectedItem;
}
// iterate over all items
for (int i = 0, n = model.getSize(); i < n; i++) {
Object currentItem = model.getElementAt(i);
// current item starts with the pattern?
if (startsWithIgnoreCase(getStringFor(currentItem), pattern)) {
return currentItem;
}
}
// no item starts with the pattern => return null
return null;
}
/**
* @see javax.swing.text.Document#remove(int, int)
*/
public void remove(int offs, int length) throws BadLocationException {
// ignore no deletion
if (length == 0)
return;
// check positions
if (offs < 0 || offs > getLength() || length < 0 || (offs + length) > getLength())
throw new BadLocationException("Invalid parameters.", offs);
if (hitBackspace) {
// user hit backspace => move the selection backwards
// old item keeps being selected
if (offs > 0) {
if (hitBackspaceOnSelection)
offs--;
}
else {
// User hit backspace with the cursor positioned on the start =>
// beep
comboBox.getToolkit().beep();
// when available use:
// UIManager.getLookAndFeel().provideErrorFeedback(comboBox);
}
highlightCompletedText(offs);
// show popup when the user types
if (comboBox.isShowing())
comboBox.setPopupVisible(true);
}
else {
super.remove(offs, length);
}
}
private void setText(String text) throws BadLocationException {
// remove all text and insert the new text
super.remove(0, getLength());
super.insertString(0, text, null);
}
// checks if str1 starts with str2 - ignores case
private boolean startsWithIgnoreCase(String str1, String str2) {
return str1 != null && str2 != null && str1.toUpperCase().startsWith(str2.toUpperCase());
}
private final class ChangeHandler extends KeyAdapter implements FocusListener, ListDataListener {
// Highlight whole text when user hits enter
// Register when user hits backspace
public void keyPressed(KeyEvent e) {
hitBackspace = false;
switch (e.getKeyCode()) {
case KeyEvent.VK_ENTER:
highlightCompletedText(0);
break;
// determine if the pressed key is backspace (needed by the remove
// method)
case KeyEvent.VK_BACK_SPACE:
hitBackspace = true;
hitBackspaceOnSelection = editor.getSelectionStart() != editor.getSelectionEnd();
break;
// ignore delete key
case KeyEvent.VK_DELETE:
e.consume();
ComboBoxAutoCompletion.this.comboBox.getToolkit().beep();
break;
}
}
// Bug 5100422 on Java 1.5: Editable JComboBox won't hide popup when
// tabbing out
private boolean hidePopupOnFocusLoss = System.getProperty("java.version").startsWith("1.5");
public void focusGained(FocusEvent e) {
// Highlight whole text when gaining focus
highlightCompletedText(0);
}
public void focusLost(FocusEvent e) {
// Workaround for Bug 5100422 - Hide Popup on focus loss
if (hidePopupOnFocusLoss)
ComboBoxAutoCompletion.this.comboBox.setPopupVisible(false);
}
public void contentsChanged(ListDataEvent e) {
if(!selectingValue)
fillItem2StringMap();
}
public void intervalAdded(ListDataEvent e) {
fillItem2StringMap();
}
public void intervalRemoved(ListDataEvent e) {
fillItem2StringMap();
}
}
}