/*
* Copyright 2004 - 2008 Christian Sprajc. All rights reserved.
*
* This file is part of PowerFolder.
*
* PowerFolder 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.
*
* PowerFolder 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 PowerFolder. If not, see <http://www.gnu.org/licenses/>.
*
* $Id$
*/
package de.dal33t.powerfolder.ui.util;
import java.awt.Color;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.swing.ComboBoxEditor;
import javax.swing.JComboBox;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import com.jgoodies.binding.value.ValueModel;
import de.dal33t.powerfolder.util.StringUtils;
/**
* This item is a editable combo box, which manages a history. It writes to the
* textmodel on each entered character. Does not buffer until user presses
* enter. Values are added to the history when selected or entered.
*
* @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc </a>
* @version $Revision: 1.6 $
*/
public class EditableHistoryComboBox extends JComboBox {
private static final Logger log = Logger.getLogger(EditableHistoryComboBox.class.getName());
private static final int DEFAULT_HISTORY_LENGTH = 30;
private ValueModel textModel;
private int maxHistoryLength;
private String boxName;
private Preferences prefs;
private String infoText;
private EditorsDocumentListener editorsDocumentListener;
private boolean textModelChangedFromInnerBox;
private boolean selectionChangedFromTextModel;
// The number of documents events to ignore until write again to text model
private int nDocumentEventToIgnore;
/**
* Builds a textfield, which offers a dropdown box with the last number of
* entered values. History is not kept persistent
*
* @param textModel
* the model the text will be placed in
* @param maxHistoryLength
* the maximum number of history entries
*/
public EditableHistoryComboBox(ValueModel textModel, int maxHistoryLength) {
this(textModel, maxHistoryLength, null, null, null);
}
/**
* Builds a textfield, which offers a dropdown box with the last number of
* entered values. History may kept persistent in preferences in persistent
* name is given. Optional a info text can be provided, it is shown until
* the first time the focus is gained
*
* @param textModel
* the model the text will be placed in
* @param maxHistoryLength
* the maximum number of history entries
* @param boxName
* the persistent box name for the history
* @param prefs
* the prefs to store the history in
* @param infoText
* the info text displayed until first focus is gained
*/
public EditableHistoryComboBox(ValueModel textModel, int maxHistoryLength,
String boxName, Preferences prefs, String infoText)
{
super();
this.textModel = textModel;
this.maxHistoryLength = maxHistoryLength > 0 ? maxHistoryLength : DEFAULT_HISTORY_LENGTH;
this.boxName = boxName;
this.prefs = prefs;
this.infoText = infoText;
this.editorsDocumentListener = new EditorsDocumentListener();
this.selectionChangedFromTextModel = false;
this.textModelChangedFromInnerBox = false;
// Setup
super.setEditable(true);
// Load history from preferences
loadHistory();
// Listen to changes from "below"
textModel.addValueChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
if (!textModelChangedFromInnerBox) {
log.finer("Value changed from below to "
+ evt.getNewValue());
// Only set selected item if textmodel not changed from
// ourself
selectionChangedFromTextModel = true;
setSelectedItem(evt.getNewValue());
selectionChangedFromTextModel = false;
}
}
});
// Item/Selection listener
addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
if (selectionChangedFromTextModel) {
log.finer("Ingonoring Selection, came from textmodel directly item: "
+ e.getItem());
return;
}
if (e.getStateChange() == ItemEvent.SELECTED) {
log.finer("Selected item: " + e.getItem());
nDocumentEventToIgnore = 2;
// update text model value
setTextModelValue(getEditor().getItem());
// Add item to history if new
addToHistory(e.getItem());
}
}
});
// Behavior on editor chage
addPropertyChangeListener("editor", new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
ComboBoxEditor oldEditor = (ComboBoxEditor) evt.getOldValue();
ComboBoxEditor newEditor = (ComboBoxEditor) evt.getNewValue();
// Remove our document listener from the old editor
if (oldEditor != null
&& oldEditor.getEditorComponent() instanceof JTextField)
{
JTextField oldEditorsTextField = (JTextField) oldEditor
.getEditorComponent();
oldEditorsTextField.getDocument().removeDocumentListener(
editorsDocumentListener);
}
// Add our document listener to the new editors document
if (newEditor != null
&& newEditor.getEditorComponent() instanceof JTextField)
{
JTextField newEditorsTextField = (JTextField) newEditor
.getEditorComponent();
newEditorsTextField.getDocument().addDocumentListener(
editorsDocumentListener);
}
log.finer("Editor has changed");
}
});
// Register our document listner at current editors documents
if (getEditor().getEditorComponent() instanceof JTextField) {
JTextField field = (JTextField) getEditor().getEditorComponent();
if (!StringUtils.isBlank(infoText)) {
installInfoText(field, infoText);
}
field.getDocument().addDocumentListener(editorsDocumentListener);
}
}
public void updateInfoText() {
if (getEditor().getEditorComponent() instanceof JTextField) {
JTextField field = (JTextField) getEditor().getEditorComponent();
installInfoText(field, infoText);
}
}
/**
* Installs a preview info text on a TextField. The infotext will be shown
* gray. The info text will be removed when focus is gained
*
* @param field
* the field
* @param aInfoText
* the info text to display until
*/
private void installInfoText(final JTextField field, String aInfoText) {
final Color originalColor = field.getForeground();
final String originalText = field.getText();
// Set info text
field.setText(aInfoText);
field.setForeground(Color.GRAY);
FocusListener focusListener = new FocusListener() {
public void focusGained(FocusEvent e) {
// Reset field to origial values
log.finer("Resetting input field to orignal content");
field.setText(originalText);
field.setForeground(originalColor);
//field.removeFocusListener(this);
}
public void focusLost(FocusEvent e) {
//if no focus and empty text reset the infoText
if (field.getDocument().getLength() == 0) {
updateInfoText();
}
}
};
// Focuslister to reset field
field.addFocusListener(focusListener);
}
/**
* Overridden and disabled
*
* @see javax.swing.JComboBox#setEditable(boolean)
*/
public void setEditable(boolean editable) {
throw new IllegalStateException(
"Unable to change editable state of this component");
}
/**
* Sets the value from text model, while telling that the change is comming
* from inside this instance
* ignores it if it equals the infoText
* @param value
* the new value
*/
private void setTextModelValue(Object value) {
if (value instanceof String) {
String strValue = (String)value;
if (strValue.equals(infoText)) {
return;
}
}
textModelChangedFromInnerBox = true;
textModel.setValue(value);
textModelChangedFromInnerBox = false;
}
/**
* Adds an item to the history list. Removes old items if nesseary. Item
* won't be added if already in list. String items will be stored in
* preferences is box is persistent
*
* @param item
*/
private void addToHistory(Object item) {
addToHistory0(item);
// History only available for string items
if (prefs != null && item instanceof String) {
int prefIndex = prefs.getInt(boxName + ".startindex", 0);
prefIndex++;
if (prefIndex >= maxHistoryLength) {
prefIndex = 0;
}
prefs.put(boxName + "." + prefIndex, (String) item);
prefs.putInt(boxName + ".startindex", prefIndex);
}
}
/**
* Adds an item to the history list. Removes old items if nesseary. Item
* won't be added if already in list (FIXME).
*
* @param item
*/
private void addToHistory0(Object item) {
if (item == null) {
return;
}
// Not add empty string to history
if (item instanceof String && StringUtils.isBlank((String) item)) {
return;
}
// Not addd item if in list
int itemCount = getItemCount();
for (int i = 0; i < itemCount; i++) {
if (item.equals(getItemAt(i))) {
return;
}
}
log.finer("Adding to history: " + item);
// Add Item at top position
insertItemAt(item, 0);
//Remove old items
if (itemCount >= maxHistoryLength) {
// Remove last element
removeItemAt(itemCount);
}
}
/**
* Loads the history from the preferences if available
*/
private void loadHistory() {
if (prefs == null) {
return;
}
int prefIndex = prefs.getInt(boxName + ".startindex", 0);
prefIndex++;
if (prefIndex >= maxHistoryLength) {
prefIndex = 0;
}
for (int i = 0; i < maxHistoryLength; i++) {
String item = prefs.get(boxName + "." + prefIndex, null);
if (item != null) {
addToHistory0(item);
}
prefIndex++;
if (prefIndex >= maxHistoryLength) {
prefIndex = 0;
}
}
}
/**
* Document listener on the editors document of the JComboBox. Basically
* acts on document changes and gives it to the text model
*
* @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc </a>
* @version $Revision: 1.6 $
*/
private class EditorsDocumentListener implements DocumentListener {
public void changedUpdate(DocumentEvent e) {
if (nDocumentEventToIgnore <= 0) {
// LOG
// .warn("changedUpdate " + e.getOffset() + "/"
// + e.getLength());
setTextModelValue(getEditor().getItem());
} else {
nDocumentEventToIgnore--;
}
}
public void insertUpdate(DocumentEvent e) {
if (nDocumentEventToIgnore <= 0) {
//LOG.warn("insertUpdate " + e.getOffset() + "/" +
// e.getLength());
setTextModelValue(getEditor().getItem());
} else {
nDocumentEventToIgnore--;
}
}
public void removeUpdate(DocumentEvent e) {
if (nDocumentEventToIgnore <= 0) {
//LOG.warn("removeUpdate " + e.getOffset() + "/" +
// e.getLength());
setTextModelValue(getEditor().getItem());
} else {
nDocumentEventToIgnore--;
}
}
}
}