/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2012 Maxence Bernard
*
* muCommander 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 3 of the License, or
* (at your option) any later version.
*
* muCommander 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, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.ui.combobox;
import com.mucommander.commons.runtime.JavaVersion;
import javax.swing.*;
import javax.swing.plaf.basic.BasicComboBoxEditor;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Vector;
import java.util.WeakHashMap;
/**
* EditableComboBox is an editable combo box (really!) that can use a specified JTextField to be used as the editor.
*
* <p>EditableComboBox also extends JComboBox to make it much easier to use, instead of having to work around its
* numerous bugs and weird behavior (understatement). Registering a {@link EditableComboBoxListener} makes it
* easy to know for sure when an item has been selected from the combo popup menu, or when the text field has been
* validated ('Enter' key pressed) or cancelled ('Escape' key pressed). It is strongly recommanded to use this interface
* instead of ActionListener / ItemListener, their already erratic behavior could be further aggravated by the tweakings
* used in this class.
*
* <p>The {@link #setComboSelectionUpdatesTextField(boolean)} method allows to automatically replace the text field's
* contents when an item is selected from the associated combo box, replacing its value by the selected item's
* string representation. This feature is disabled by default.
*
* @see EditableComboBoxListener
* @author Maxence Bernard
*/
public class EditableComboBox extends SaneComboBox {
/** Used to render the content of the combo box. */
private ComboBoxCellRenderer renderer;
/** The text field used as the combo box's editor */
private JTextField textField;
/** Contains all registered EditableComboBoxListener instances, stored as weak references */
private WeakHashMap<EditableComboBoxListener, Object> listeners = new WeakHashMap<EditableComboBoxListener, Object>();
/** Specifies whether the text field's contents is updated when an item is selected in the associated combo box */
private boolean comboSelectionUpdatesTextField;
/**
* Creates a new editable combo box and a JTextField to be used as the editor.
* Has the same effect as calling {@link #EditableComboBox(javax.swing.JTextField)} with a null value.
*/
public EditableComboBox() {
init(null);
}
/**
* Creates a new editable combo box using the given text field as the editor.
*
* @param textField the text field to be used as the combo box's editor. If null, a new JTextField instance
* will be created and used.
*/
public EditableComboBox(JTextField textField) {
init(textField);
}
/**
* Creates a new editable combo box using the given text field as the editor and ComboBoxModel.
*
* @param textField the text field to be used as the combo box's editor. If null, a new JTextField instance
* will be created and used.
* @param comboBoxModel the ComboBoxModel to use for this combo box
*/
public EditableComboBox(JTextField textField, ComboBoxModel comboBoxModel) {
super(comboBoxModel);
init(textField);
}
/**
* Creates a new editable combo box using the given text field as the editor and items to populate the initial items list.
*
* @param textField the text field to be used as the combo box's editor. If null, a new JTextField instance
* will be created and used.
* @param items items used to populate the initial items list.
*/
public EditableComboBox(JTextField textField, Object[] items) {
super(items);
init(textField);
}
/**
* Creates a new editable combo box using the given text field as the editor and items to populate the initial items list.
*
* @param textField the text field to be used as the combo box's editor. If null, a new JTextField instance
* will be created and used.
* @param items items used to populate the initial items list.
*/
public EditableComboBox(JTextField textField, Vector<Object> items) {
super(items);
init(textField);
}
/**
* Returns the text field used as the combo box's editor.
*/
public JTextField getTextField() {return textField;}
/**
* If true is specified, when an item is selected in this combo box, the text field's contents
* will be automatically replaced by the selected item's string representation.
*/
public void setComboSelectionUpdatesTextField(boolean comboSelectionUpdatesTextField) {
this.comboSelectionUpdatesTextField = comboSelectionUpdatesTextField;
}
/**
* If true is returned, when an item is selected in this combo box, the text field's contents
* will be automatically replaced by the selected item's string representation.
* This feature is disabled by default (false is returned).
*/
public boolean getComboSelectionUpdatesTextField() {
return comboSelectionUpdatesTextField;
}
/**
* Initializes the combo box to make it editable and use the given text field.
*
* @param textField the text field to be used as the combo box's editor. If null, a JTextField instance will be created and used.
*/
private void init(JTextField textField) {
setRenderer(renderer = new ComboBoxCellRenderer());
// Create a new JTextField if no text field was specified
if(textField==null) {
this.textField = new JTextField();
}
// Use the specified text field
else
this.textField = textField;
// Use a custom editor that uses the text field
setEditor(new BasicComboBoxEditor() {
@Override
public Component getEditorComponent() {
return EditableComboBox.this.textField;
}
});
// Make this combo box editable
setEditable(true);
// Note: the default JComboBox behavior is to also fire an ActionEvent when enter is pressed on the text field
// and the popup menu is not visible, making the ActionEvent indistinguishable from a combo item selection.
// This awful behavior is overridden by keyPressed() so that ActionEvent is only fired for item selections.
// Listen to the text field's key events. These are fired regardless of the combo box popup menu being visible or not.
// The following KeyListener is added as an anonymous inner class so that any class overridding EditableComboBox
// can safely implement KeyListener without risking to override those methods by accident.
this.textField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent keyEvent) {
int keyCode = keyEvent.getKeyCode();
// Combo popup menu is visible
if(isPopupVisible()) {
if(keyCode==KeyEvent.VK_ENTER) {
// Under Java 1.5 or lower, we need to explicitely hide the popup.
if(JavaVersion.JAVA_1_5.isCurrentOrLower())
hidePopup();
// Note that since the event is not consumed, JComboBox will catch it and fire
}
else if(keyCode==KeyEvent.VK_ESCAPE) {
// Explicitely hide popup menu, JComboBox does not seem do it automatically (at least under Mac OS X + Java 1.5 and Java 1.4)
hidePopup();
// Consume the event so that it is not propagated, since dialogs catch this event to close the window
keyEvent.consume();
}
}
// Combo popup menu is not visible, these events really belong to the text field
else {
if(keyCode==KeyEvent.VK_ENTER) {
// Notify listeners that the text field has been validated
fireComboFieldValidated();
// /!\ Consume the event so to prevent JComboBox from firing an ActionEvent (default JComboBox behavior)
keyEvent.consume();
}
else if(keyCode==KeyEvent.VK_ESCAPE) {
// Notify listeners that the text field has been cancelled
fireComboFieldCancelled();
}
}
}
});
}
//////////////////////////////////////////////
// EditableComboBoxListener support methods //
//////////////////////////////////////////////
/**
* Adds the specified EditableComboBoxListener to the list of registered listeners.
*
* <p>Listeners are stored as weak references so {@link #removeEditableComboBoxListener(EditableComboBoxListener)}
* doesn't need to be called for listeners to be garbage collected when they're not used anymore.
*
* @param listener the EditableComboBoxListener to add to the list of registered listeners.
*/
public void addEditableComboBoxListener(EditableComboBoxListener listener) {
addComboBoxListener(listener);
listeners.put(listener, null);
}
/**
* Removes the specified EditableComboBoxListener from the list of registered listeners.
*
* @param listener the EditableComboBoxListener to remove from the list of registered listeners.
*/
public void removeEditableComboBoxListener(EditableComboBoxListener listener) {
removeComboBoxListener(listener);
listeners.remove(listener);
}
/**
* Overrides {@link SaneComboBox#fireComboBoxSelectionChanged()} to set the text field's contents to the item that
* has been selected, if {@link #setComboSelectionUpdatesTextField(boolean)} has been enabled.
*/
@Override
protected void fireComboBoxSelectionChanged() {
if(comboSelectionUpdatesTextField) {
// Replace the text field's contents by the selected item's string representation,
// only if this feature has been enabled
if(getSelectedIndex() != -1)
textField.setText(getSelectedItem().toString());
}
super.fireComboBoxSelectionChanged();
}
/**
* Notifies all registered EditableComboBoxListener instances that the text field has been validated, that is
* the 'Enter' key has been pressed in the text field, without the popup menu being visible.
*
* <p>Note: Unlike JComboBox's weird ActionEvent handling, this event is *not* fired when 'Enter' is pressed
* in the combo popup menu.
*/
protected void fireComboFieldValidated() {
// Iterate on all listeners
for(EditableComboBoxListener listener: listeners.keySet())
listener.textFieldValidated(this);
}
/**
* Notifies all registered EditableComboBoxListener instances that the text field has been cancelled, that is
* the 'Escape' key has been pressed in the text field, without the popup menu being visible.
*
* <p>Note: This event is *not* fired when 'Escape' is pressed in the combo popup menu.
*/
protected void fireComboFieldCancelled() {
// Iterate on all listeners
for(EditableComboBoxListener listener: listeners.keySet())
listener.textFieldCancelled(this);
}
// - Aspect managenement -------------------------------------------------------------
// -----------------------------------------------------------------------------------
@Override
public void setForeground(Color color) {
if(renderer == null)
super.setForeground(color);
else {
renderer.setForeground(color);
textField.setForeground(color);
}
}
@Override
public void setBackground(Color color) {
if(renderer == null)
super.setBackground(color);
else {
renderer.setBackground(color);
textField.setBackground(color);
}
}
public void setSelectionForeground(Color color) {
if(renderer != null) {
renderer.setSelectionForeground(color);
textField.setSelectedTextColor(color);
}
}
public void setSelectionBackground(Color color) {
if(renderer != null) {
renderer.setSelectionBackground(color);
textField.setSelectionColor(color);
}
}
@Override
public void setFont(Font font) {
super.setFont(font);
if(renderer != null) {
renderer.setFont(font);
textField.setFont(font);
}
}
}