001: package abbot.editor.widgets;
002:
003: import java.awt.*;
004: import java.awt.event.*;
005:
006: import javax.swing.*;
007: import javax.swing.event.*;
008:
009: import abbot.Log;
010:
011: /** A better text field with some useful features.
012: <ul>
013: <li>Fires when focus leaves the component.
014: <li>Selects all the contents when the action is fired to indicate the
015: contents were accepted.
016: <li>Until the user causes a notify-field-accept (usually with the
017: Enter key), the contents may be reverted to the original value by invoking
018: the field-revert action (bound to ESC by default).
019: <li>Actions are fired on all edits.
020: <li>The field has a fixed height.
021: <li>Pressing Enter when the field is blank will insert a default value, if
022: one has been provided.
023: </ul>
024: <p>
025: This functionality may be applied to any JTextField with the
026: {@link #decorate(JTextField)} method.
027: */
028: public class TextField extends JTextField {
029:
030: /** Action command when the field loses focus. */
031: public static final String ACTION_FOCUS_LOST = "focus-lost";
032: /** Action command when the text changes. */
033: public static final String ACTION_TEXT_CHANGED = "text-changed";
034: /** Action command when text is inserted. */
035: public static final String ACTION_TEXT_INSERTED = "text-inserted";
036: /** Action command when text is removed. */
037: public static final String ACTION_TEXT_REMOVED = "text-removed";
038: /** Action command when the field reverts to its original value. The
039: * action is equivalent to typing the original text and hitting "enter".
040: */
041: public static final String ACTION_TEXT_REVERTED = "text-reverted";
042:
043: private static final String REVERT_ACTION_NAME = "field-revert";
044:
045: public static boolean isDocumentAction(String action) {
046: return action == ACTION_TEXT_CHANGED
047: || action == ACTION_TEXT_INSERTED
048: || action == ACTION_TEXT_REMOVED;
049: }
050:
051: public static void decorate(JTextField tf) {
052: new Decorator(tf);
053: }
054:
055: public static void decorate(JTextField tf, String defaultValue) {
056: new Decorator(tf, defaultValue);
057: }
058:
059: /** Avoid recursive changes to the field's text. */
060: private boolean notifying;
061: private Decorator decorator;
062:
063: public TextField(String value, int columns) {
064: super (value, columns);
065: decorator = new Decorator(this );
066: }
067:
068: public TextField(String value, String defaultValue, int columns) {
069: super (value, columns);
070: decorator = new Decorator(this , defaultValue);
071: }
072:
073: public TextField(String value) {
074: super (value);
075: decorator = new Decorator(this );
076: }
077:
078: public TextField(String value, String defaultValue) {
079: super (value);
080: decorator = new Decorator(this , defaultValue);
081: }
082:
083: /** Don't allow text field to resize height. */
084: public Dimension getMaximumSize() {
085: Dimension size = super .getMaximumSize();
086: size.height = super .getPreferredSize().height;
087: return size;
088: }
089:
090: /** Don't allow text field to resize height. */
091: public Dimension getMinimumSize() {
092: Dimension size = super .getMinimumSize();
093: size.height = super .getPreferredSize().height;
094: return size;
095: }
096:
097: /** The default value will be inserted when the field is blank and ENTER
098: is pressed. This behavior is disabled if the value is null.
099: */
100: public void setDefaultValue(String value) {
101: decorator.setDefaultValue(value);
102: }
103:
104: public void setText(String text) {
105: if (!getText().equals(text) && !notifying)
106: super .setText(text != null ? text : "");
107: }
108:
109: protected void fireActionPerformed() {
110: notifying = true;
111: try {
112: super .fireActionPerformed();
113: } finally {
114: notifying = false;
115: }
116: }
117:
118: public static class Decorator {
119: private JTextField textField;
120: /** Text used when field is reverted. Updated on any
121: notify-field-accept action or when setText() is invoked directly.
122: */
123: private String revertText;
124: // whether to notify action listeners on every text change
125: private boolean continuousFire = true;
126: // Value to place in field when it is empty and Enter is hit
127: private String defaultValue;
128:
129: public Decorator(JTextField textField) {
130: this (textField, null);
131: }
132:
133: public Decorator(final JTextField textField, String defValue) {
134: this .textField = textField;
135: this .defaultValue = defValue;
136: textField
137: .addFocusListener(new java.awt.event.FocusAdapter() {
138: public void focusLost(
139: java.awt.event.FocusEvent ev) {
140: if (!ev.isTemporary()
141: && !isLocalMenuActive(textField)) {
142: fireActionPerformed(ACTION_FOCUS_LOST);
143: }
144: }
145: });
146: DocumentListener listener = new DocumentListener() {
147: public void changedUpdate(DocumentEvent ev) {
148: if (continuousFire) {
149: fireActionPerformed(ACTION_TEXT_CHANGED);
150: }
151: }
152:
153: public void insertUpdate(DocumentEvent ev) {
154: // If setText is called, update the revert text
155: String stack = Log.getStack(Log.FULL_STACK);
156: if (stack.indexOf("JTextComponent.setText") != -1) {
157: revertText = textField.getText();
158: }
159: if (continuousFire) {
160: fireActionPerformed(ACTION_TEXT_INSERTED);
161: }
162: }
163:
164: public void removeUpdate(DocumentEvent ev) {
165: if (continuousFire) {
166: fireActionPerformed(ACTION_TEXT_REMOVED);
167: }
168: }
169: };
170: textField.getDocument().addDocumentListener(listener);
171: textField.addActionListener(new ActionListener() {
172: public void actionPerformed(ActionEvent e) {
173: // Select all text when there is an effective commit,
174: // and make note of the new committed text.
175: // If the field is blank, set the default value if there
176: // is one.
177: String text = textField.getText();
178: if (!isDocumentAction(e.getActionCommand())) {
179: if (defaultValue != null && "".equals(text)) {
180: text = defaultValue;
181: SwingUtilities.invokeLater(new Runnable() {
182: public void run() {
183: textField.setText(defaultValue);
184: textField.selectAll();
185: }
186: });
187: }
188: revertText = text;
189: textField.selectAll();
190: }
191: }
192: });
193:
194: // Changing the input map doesn't work on the JComboBox editor,
195: // so use a key listener instead.
196: textField.addKeyListener(new KeyAdapter() {
197: public void keyPressed(KeyEvent e) {
198: if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
199: revertText();
200: }
201: }
202: });
203:
204: /*
205: // This would appear to be a better method for handling revert,
206: // but the following code doesn't work, and I can't figure out why
207: ActionMap am = textField.getActionMap();
208: am.put(REVERT_ACTION_NAME, new RevertFieldAction());
209:
210: InputMap im = textField.getInputMap();
211: im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
212: REVERT_ACTION_NAME);
213: */
214: // Initialize
215: revertText = textField.getText();
216: }
217:
218: private void setDefaultValue(String value) {
219: this .defaultValue = value;
220: }
221:
222: private void revertText() {
223: if (!textField.getText().equals(revertText)) {
224: textField.setText(revertText);
225: fireActionPerformed(ACTION_TEXT_REVERTED);
226: }
227: }
228:
229: private void fireActionPerformed(String command) {
230: textField.setActionCommand(command);
231: textField.postActionEvent();
232: textField.setActionCommand(null);
233: }
234:
235: /** Detect temporary focus loss due to menu activation (pre-1.4). */
236: private boolean isLocalMenuActive(JTextField field) {
237: Window window = SwingUtilities.getWindowAncestor(field);
238: if (window != null) {
239: Component comp = window.getFocusOwner();
240: return comp != null && (comp instanceof JMenuItem);
241: }
242: return false;
243: }
244:
245: protected class RevertFieldAction extends AbstractAction {
246: public RevertFieldAction() {
247: super (REVERT_ACTION_NAME);
248: }
249:
250: public void actionPerformed(ActionEvent e) {
251: revertText();
252: }
253: }
254: }
255: }
|