package org.freehep.application;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Properties;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
/**
* This class is ideal if you want to have a text field for user input but
* also have a list of recently selected items available to choose from.
* Some notes on using this class:
* <ul>
* <li>The default button will be clicked on the root pane of this
* component if the value of the constructor parameter <code>clickDefault</code>
* is <code>true</code>. This behaviour may be desirable because
* the default button is normally "clicked" when the user clicks on
* the "Enter" key, but this behaviour doesn't normally happen unless
* you add an ActionListener to the text field.</li>
* <li>Calling <code>addActionListener(ActionListener)</code> on this
* object causes the ActionListener to be added to the text field.</li>
* <li>A KeyEvent is generated for every time a key is pressed in the
* field, and all listeners are notified. KeyEvents normally generated
* by the JComboBox will not be sent by this class; they are suppressed.</li>
* <li>A ChangeEvent is generated any time the text in the field changes.
* Therefore, if you want to update a button state (for example) each time the
* text changes, simply implement a ChangeListener and add it to the
* listener list for this object. Otherwise, you would have to add a
* KeyListener and an ItemListener, and it's better just to have one.</li>
* </ul>
* It is important to point this out because the KeyEvents, ActionEvents,
* and ChangeEvents described above are not how a normal combo
* box will generate them.
* <p>
* Basically, a ChangeEvent is a dual purpose event. One is sent to listeners
* both when a KeyEvent is generated from the text field, and one is sent when
* an ItemEvent is generated (which happens when an item is selected from the
* drop-down list). This means that somebody who wants to know when the actual
* text showing has changed only needs to implement ChangeListener instead of
* both ItemListener and KeyListener.
* @author Jonas Gifford
* @version $Id: RecentItemTextField.java 8584 2006-08-10 23:06:37Z duns $
*/
public class RecentItemTextField extends JComboBox
{
public RecentItemTextField()
{
this(null,4,false);
}
/**
* Creates a RecentItemTextField with the given list of drop-down items. You are
* responsible for keeping and providing an updated list.
* @param dropDownItems the items to show in the drop-down list (can safely be <code>null</code>)
* @param clickDefault whether the default button should be clicked when Enter is pressed
* @see javax.swing.JRootPane#defaultButton
*/
public RecentItemTextField(String[] dropDownItems,boolean clickDefault)
{
m_clickDefault = clickDefault;
init(dropDownItems);
}
/**
* Creates a RecentItemTextField with a dropDown list that will be stored in
* the user properties object for the Application. Invoke the <code>saveState()</code>
* method to have the list be updated to include the selected item.
* @param key the key that the drop-down items will be stored by
* @param nItems the maximum number of items that will be stored on the drop-down list
* @param clickDefault whether the default button should be clicked when Enter is pressed
* @see Application#getUserProperties()
* @see javax.swing.JRootPane#defaultButton
*/
public RecentItemTextField(String key,int nItems,boolean clickDefault)
{
m_clickDefault = clickDefault;
Application app = Application.getApplication();
m_prop = app == null ? null : app.getUserProperties();
m_nItems = nItems;
m_key = key;
init(key != null && m_prop != null ? PropertyUtilities.getStringArray(m_prop,key,null) : null);
}
/**
* Creates a RecentItemTextField with a dropDown list that will be stored in
* the user properties object for the Application. Invoke the <code>saveState()</code>
* method to have the list be updated to include the selected item.
* @param key the key that the drop-down items will be stored by
* @param lengthKey the key that maps to the maximum number of items that will be stored on the drop-down list
* @param clickDefault whether the default button should be clicked when Enter is pressed
* @see Application#getUserProperties()
* @see javax.swing.JRootPane#defaultButton
*/
public RecentItemTextField(String key,String lengthKey,boolean clickDefault)
{
this(key, PropertyUtilities.getInteger(Application.getApplication().getUserProperties(),lengthKey, 4), clickDefault);
}
private void init(String[] dropDownItems)
{
m_dropDownItems = dropDownItems;
if (m_dropDownItems != null)
for (int i = 0; i < m_dropDownItems.length; i++)
addItem(m_dropDownItems[i]);
setEditable(true);
m_textField = (JTextField) getEditor().getEditorComponent();
m_textField.addKeyListener(myListener);
m_textField.getDocument().addDocumentListener(myListener);
}
public void addKeyListener(KeyListener kl)
{
if (m_textField != null)
m_textField.addKeyListener(kl);
}
/**
* If a key was supplied to the constructor, the drop-down list will be updated to
* include the currently selected item.
*/
public void saveState()
{
if (m_key != null && m_prop != null)
PropertyUtilities.setStringArray(m_prop, m_key, updateStringArray(m_dropDownItems, getText(), m_nItems));
}
/** Returns the text currently showing in the text field. */
public String getText()
{
return m_textField.getText();
}
/** Sets the text showing in the text field. */
public void setText(String s)
{
if (s.equals(getItemAt(0))) return; // it's already there; nothing to do
removeItem(s); // if the item is already there, just move it to the top
insertItemAt(s, 0); // put the new text at the top
setSelectedIndex(0);
}
private void fireStateChanged()
{
ChangeListener[] listeners = (ChangeListener[]) listenerList.getListeners(ChangeListener.class);
for (int i = listeners.length-1; i >= 0; i--)
{
if (m_changeEvent == null) m_changeEvent = new ChangeEvent(this);
listeners[i].stateChanged(m_changeEvent);
}
}
/**
* Change listeners will be notified any time the text visible in the text field changes.
* This is equivalent to having a KeyListener and an ItemListener.
* @param l the ChangeListener to add
*/
public void addChangeListener(ChangeListener l)
{
listenerList.add(ChangeListener.class, l);
}
/**
* Adds an the given ActionListener to the text field.
*/
public void addActionListener(ActionListener l)
{
m_textField.addActionListener(l);
}
/** This method is protected as an implementation side effect. */
protected void fireItemStateChanged(ItemEvent event)
{
super.fireItemStateChanged(event); // allow normal ItemEvent firing
fireStateChanged(); // when an ItemEvent is generated, also notify listeners of ChangeEvent
}
/** Requests focus for the text field of the box. */
public void requestFocus()
{
m_textField.requestFocus();
}
/**
* Set the maximum width of the box. The default is used if this method is not called.
* @param maxWidth give a number in pixels, or <code>-1</code> to use default
*/
public void setMaxWidth(int maxWidth)
{
m_maxWidth = maxWidth;
}
/**
* Set the minimum width of the box. The default is used if this method is not called.
* @param minWidth give a number in pixels, or <code>-1</code> to use default
*/
public void setMinWidth(int minWidth)
{
m_minWidth = minWidth;
}
/**
* Incorporates the maximum width if it has been set.
* @see #setMaxWidth(int)
*/
public Dimension getMaximumSize()
{
Dimension d = super.getMaximumSize();
if (m_maxWidth > 0 && m_maxWidth < d.width) d.width = m_maxWidth;
return d;
}
/**
* Incorporates the minimum width if it has been set.
* @see #setMinWidth(int)
*/
public Dimension getMinimumSize()
{
Dimension d = super.getMinimumSize();
if (m_minWidth > 0 && m_minWidth > d.width) d.width = m_minWidth;
return d;
}
/**
* Incorporates the minimum and maximum widths if they have been set.
* @see #setMinWidth(int)
* @see #setMaxWidth(int)
*/
public Dimension getPreferredSize()
{
Dimension d = super.getPreferredSize();
if (m_minWidth > 0 && m_minWidth > d.width) d.width = m_minWidth;
if (m_maxWidth > 0 && m_maxWidth < d.width) d.width = m_maxWidth;
return d;
}
/**
* Show an input dialog, with a record of recent entries.
* @param owner The dialog owner
* @param message The message to display
* @param key The key under which the recent items are stored in the user properties
* @see javax.swing.JOptionPane#showInputDialog(Component,Object)
*/
public static String showInputDialog(Component owner, Object message, String key)
{
return showInputDialog(owner,message,"Input",JOptionPane.QUESTION_MESSAGE,key);
}
/**
* Show an input dialog, with a record of recent entries.
* @param owner The dialog owner
* @param message The message to display
* @param title The dialog title
* @param messageType The message type
* @param key The key under which the recent items are stored in the user properties
* @see JOptionPane#showInputDialog(Component,Object,String,int)
*/
public static String showInputDialog(Component owner, Object message, String title, int messageType, String key)
{
RecentItemTextField field = new RecentItemTextField(key,4,true);
JPanel mpanel = new JPanel(new BorderLayout());
if (message instanceof Component) mpanel.add((JComponent) message);
else mpanel.add(new JLabel(message.toString()));
mpanel.add(field,BorderLayout.SOUTH);
int rc = JOptionPane.showOptionDialog(owner,mpanel,title,JOptionPane.OK_CANCEL_OPTION,messageType,null,null,null);
if (rc == JOptionPane.OK_OPTION)
{
field.saveState();
return field.getText();
}
else return null;
}
/**
* This is a utility method for updating a string array of recently used items.
* Supply it with an old array and a new item. If the new item was already in the
* old array, then it is simply moved to the beginning. If it was not in the old
* array then it is placed at the front and the other items are shuffled back.
* The method will return an array of a maximum size given by the <code>nStored</code> parameter.
* @param oldArray the array to update (may safely be <code>null</code>)
* @param newString the new item to include
* @param nStored the maximum size of the updated array
* @return the updated array
*/
private static String[] updateStringArray(String[] oldArray, String newString, int nStored)
{
if (newString == null) return oldArray;
if (oldArray != null && oldArray.length > 0)
{
if (oldArray[0].equals(newString)) return oldArray;
/*
* If the new String is already at the front, then
* there is nothing left to do.
*/
for (int i = 1; i < oldArray.length; i++)
/*
* Begin checking from the second element
* onwards: is the String already there?
*/
{
if (oldArray[i].equals(newString))
/*
* If it is already there, put
* the new one at the front and
* shuffle the others backward.
*/
{
while (i > 0)
{
oldArray[i] = oldArray[i-1];
i--;
}
oldArray[0] = newString;
return oldArray;
}
}
if (oldArray.length < nStored)
{
String[] result = new String[oldArray.length + 1];
result[0] = newString;
for (int i = 0; i < oldArray.length; i++)
result[i + 1] = oldArray[i];
return result;
}
else
{
for (int i = nStored-1; i > 0; i--)
oldArray[i] = oldArray[i-1];
oldArray[0] = newString;
return oldArray;
}
}
else
{
oldArray = new String[1];
oldArray[0] = newString;
return oldArray;
}
}
/**
* Getter for property key.
* @return Value of property key.
*/
public String getKey()
{
return m_key;
}
/**
* Setter for property key.
* @param key New value of property key.
*/
public void setKey(String key)
{
m_key = key;
if (key != null && m_prop != null) init(PropertyUtilities.getStringArray(m_prop,key,null));
}
/**
* Getter for property numberOfItems.
* @return Value of property numberOfItems.
*/
public int getNumberOfItems()
{
return m_nItems;
}
/**
* Setter for property numberOfItems.
* @param numberOfItems New value of property numberOfItems.
*/
public void setNumberOfItems(int numberOfItems)
{
m_nItems = numberOfItems;
}
/**
* Getter for property clickDefault.
* @return Value of property clickDefault.
*/
public boolean isClickDefault()
{
return m_clickDefault;
}
/**
* Setter for property clickDefault.
* @param clickDefault New value of property clickDefault.
*/
public void setClickDefault(boolean clickDefault)
{
m_clickDefault = clickDefault;
}
private int m_minWidth = -1, m_maxWidth = -1; // -1 indicates use default
private int m_nItems;
private String[] m_dropDownItems;
private String m_key = null;
private JTextField m_textField;
private ChangeEvent m_changeEvent = null;
private Properties m_prop = null;
private boolean m_clickDefault;
private MyListener myListener = new MyListener();
private class MyListener implements KeyListener, DocumentListener
{
public void keyReleased(KeyEvent e){}
public void keyTyped(KeyEvent e){}
public void keyPressed(KeyEvent e)
{
if (e.getSource() != m_textField) return; // avoid cases where this listens to another by accident
if (e.getKeyCode() == KeyEvent.VK_ENTER)
{
if (m_clickDefault)
{
JButton b = getRootPane().getDefaultButton();
if (b != null) b.doClick();
}
}
}
public void changedUpdate(DocumentEvent e)
{
fireStateChanged();
}
public void insertUpdate(DocumentEvent e)
{
fireStateChanged();
}
public void removeUpdate(DocumentEvent e)
{
fireStateChanged();
}
}
}